Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 17 additions & 62 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import (
"github.com/urfave/cli/v3"

"github.com/livekit/livekit-cli/v2/pkg/agentfs"
"github.com/livekit/livekit-cli/v2/pkg/bootstrap"
"github.com/livekit/livekit-cli/v2/pkg/config"
"github.com/livekit/livekit-cli/v2/pkg/util"
lkproto "github.com/livekit/protocol/livekit"
Expand Down Expand Up @@ -240,36 +239,24 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs",
return createAgentClientWithOpts(ctx, cmd, confirmProject)
},
Action: initAgent,
MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{
Flags: [][]cli.Flag{{
&cli.StringFlag{
Name: "lang",
Usage: "`LANGUAGE` of the starter template, one of \"python\", \"node\".",
Action: func(ctx context.Context, cmd *cli.Command, l string) error {
if _, ok := starterTemplateURLs[l]; !ok {
return fmt.Errorf("unsupported language: %s", l)
}
return nil
},
},
&cli.BoolFlag{
Name: "deploy",
Usage: "If set, automatically deploys the agent to LiveKit Cloud after initialization.",
Value: false,
},
templateFlag,
templateURLFlag,
}, {
sandboxFlag,
&cli.BoolFlag{
Name: "no-sandbox",
Usage: "If set, will not create a sandbox for the project.",
Value: true,
Hidden: true,
},
}},
}},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "lang",
Usage: "`LANGUAGE` of the starter template, one of \"python\", \"node\".",
Action: func(ctx context.Context, cmd *cli.Command, l string) error {
if _, ok := starterTemplateURLs[l]; !ok {
return fmt.Errorf("unsupported language: %s", l)
}
return nil
},
},
&cli.BoolFlag{
Name: "deploy",
Usage: "If set, automatically deploys the agent to LiveKit Cloud after initialization.",
Value: false,
},
templateFlag,
templateURLFlag,
regionFlag,
installFlag,
skillsSetupFlag,
Expand Down Expand Up @@ -613,38 +600,6 @@ func initAgent(ctx context.Context, cmd *cli.Command) error {
appName = project.Name
}

// Create sandbox only when not disabled by flag and we don't already have one
if !cmd.Bool("no-sandbox") && sandboxID == "" {
if err := out.Await("Creating sandbox app...", ctx, func(ctx context.Context) error {
token, err := requireToken(ctx, cmd)
if err != nil {
return err
}

// TODO: (@rektdeckard) figure out why AccessKeyProvider does not immediately
// have access to newly-created API keys, then remove this sleep
time.Sleep(4 * time.Second)
sandboxID, err = bootstrap.CreateSandbox(
ctx,
appName,
// NOTE: we may want to support embed sandbox in the future
"https://github.com/livekit-examples/agent-starter-react",
token,
serverURL,
)

// We set sandbox ID in env for use in template tasks
os.Setenv("LIVEKIT_SANDBOX_ID", sandboxID)

return err
}); err != nil {
return fmt.Errorf("failed to create sandbox: %w", err)
} else {
out.Status("Creating sandbox app...")
out.Statusf("Created sandbox app [%s]", util.Accented(sandboxID))
}
}

// Run template bootstrap
shouldDeploy := cmd.Bool("deploy")
if shouldDeploy {
Expand Down
38 changes: 4 additions & 34 deletions cmd/lk/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ var (
template *bootstrap.Template
templateName string
templateURL string
sandboxID string
appName string
appNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_-]*$`)
destinationFile string
Expand All @@ -63,7 +62,6 @@ var (
Flags: []cli.Flag{
templateFlag,
templateURLFlag,
sandboxFlag,
installFlag,
},
},
Expand Down Expand Up @@ -291,10 +289,8 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error {
func setupTemplateWith(ctx context.Context, cmd *cli.Command, afterInstall func(ctx context.Context, cmd *cli.Command, dir string) error) error {
verbose := cmd.Bool("verbose")
install := cmd.Bool("install")
isSandbox := sandboxID != ""

var preinstallPrompts []huh.Field
var templateOptions []bootstrap.Template

if templateName != "" && templateURL != "" {
return errors.New("only one of template or template-url can be specified")
Expand All @@ -304,31 +300,9 @@ func setupTemplateWith(ctx context.Context, cmd *cli.Command, afterInstall func(
return errors.New("non-interactive mode: set --template or --template-url")
}

if isSandbox {
_, err := requireAppProject(ctx, cmd)
if err != nil {
return err
}
token, err := requireToken(ctx, cmd)
if err != nil {
return err
}
if templateURL == "" {
details, err := bootstrap.FetchSandboxDetails(ctx, sandboxID, token, serverURL)
if err != nil {
return err
}
if len(details.ChildTemplates) == 0 {
return errors.New("no child templates found for sandbox")
}
templateOptions = details.ChildTemplates
}
} else {
var err error
templateOptions, err = bootstrap.FetchTemplates(ctx)
if err != nil {
return err
}
templateOptions, err := bootstrap.FetchTemplates(ctx)
if err != nil {
return err
}

// if no template name or URL is specified, prompt user to choose from available templates
Expand Down Expand Up @@ -418,10 +392,6 @@ func setupTemplateWith(ctx context.Context, cmd *cli.Command, afterInstall func(
}

out.Status("Instantiating environment...")
addlEnv := &map[string]string{
"LIVEKIT_SANDBOX_ID": sandboxID,
"NEXT_PUBLIC_LIVEKIT_SANDBOX_ID": sandboxID,
}
envOutputFile := ".env.local"
envExampleFile := ".env.example"
if tf != nil {
Expand All @@ -436,7 +406,7 @@ func setupTemplateWith(ctx context.Context, cmd *cli.Command, afterInstall func(
}
}
}
env, err := instantiateEnv(ctx, cmd, appName, addlEnv, envExampleFile, nil)
env, err := instantiateEnv(ctx, cmd, appName, nil, envExampleFile, nil)
if err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/lk/app_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import (
//
// Session tokens cover the CLI's own calls, but an app or agent runs on its own
// and needs a real API key. Commands that write credentials into an app
// (`lk app create`, `lk app env`, `lk agent init`) or sign with the secret
// themselves (sandbox tokens) call ensureProjectAPIKey, which reuses a key
// already in the app's env files or generates a service-account key.
// (`lk app create`, `lk app env`, `lk agent init`) call ensureProjectAPIKey,
// which reuses a key already in the app's env files or generates a
// service-account key.
// Commands that run an agent (`lk agent dev`, `start`, the debugger) only ever
// read the key from the agent's env files (agentEnvCredentials): running an
// agent never creates a key.
Expand Down Expand Up @@ -93,7 +93,7 @@ func agentEnvCredentials(ctx context.Context, cmd *cli.Command, dir string) (age
}

// ensureProjectAPIKey gives a session project real API credentials, for
// commands that write them into an app in dir or sign with the secret. It
// commands that write them into an app in dir. It
// reuses a key for the same project already in one of dir's envFiles, and
// otherwise generates a service-account key, which outlives the user's
// membership in the project. A project loaded with an API key is left alone.
Expand Down
9 changes: 0 additions & 9 deletions cmd/lk/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,6 @@ func handleAuth(ctx context.Context, cmd *cli.Command) error {
}

func requireToken(ctx context.Context, cmd *cli.Command) (string, error) {
if experimentalAuthEnabled(cmd) {
if _, err := requireProjectForSDK(ctx, cmd); err != nil {
return "", err
}
// The identity below is derived from the secret, so this needs a real key.
if err := ensureProjectAPIKey(ctx, cmd, ""); err != nil {
return "", err
}
}
if project == nil {
var err error
project, err = loadProjectDetails(cmd)
Expand Down
6 changes: 0 additions & 6 deletions cmd/lk/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,6 @@ var (
Usage: "`URL` to instantiate, must contain a taskfile.yaml",
Destination: &templateURL,
}
sandboxFlag = &cli.StringFlag{
Name: "sandbox",
Usage: "`NAME` of the sandbox, see your cloud dashboard",
Destination: &sandboxID,
Hidden: true,
}
installFlag = &cli.BoolFlag{
Name: "install",
Usage: "Run installation after creating the application",
Expand Down
112 changes: 14 additions & 98 deletions pkg/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@
// limitations under the License.

// This package defines the configuration file associated with LiveKit
// templates, which include instructions for package setup and development in
// both Playground mode and local dev.
// templates, which include instructions for package setup and local dev.
package bootstrap

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -40,18 +37,14 @@ import (
"github.com/joho/godotenv"
"gopkg.in/yaml.v3"

authutil "github.com/livekit/livekit-cli/v2/pkg/auth"
"github.com/livekit/livekit-cli/v2/pkg/util"
)

const (
TaskFile = "taskfile.yaml"
TemplateIndexFile = "templates.yaml"
TemplateIndexURL = "https://raw.githubusercontent.com/livekit-examples/index/main"
TemplateBaseURL = "https://github.com/livekit-examples"
SandboxDashboardURL = "https://cloud.livekit.io/projects/p_/sandbox"
SandboxTemplateEndpoint = "/api/sandbox/template"
SandboxCreateEndpoint = "/api/sandbox/create"
TaskFile = "taskfile.yaml"
TemplateIndexFile = "templates.yaml"
TemplateIndexURL = "https://raw.githubusercontent.com/livekit-examples/index/main"
TemplateBaseURL = "https://github.com/livekit-examples"
)

type KnownTask string
Expand All @@ -75,22 +68,15 @@ var templateIgnoreFiles = []string{
}

type Template struct {
Name string `yaml:"name" json:"name"`
Desc string `yaml:"desc" json:"description,omitempty"`
URL string `yaml:"url" json:"url,omitempty"`
Docs string `yaml:"docs" json:"docs_url,omitempty"`
Image string `yaml:"image" json:"image_ref,omitempty"`
Tags []string `yaml:"tags" json:"tags,omitempty"`
Attrs map[string]string `yaml:"attrs" json:"attrs,omitempty"`
Requires []string `yaml:"requires" json:"requires,omitempty"`
IsSandbox bool `yaml:"is_sandbox" json:"is_sandbox,omitempty"`
IsHidden bool `yaml:"is_hidden" json:"is_hidden,omitempty"`
}

type SandboxDetails struct {
Name string `json:"name"`
Template Template `json:"template"`
ChildTemplates []Template `json:"childTemplates"`
Name string `yaml:"name" json:"name"`
Desc string `yaml:"desc" json:"description,omitempty"`
URL string `yaml:"url" json:"url,omitempty"`
Docs string `yaml:"docs" json:"docs_url,omitempty"`
Image string `yaml:"image" json:"image_ref,omitempty"`
Tags []string `yaml:"tags" json:"tags,omitempty"`
Attrs map[string]string `yaml:"attrs" json:"attrs,omitempty"`
Requires []string `yaml:"requires" json:"requires,omitempty"`
IsHidden bool `yaml:"is_hidden" json:"is_hidden,omitempty"`
}

func FetchTemplates(ctx context.Context) ([]Template, error) {
Expand All @@ -106,76 +92,6 @@ func FetchTemplates(ctx context.Context) ([]Template, error) {
return templates, nil
}

func CreateSandbox(ctx context.Context, agentName, templateURL, token, serverURL string) (string, error) {
type createSandboxRequest struct {
TemplateURL string `json:"template_url"`
AgentName string `json:"agent_name,omitempty"`
}

type createSandboxResponse struct {
SandboxID string `json:"sandbox_id"`
}

body := createSandboxRequest{
TemplateURL: templateURL,
AgentName: agentName,
}

buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(body); err != nil {
panic(err)
}

req, err := http.NewRequestWithContext(ctx, "POST", serverURL+SandboxCreateEndpoint, buf)
req.Header = authutil.NewHeaderWithToken(token)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", errors.New(resp.Status)
}

var response createSandboxResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return "", err
}
return response.SandboxID, nil
}

func FetchSandboxDetails(ctx context.Context, sid, token, serverURL string) (*SandboxDetails, error) {
req, err := http.NewRequestWithContext(ctx, "GET", serverURL+SandboxTemplateEndpoint, nil)
req.Header = authutil.NewHeaderWithToken(token)
query := req.URL.Query()
query.Add("id", sid)
req.URL.RawQuery = query.Encode()
if err != nil {
return nil, err
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == 404 {
return nil, fmt.Errorf("sandbox not found: %s", sid)
}
if resp.StatusCode != 200 {
return nil, errors.New(resp.Status)
}

var details SandboxDetails
if err := json.NewDecoder(resp.Body).Decode(&details); err != nil {
return nil, err
}
return &details, nil
}

func ParseTaskfile(rootPath string) (*ast.Taskfile, error) {
os.Setenv("TASK_X_REMOTE_TASKFILES", "1")
experiments.Parse(rootPath)
Expand Down
Loading