diff --git a/Makefile b/Makefile index 9c225253..5e2b68e7 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,16 @@ -.PHONY: build lint generate test test_install test_install_linux test_install_alpine test_install_debian +.PHONY: build fmt lint test generate test_install test_install_linux test_install_alpine test_install_debian build: lint generate @go build -o agentuity -lint: +fmt: @go fmt ./... @go vet ./... @go mod tidy +lint: + @make fmt + generate: @echo "Running go generate..." @go generate ./... @@ -25,6 +28,12 @@ test_install_debian: @docker run -it --rm agentuity-test-install-debian test: - @go test -race ./... - -test_install: test_install_linux test_install_alpine test_install_debian \ No newline at end of file + @make fmt + @make lint + @make generate + @go test -v -count=1 -race ./... + @make test_install_linux + @make test_install_alpine + @make test_install_debian + +test_install: test_install_linux test_install_alpine test_install_debian diff --git a/cmd/agent.go b/cmd/agent.go index ea302acf..de4578d4 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -22,6 +22,7 @@ import ( "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/logger" + cproject "github.com/agentuity/go-common/project" "github.com/agentuity/go-common/slice" "github.com/agentuity/go-common/tui" "github.com/charmbracelet/lipgloss/tree" @@ -48,7 +49,8 @@ var agentDeleteCmd = &cobra.Command{ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() theproject := project.EnsureProject(ctx, cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API if !tui.HasTTY && len(args) == 0 { logger.Fatal("No TTY detected, please specify an Agent id from the command line") @@ -99,7 +101,7 @@ var agentDeleteCmd = &cobra.Command{ maybedelete = append(maybedelete, agent.Filename) } } - var agents []project.AgentConfig + var agents []cproject.AgentConfig for _, agent := range theproject.Project.Agents { if !slice.Contains(deleted, agent.ID) { agents = append(agents, agent) @@ -231,7 +233,8 @@ var agentCreateCmd = &cobra.Command{ logger := env.NewLogger(cmd) theproject := project.EnsureProject(ctx, cmd) apikey := theproject.Token - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API var remoteAgents []agent.Agent @@ -328,7 +331,7 @@ var agentCreateCmd = &cobra.Command{ errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithAttributes(map[string]any{"name": name})).ShowErrorAndExit() } - theproject.Project.Agents = append(theproject.Project.Agents, project.AgentConfig{ + theproject.Project.Agents = append(theproject.Project.Agents, cproject.AgentConfig{ ID: agentID, Name: name, Description: description, @@ -392,8 +395,8 @@ func reconcileAgentList(logger logger.Logger, cmd *cobra.Command, apiUrl string, } // make a map of the agents in the agentuity config file - fileAgents := make(map[string]project.AgentConfig) - fileAgentsByID := make(map[string]project.AgentConfig) + fileAgents := make(map[string]cproject.AgentConfig) + fileAgentsByID := make(map[string]cproject.AgentConfig) for _, agent := range theproject.Project.Agents { key := normalAgentName(agent.Name, theproject.Project.IsPython()) if existing, ok := fileAgents[key]; ok { @@ -598,7 +601,8 @@ var agentListCmd = &cobra.Command{ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() project := project.EnsureProject(ctx, cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API // perform the reconcilation keys, state := reconcileAgentList(logger, cmd, apiUrl, project.Token, project) @@ -647,7 +651,8 @@ Examples: ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() project := project.EnsureProject(ctx, cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API // perform the reconcilation keys, state := reconcileAgentList(logger, cmd, apiUrl, project.Token, project) diff --git a/cmd/apikey.go b/cmd/apikey.go index 7b9a02d3..f02c5db0 100644 --- a/cmd/apikey.go +++ b/cmd/apikey.go @@ -73,7 +73,7 @@ var apikeyListCmd = &cobra.Command{ Args: cobra.NoArgs, Short: "List API keys", Long: `List all API keys. - + This command displays all apikeys set for your org or project. Examples: @@ -83,7 +83,8 @@ Examples: agentuity apikey ls --mask`, Run: func(cmd *cobra.Command, args []string) { logger := env.NewLogger(cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() format, _ := cmd.Flags().GetString("format") @@ -152,7 +153,7 @@ var apikeyCreateCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), Short: "Create an API key", Long: `Create an API key. - + This command creates an API key for your org or project. Examples: @@ -161,7 +162,8 @@ Examples: agentuity apikey create --expires-at --project-id `, Run: func(cmd *cobra.Command, args []string) { logger := env.NewLogger(cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() apiKey, _ := util.EnsureLoggedIn(ctx, logger, cmd) @@ -223,7 +225,8 @@ var apikeyDeleteCmd = &cobra.Command{ Long: `Delete an API key.`, Run: func(cmd *cobra.Command, args []string) { logger := env.NewLogger(cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() apiKey, _ := util.EnsureLoggedIn(ctx, logger, cmd) @@ -264,13 +267,14 @@ var apikeyGetCmd = &cobra.Command{ Args: cobra.ExactArgs(1), Short: "Get an API key", Long: `Get an API key. - + Examples: agentuity apikey get agentuity apikey get --mask`, Run: func(cmd *cobra.Command, args []string) { logger := env.NewLogger(cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() apiKey, _ := util.EnsureLoggedIn(ctx, logger, cmd) diff --git a/cmd/auth.go b/cmd/auth.go index b7faab6f..b3305854 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -42,7 +42,9 @@ Examples: agentuity auth login`, Run: func(cmd *cobra.Command, args []string) { logger := env.NewLogger(cmd) - apiUrl, appUrl, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + appUrl := urls.App var otp string var upgrade bool ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) @@ -144,7 +146,8 @@ Examples: agentuity auth whoami`, Run: func(cmd *cobra.Command, args []string) { logger := env.NewLogger(cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() apiKey, userId := util.EnsureLoggedIn(ctx, logger, cmd) @@ -183,7 +186,10 @@ Examples: agentuity auth signup`, Run: func(cmd *cobra.Command, args []string) { logger := env.NewLogger(cmd) - apiUrl, appUrl, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + appUrl := urls.App + var otp string ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() diff --git a/cmd/cloud.go b/cmd/cloud.go index 7a501336..91e119e6 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -18,16 +18,16 @@ import ( "syscall" "time" - "github.com/agentuity/cli/internal/agent" "github.com/agentuity/cli/internal/deployer" "github.com/agentuity/cli/internal/envutil" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/ignore" - "github.com/agentuity/cli/internal/project" + iproject "github.com/agentuity/cli/internal/project" "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/crypto" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/logger" + "github.com/agentuity/go-common/project" "github.com/agentuity/go-common/tui" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -110,7 +110,7 @@ func ShowNewProjectImport(ctx context.Context, logger logger.Logger, cmd *cobra. } tui.ClearScreen() tui.ShowSpinner("Importing project ...", func() { - result, err := project.Import(ctx, logger, apiUrl, apikey, orgId, createWebhookAuth) + result, err := iproject.ProjectImport(ctx, logger, apiUrl, apikey, orgId, project, createWebhookAuth) if err != nil { if isCancelled(ctx) { os.Exit(1) @@ -184,7 +184,7 @@ Examples: ctx, cancel := signal.NotifyContext(parentCtx, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() logger := env.NewLogger(cmd) - context := project.EnsureProject(ctx, cmd) + context := iproject.EnsureProject(ctx, cmd) theproject := context.Project dir := context.Dir apiUrl := context.APIURL @@ -223,10 +223,10 @@ Examples: logger.Debug("preview: %v", preview) - deploymentConfig := project.NewDeploymentConfig() + deploymentConfig := iproject.NewDeploymentConfig() client := util.NewAPIClient(ctx, logger, apiUrl, token) var envFile *deployer.EnvFile - var projectData *project.ProjectData + var projectData *iproject.ProjectData var state map[string]agentListState if !ci { @@ -252,7 +252,7 @@ Examples: if !context.NewProject { action = func() { - projectData, err = theproject.GetProject(ctx, logger, apiUrl, token, false, false) + projectData, err = iproject.GetProject(ctx, logger, apiUrl, token, context.Project.ProjectId, false, false) if err != nil { if err == project.ErrProjectNotFound { return @@ -463,7 +463,11 @@ Examples: if agent.Agent.ID == "" { continue } - newagents = append(newagents, project.AgentConfig(*agent.Agent)) + newagents = append(newagents, project.AgentConfig{ + ID: agent.Agent.ID, + Name: agent.Agent.Name, + Description: agent.Agent.Description, + }) } theproject.Agents = newagents saveProject = true @@ -683,15 +687,15 @@ Examples: errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Error updating deployment status to completed")).ShowErrorAndExit() } - if len(theproject.Agents) == 1 { - if len(theproject.Agents[0].Types) > 0 { - webhookToken, err = agent.GetApiKey(ctx, logger, apiUrl, token, theproject.Agents[0].ID, theproject.Agents[0].Types[0]) - if err != nil { - errsystem.New(errsystem.ErrApiRequest, err, - errsystem.WithContextMessage("Error getting Agent API key")).ShowErrorAndExit() - } - } - } + // if len(theproject.Agents) == 1 { + // if len(theproject.Agents[0].Types) > 0 { + // webhookToken, err = agent.GetApiKey(ctx, logger, apiUrl, token, theproject.Agents[0].ID, theproject.Agents[0].Types[0]) + // if err != nil { + // errsystem.New(errsystem.ErrApiRequest, err, + // errsystem.WithContextMessage("Error getting Agent API key")).ShowErrorAndExit() + // } + // } + // } } tui.ShowSpinner("Deploying ...", deployAction) @@ -756,13 +760,15 @@ Examples: ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + deleteFlag, _ := cmd.Flags().GetBool("delete") dir, _ := cmd.Flags().GetString("dir") var selectedProject string if dir != "" { - proj := project.EnsureProject(ctx, cmd) + proj := iproject.EnsureProject(ctx, cmd) if proj.Project == nil { errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("project not found")).ShowErrorAndExit() } @@ -771,7 +777,7 @@ Examples: projectId, _ := cmd.Flags().GetString("project") if projectId != "" { // look up the project by id - projects, err := project.ListProjects(ctx, logger, apiUrl, apikey) + projects, err := iproject.ListProjects(ctx, logger, apiUrl, apikey) if err != nil { errsystem.New(errsystem.ErrApiRequest, err).ShowErrorAndExit() } @@ -806,10 +812,10 @@ Examples: var selectedDeployment string if tag != "" { // List deployments and match by tag - var deployments []project.DeploymentListData + var deployments []iproject.DeploymentListData action := func() { var err error - deployments, err = project.ListDeployments(ctx, logger, apiUrl, apikey, selectedProject) + deployments, err = iproject.ListDeployments(ctx, logger, apiUrl, apikey, selectedProject) if err != nil { errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list deployments")).ShowErrorAndExit() } @@ -858,13 +864,13 @@ Examples: } if deleteFlag { - err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) + err := iproject.DeleteDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) if err != nil { errsystem.New(errsystem.ErrDeleteApiKey, err, errsystem.WithContextMessage("Failed to delete deployment")).ShowErrorAndExit() } tui.ShowSuccess("Deployment deleted successfully") } else { - err := project.RollbackDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) + err := iproject.RollbackDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) if err != nil { errsystem.New(errsystem.ErrDeployProject, err, errsystem.WithContextMessage("Failed to rollback deployment")).ShowErrorAndExit() } @@ -875,10 +881,10 @@ Examples: // Helper to fetch projects and prompt user to select one. Returns selected project ID or empty string. func cloudSelectProject(ctx context.Context, logger logger.Logger, apiUrl, apikey string, prompt string) string { - var projects []project.ProjectListData + var projects []iproject.ProjectListData action := func() { var err error - projects, err = project.ListProjects(ctx, logger, apiUrl, apikey) + projects, err = iproject.ListProjects(ctx, logger, apiUrl, apikey) if err != nil { errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list projects")).ShowErrorAndExit() } @@ -905,10 +911,10 @@ func cloudSelectProject(ctx context.Context, logger logger.Logger, apiUrl, apike } func cloudSelectDeployment(ctx context.Context, logger logger.Logger, apiUrl, apikey, projectId string, prompt string) string { - var deployments []project.DeploymentListData + var deployments []iproject.DeploymentListData fetchDeploymentsAction := func() { var err error - deployments, err = project.ListDeployments(ctx, logger, apiUrl, apikey, projectId) + deployments, err = iproject.ListDeployments(ctx, logger, apiUrl, apikey, projectId) if err != nil { errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list deployments")).ShowErrorAndExit() } @@ -966,7 +972,9 @@ Examples: ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + projectId, _ := cmd.Flags().GetString("project") format, _ := cmd.Flags().GetString("format") @@ -981,10 +989,10 @@ Examples: return } - var deployments []project.DeploymentListData + var deployments []iproject.DeploymentListData action := func() { var err error - deployments, err = project.ListDeployments(ctx, logger, apiUrl, apikey, selectedProject) + deployments, err = iproject.ListDeployments(ctx, logger, apiUrl, apikey, selectedProject) if err != nil { errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list deployments")).ShowErrorAndExit() } diff --git a/cmd/dev.go b/cmd/dev.go index 5becabb2..37d171cb 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -17,18 +17,19 @@ import ( "github.com/agentuity/cli/internal/dev" "github.com/agentuity/cli/internal/envutil" "github.com/agentuity/cli/internal/errsystem" + "github.com/agentuity/cli/internal/gravity" "github.com/agentuity/cli/internal/project" "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/tui" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var devCmd = &cobra.Command{ - Use: "dev", - Aliases: []string{"run"}, - Args: cobra.NoArgs, - Short: "Run the development server", + Use: "dev", + Args: cobra.NoArgs, + Short: "Run the development server", Long: `Run the development server for local testing and development. This command starts a local development server that connects to the Agentuity Cloud @@ -44,8 +45,11 @@ Examples: agentuity dev --no-build`, Run: func(cmd *cobra.Command, args []string) { log := env.NewLogger(cmd) - logLevel := env.LogLevel(cmd) - apiUrl, appUrl, transportUrl := util.GetURLs(log) + urls := util.GetURLs(log) + apiUrl := urls.API + appUrl := urls.App + gravityUrl := urls.Gravity + noBuild, _ := cmd.Flags().GetBool("no-build") promptsEvalsFF := CheckFeatureFlag(cmd, FeaturePromptsEvals, "enable-prompts-evals") @@ -53,12 +57,10 @@ Examples: ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) defer cancel() - apiKey, userId := util.EnsureLoggedIn(ctx, log, cmd) + apiKey, _ := util.EnsureLoggedIn(ctx, log, cmd) theproject := project.EnsureProject(ctx, cmd) dir := theproject.Dir - checkForUpgrade(ctx, log, false) - if theproject.NewProject { var projectId string if theproject.Project.ProjectId != "" { @@ -67,11 +69,23 @@ Examples: ShowNewProjectImport(ctx, log, cmd, theproject.APIURL, apiKey, projectId, theproject.Project, dir, false) } - project, err := theproject.Project.GetProject(ctx, log, theproject.APIURL, apiKey, false, true) + project, err := project.GetProject(ctx, log, theproject.APIURL, apiKey, theproject.Project.ProjectId, false, true) if err != nil { errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithUserMessage("Failed to validate project (%s). This is most likely due to the API key being invalid or the project has been deleted.\n\nYou can import this project using the following command:\n\n"+tui.Command("project import"), theproject.Project.ProjectId), errsystem.WithContextMessage(fmt.Sprintf("Failed to get project: %s", err))).ShowErrorAndExit() } + hostname := viper.GetString("devmode.hostname") + + endpoint, err := dev.GetDevModeEndpoint(ctx, log, theproject.APIURL, apiKey, theproject.Project.ProjectId, hostname) + if err != nil { + errsystem.New(errsystem.ErrRetrieveDevmodeEndpoint, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to retrieve devmode endpoint: %s", err))).ShowErrorAndExit() + } + + if hostname != endpoint.Hostname { + viper.Set("devmode.hostname", endpoint.Hostname) + viper.WriteConfig() + } + var envfile *deployer.EnvFile envfile, project = envutil.ProcessEnvFiles(ctx, log, dir, theproject.Project, project, theproject.APIURL, apiKey, false, true) @@ -95,7 +109,6 @@ Examples: } } // Add the required Agentuity SDK and project keys - fmt.Fprintf(of, "AGENTUITY_SDK_KEY=%s\n", apiKey) fmt.Fprintf(of, "AGENTUITY_PROJECT_KEY=%s\n", project.ProjectKey) of.Close() tui.ShowSuccess("Synchronized project to .env file: %s", tui.Muted(filename)) @@ -103,24 +116,35 @@ Examples: orgId := project.OrgId - port, _ := cmd.Flags().GetInt("port") - port, err = dev.FindAvailablePort(theproject, port) + agentPort, _ := cmd.Flags().GetInt("port") + agentPort, err = dev.FindAvailablePort(theproject, agentPort) + if err != nil { + log.Fatal("failed to find available port: %s", err) + } + proxyPort, err := dev.FindAvailableOpenPort() if err != nil { log.Fatal("failed to find available port: %s", err) } server, err := dev.New(dev.ServerArgs{ - Ctx: ctx, - Logger: log, - LogLevel: logLevel, - APIURL: apiUrl, - TransportURL: transportUrl, - APIKey: apiKey, - OrgId: orgId, - Project: theproject, - Version: Version, - UserId: userId, - Port: port, + APIURL: apiUrl, + APIKey: apiKey, + Hostname: endpoint.Hostname, + Config: &gravity.Config{ + Context: ctx, + Logger: log, + Version: Version, + OrgID: orgId, + Project: theproject, + EndpointID: endpoint.ID, + URL: gravityUrl, + SDKKey: project.Secrets["AGENTUITY_SDK_KEY"], + ProxyPort: uint(proxyPort), + AgentPort: uint(agentPort), + Ephemeral: true, + ClientName: "cli/devmode", + DynamicHostname: true, + }, }) if err != nil { log.Fatal("failed to create live dev connection: %s", err) @@ -135,7 +159,7 @@ Examples: if errors.Is(err, context.Canceled) { return } - log.Error("failed to start live dev connection: %s", err) + log.Fatal("failed to start devmode connection: %s", err) return } } @@ -144,11 +168,11 @@ Examples: publicUrl := server.PublicURL(appUrl) consoleUrl := server.WebURL(appUrl) - devModeUrl := fmt.Sprintf("http://127.0.0.1:%d", port) + devModeUrl := fmt.Sprintf("http://127.0.0.1:%d", agentPort) infoBox := server.GenerateInfoBox(publicUrl, consoleUrl, devModeUrl) fmt.Println(infoBox) - projectServerCmd, err := dev.CreateRunProjectCmd(processCtx, log, theproject, server, dir, orgId, port, os.Stdout, os.Stderr) + projectServerCmd, err := dev.CreateRunProjectCmd(processCtx, log, theproject, server, dir, orgId, agentPort, os.Stdout, os.Stderr) if err != nil { errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to run project")).ShowErrorAndExit() } @@ -187,7 +211,7 @@ Examples: } runServer := func() { - projectServerCmd, err = dev.CreateRunProjectCmd(processCtx, log, theproject, server, dir, orgId, port, os.Stdout, os.Stderr) + projectServerCmd, err = dev.CreateRunProjectCmd(processCtx, log, theproject, server, dir, orgId, agentPort, os.Stdout, os.Stderr) if err != nil { errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to run project")).ShowErrorAndExit() } diff --git a/cmd/env.go b/cmd/env.go index 19a58dd0..f15717a5 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -227,7 +227,7 @@ Examples: delete(secrets, k) combined[k] = envs[k] } - _, err := theproject.SetProjectEnv(ctx, logger, apiUrl, apiKey, envs, secrets) + _, err := project.SetProjectEnv(ctx, logger, apiUrl, apiKey, theproject.ProjectId, envs, secrets) if err != nil { errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithUserMessage("Failed to save project settings")).ShowErrorAndExit() } @@ -276,7 +276,7 @@ Examples: mask, _ := cmd.Flags().GetBool("mask") - projectData, err := theproject.GetProject(ctx, logger, apiUrl, apiKey, mask, false) + projectData, err := project.GetProject(ctx, logger, apiUrl, apiKey, theproject.ProjectId, mask, false) if err != nil { errsystem.New(errsystem.ErrApiRequest, err).ShowErrorAndExit() } @@ -353,7 +353,7 @@ Examples: mask, _ := cmd.Flags().GetBool("mask") includeProjectKeys, _ := cmd.Flags().GetBool("include-project-keys") - projectData, err := theproject.GetProject(ctx, logger, apiUrl, apiKey, mask, includeProjectKeys) + projectData, err := project.GetProject(ctx, logger, apiUrl, apiKey, theproject.ProjectId, mask, includeProjectKeys) if err != nil { errsystem.New(errsystem.ErrApiRequest, err).ShowErrorAndExit() } @@ -417,7 +417,7 @@ Examples: apiUrl := context.APIURL apiKey := context.Token - projectData, err := theproject.GetProject(ctx, logger, apiUrl, apiKey, true, false) + projectData, err := project.GetProject(ctx, logger, apiUrl, apiKey, theproject.ProjectId, true, false) if err != nil { errsystem.New(errsystem.ErrApiRequest, err).ShowErrorAndExit() } @@ -499,7 +499,7 @@ Examples: return } } - err := theproject.DeleteProjectEnv(ctx, logger, apiUrl, apiKey, envsToDelete, secretsToDelete) + err := project.DeleteProjectEnv(ctx, logger, apiUrl, apiKey, theproject.ProjectId, envsToDelete, secretsToDelete) if err != nil { errsystem.New(errsystem.ErrApiRequest, err).ShowErrorAndExit() } diff --git a/cmd/logs.go b/cmd/logs.go index b0c53878..f739b604 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -58,7 +58,8 @@ type LogsResponse struct { func printLogs(ctx context.Context, logger logger.Logger, cmd *cobra.Command, query url.Values, tail bool, hideDate bool, hideTime bool) { - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API apiKey, _ := util.EnsureLoggedIn(ctx, logger, cmd) client := util.NewAPIClient(ctx, logger, apiUrl, apiKey) diff --git a/cmd/profile.go b/cmd/profile.go index 7d13d283..2c8d745e 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -21,7 +21,7 @@ type profile struct { selected bool } -var profileNameRegex = regexp.MustCompile(`name:\s+["]?([\w-_]+)["]?`) +var profileNameRegex = regexp.MustCompile(`\bname:\s+["]?([\w-_]+)["]?`) var profileNameValidRegex = regexp.MustCompile(`^[\w-_]{3,}$`) func saveProfile(name string) { diff --git a/cmd/project.go b/cmd/project.go index 995c6819..ba85966e 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -22,6 +22,7 @@ import ( "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/logger" + cproject "github.com/agentuity/go-common/project" "github.com/agentuity/go-common/tui" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -122,7 +123,7 @@ type InitProjectArgs struct { EnableWebhookAuth bool AuthType string Provider *templates.TemplateRules - Agents []project.AgentConfig + Agents []cproject.AgentConfig Framework string } @@ -150,9 +151,9 @@ func initProject(ctx context.Context, logger logger.Logger, args InitProjectArgs proj.Name = args.Name proj.Description = args.Description - proj.Development = &project.Development{ + proj.Development = &cproject.Development{ Port: args.Provider.Development.Port, - Watch: project.Watch{ + Watch: cproject.Watch{ Enabled: args.Provider.Development.Watch.Enabled, Files: args.Provider.Development.Watch.Files, }, @@ -160,14 +161,14 @@ func initProject(ctx context.Context, logger logger.Logger, args InitProjectArgs Args: args.Provider.Development.Args, } - proj.Bundler = &project.Bundler{ + proj.Bundler = &cproject.Bundler{ Enabled: args.Provider.Bundle.Enabled, Identifier: args.Provider.Identifier, Language: args.Provider.Language, Framework: args.Provider.Framework, Runtime: detectRuntime(args.Dir, args.Provider.Runtime), Ignore: args.Provider.Bundle.Ignore, - AgentConfig: project.AgentBundlerConfig{ + AgentConfig: cproject.AgentBundlerConfig{ Dir: args.Provider.SrcDir, }, } @@ -178,7 +179,7 @@ func initProject(ctx context.Context, logger logger.Logger, args InitProjectArgs proj.Deployment.Resources.CPU = args.Provider.Deployment.Resources.CPU proj.Deployment.Resources.Memory = args.Provider.Deployment.Resources.Memory proj.Deployment.Resources.Disk = args.Provider.Deployment.Resources.Disk - proj.Deployment.Mode = &project.Mode{ + proj.Deployment.Mode = &cproject.Mode{ Type: "on-demand", } @@ -333,7 +334,9 @@ Examples: defer cancel() logger := env.NewLogger(cmd) apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) - apiUrl, appUrl, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + appUrl := urls.App initScreenWithLogo() @@ -345,7 +348,7 @@ Examples: checkForUpgrade(ctx, logger, true) // Railgurd the user from creating a project in an existing project directory - if project.ProjectExists(cwd) { + if cproject.ProjectExists(cwd) { if tui.HasTTY { fmt.Println() tui.ShowWarning("You are currently in an existing Agentuity project directory!") @@ -616,16 +619,16 @@ Examples: } // check to see if the project already has existing agents returned and if so, we're going to use those - var agents []project.AgentConfig + var agents []cproject.AgentConfig if len(existingAgents) > 0 { for _, agent := range existingAgents { - agents = append(agents, project.AgentConfig{ + agents = append(agents, cproject.AgentConfig{ Name: agent.Name, Description: agent.Description, }) } } else { - agents = []project.AgentConfig{ + agents = []cproject.AgentConfig{ { Name: agentName, Description: agentDescription, @@ -747,7 +750,9 @@ Examples: defer cancel() logger := env.NewLogger(cmd) apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + orgId, _ := cmd.Flags().GetString("org-id") var projects []project.ProjectListData @@ -832,6 +837,7 @@ It will prompt you to select which projects to delete and confirm the deletion. Examples: agentuity project delete + agentuity project rm project_12345567890ab agentuity project rm`, Aliases: []string{"rm", "del"}, Run: func(cmd *cobra.Command, args []string) { @@ -839,7 +845,9 @@ Examples: defer cancel() logger := env.NewLogger(cmd) apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) - apiUrl, _, _ := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + orgId, _ := cmd.Flags().GetString("org-id") var selected []string @@ -925,7 +933,7 @@ Examples: if apikey != "" && orgId != "" && name != "" && description != "" { context.Project.Name = name context.Project.Description = description - result, err := context.Project.Import(ctx, logger, context.APIURL, apikey, orgId, true) + result, err := project.ProjectImport(ctx, logger, context.APIURL, apikey, orgId, context.Project, true) if err != nil { if isCancelled(ctx) { os.Exit(1) diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 00000000..0c95cf62 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,178 @@ +package cmd + +import ( + "context" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/agentuity/cli/internal/dev" + "github.com/agentuity/cli/internal/gravity" + "github.com/agentuity/cli/internal/project" + "github.com/agentuity/cli/internal/run" + "github.com/agentuity/cli/internal/util" + "github.com/agentuity/go-common/env" + "github.com/agentuity/go-common/sys" + "github.com/google/uuid" + "github.com/spf13/cobra" +) + +var runCmd = &cobra.Command{ + Use: "run", + Args: cobra.NoArgs, + Short: "Run the production server", + Long: `Run the production server for connecting to the Agentuity Cloud. + +This command starts a local production server that connects to the Agentuity Cloud +for live routing of your agents to the machine running the server. + +Examples: + agentuity run`, + Run: func(cmd *cobra.Command, args []string) { + log := env.NewLogger(cmd) + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + theproject := project.EnsureProject(ctx, cmd) + dir := theproject.Dir + + buildFolder := filepath.Join(dir, ".agentuity") + if !sys.Exists(buildFolder) { + log.Fatal("missing the build folder at %s. make sure you have run agentuity bundle --production", buildFolder) + } + + var err error + agentPort, _ := cmd.Flags().GetInt("port") + agentPort, err = dev.FindAvailablePort(theproject, agentPort) + if err != nil { + log.Fatal("failed to find available port: %s", err) + } + proxyPort, err := dev.FindAvailableOpenPort() + if err != nil { + log.Fatal("failed to find available port: %s", err) + } + + var envfile []env.EnvLineComment + if sys.Exists(filepath.Join(dir, ".env.production")) { + envfile, err = env.ParseEnvFileWithComments(filepath.Join(dir, ".env.production")) + if err != nil { + log.Fatal("failed to parse env file: %s", err) + } + } else if sys.Exists(filepath.Join(dir, ".env")) { + envfile, err = env.ParseEnvFileWithComments(filepath.Join(dir, ".env")) + if err != nil { + log.Fatal("failed to parse env file: %s", err) + } + } + + sdkKey := os.Getenv("AGENTUITY_SDK_KEY") + if sdkKey == "" { + for _, line := range envfile { + if line.Key == "AGENTUITY_SDK_KEY" { + sdkKey = line.Val + break + } + } + } + if sdkKey == "" { + log.Fatal("missing AGENTUITY_SDK_KEY environment variable") + } + + for _, line := range envfile { + os.Setenv(line.Key, line.Val) + } + + gravityurl, _ := cmd.Flags().GetString("gravity-url") + transporturl, _ := cmd.Flags().GetString("transport-url") + + var instanceId string + + if sys.Exists("/etc/machine-id") { + val, _ := os.ReadFile("/etc/machine-id") + instanceId = strings.TrimSpace(string(val)) + } + + if instanceId == "" { + instanceId = uuid.New().String() + } + + client := gravity.New(gravity.Config{ + Context: ctx, + Logger: log, + Version: Version, + Project: theproject, + URL: gravityurl, + SDKKey: sdkKey, + EndpointID: instanceId, + ClientName: "cli/run", + ProxyPort: uint(proxyPort), + AgentPort: uint(agentPort), + Ephemeral: true, + DynamicProject: true, + }) + + if err := client.Start(); err != nil { + log.Fatal("failed to start client: %s", err) + } + + defer client.Close() + + thecmd, err := run.CreateRunProjectCmd(run.Config{ + WorkingDir: dir, + Project: theproject, + OrgId: client.OrgID(), + Context: ctx, + Logger: log, + TelemetryURL: client.TelemetryURL(), + TelemetryAPIKey: client.TelemetryAPIKey(), + APIURL: client.APIURL(), + AgentPort: agentPort, + TransportURL: transporturl, + }) + + if err != nil { + log.Fatal("failed to create run project command: %s", err) + } + + if err := thecmd.Start(); err != nil { + log.Fatal("failed to start run project command: %s", err) + } + + <-ctx.Done() + + if thecmd.Process != nil { + log.Trace("sending SIGINT to agent process") + thecmd.Process.Signal(syscall.SIGINT) + } + + wctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + go func() { + defer cancel() + thecmd.Wait() + log.Trace("agent exited") + }() + + select { + case <-wctx.Done(): + case <-time.After(30 * time.Second): + log.Trace("agent stop timed out") + util.ProcessKill(thecmd) + } + }, +} + +func init() { + rootCmd.AddCommand(runCmd) + runCmd.Flags().StringP("dir", "d", ".", "The directory to run the server in") + runCmd.Flags().Int("port", 0, "The port to run the server on (uses project default if not provided)") + runCmd.Flags().String("gravity-url", "grpc://gravity.agentuity.com", "The URL to the gravity server") + runCmd.Flags().String("transport-url", "https://agentuity.ai", "The URL to the transport server") + runCmd.Flags().MarkHidden("gravity-url") + runCmd.Flags().MarkHidden("transport-url") +} diff --git a/error_codes.yaml b/error_codes.yaml index c6ef769a..a3cef919 100644 --- a/error_codes.yaml +++ b/error_codes.yaml @@ -1,46 +1,46 @@ # Error Codes for CLI # This file defines all error codes used in the CLI application -# Format: +# Format: # - code: Unique error code identifier (e.g., CLI-XXXX) # - message: Human-readable error message errors: - code: CLI-0001 message: Failed to delete agents - + - code: CLI-0002 message: Failed to create project - + - code: CLI-0003 message: Unable to authenticate user - + - code: CLI-0004 message: Environment variables not set - + - code: CLI-0005 message: API request failed - + - code: CLI-0006 message: Invalid configuration - + - code: CLI-0007 message: Failed to save project - + - code: CLI-0008 message: Failed to deploy project - + - code: CLI-0009 message: Failed to upload project - + - code: CLI-0010 message: Failed to parse environment file - + - code: CLI-0011 message: Invalid command flag error - code: CLI-0012 message: Failed to list files and directories - + - code: CLI-0013 message: Failed to write configuration file @@ -52,7 +52,7 @@ errors: - code: CLI-0016 message: Failed to create temporary file - + - code: CLI-0017 message: Failed to create zip file @@ -88,3 +88,6 @@ errors: - code: CLI-0028 message: Failed to delete API key + + - code: CLI-0029 + message: Failed to retrieve devmode endpoint diff --git a/go.mod b/go.mod index f1587f6e..d9fd18cc 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/agentuity/cli -go 1.25.1 +go 1.25.2 require ( github.com/Masterminds/semver v1.5.0 - github.com/agentuity/go-common v1.0.91 + github.com/agentuity/go-common v1.0.102 github.com/agentuity/mcp-golang/v2 v2.0.2 github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/charmbracelet/bubbles v0.20.0 @@ -22,10 +22,10 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.11.1 - github.com/zijiren233/yaml-comment v0.2.2 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 gopkg.in/yaml.v3 v3.0.1 - k8s.io/apimachinery v0.32.1 + gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe + k8s.io/apimachinery v0.34.1 ) require ( @@ -52,18 +52,21 @@ require ( github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.8.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getsentry/sentry-go v0.31.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.14.0 github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/btree v1.1.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -74,6 +77,7 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/maruel/natural v1.1.1 // indirect @@ -82,7 +86,7 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect @@ -90,12 +94,14 @@ require ( github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/shirou/gopsutil/v4 v4.25.8 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect @@ -106,13 +112,18 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zijiren233/yaml-comment v0.2.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect @@ -120,16 +131,17 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.41.0 // indirect - golang.org/x/net v0.43.0 + golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 golang.org/x/term v0.34.0 // indirect golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.9.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/grpc v1.75.0 // indirect @@ -137,4 +149,5 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect ) diff --git a/go.sum b/go.sum index 0412ddbf..c65ee32c 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/agentuity/go-common v1.0.91 h1:60CNTdJnm/KKsNG7R1NHa7bALvvROijCAzvPXmpS0iE= -github.com/agentuity/go-common v1.0.91/go.mod h1:iliwcRguPH18rPv1049wFTETZn0wUdD4SN6rN8VcAoA= +github.com/agentuity/go-common v1.0.102 h1:xs70GId7agCBg3f3NTzcFozZ9F/UuimxWWVdLp2raK0= +github.com/agentuity/go-common v1.0.102/go.mod h1:qazzsVD2ousgkbJc3+wzgPUkso36U7OdzTOSyIJO1Fc= github.com/agentuity/mcp-golang/v2 v2.0.2 h1:wZqS/aHWZsQoU/nd1E1/iMsVY2dywWT9+PFlf+3YJxo= github.com/agentuity/mcp-golang/v2 v2.0.2/go.mod h1:U105tZXyTatxxOBlcObRgLb/ULvGgT2DJ1nq/8++P6Q= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -77,6 +77,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -89,8 +91,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -110,6 +112,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -118,11 +122,12 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -152,6 +157,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -173,8 +180,9 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -197,6 +205,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -212,6 +222,8 @@ github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55 github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970= +github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= @@ -252,6 +264,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -264,10 +280,14 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zijiren233/yaml-comment v0.2.2 h1:5ghs8huXFVb/kWCi66P+xbXq0GnOE2XVCnhaWd7mTs8= github.com/zijiren233/yaml-comment v0.2.2/go.mod h1:YksA19x5zWKaz8c/bJdSuVRo2G11FYk2/lDVcjYnYI4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= @@ -322,9 +342,11 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -343,6 +365,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -376,5 +400,11 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= -k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe h1:fre4i6mv4iBuz5lCMOzHD1rH1ljqHWSICFmZRbbgp3g= +gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 7acf00d2..32185208 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -14,9 +14,10 @@ import ( "github.com/agentuity/cli/internal/bundler/prompts" "github.com/agentuity/cli/internal/errsystem" - "github.com/agentuity/cli/internal/project" + iproject "github.com/agentuity/cli/internal/project" "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/logger" + "github.com/agentuity/go-common/project" "github.com/agentuity/go-common/slice" cstr "github.com/agentuity/go-common/string" "github.com/agentuity/go-common/sys" @@ -498,7 +499,7 @@ func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject * return fmt.Errorf("failed to load %s: %w", pkgjson, err) } - externals := make([]string, 0) + externals := make([]string, len(commonExternals)) copy(externals, commonExternals) // check to see if we have any externals explicitly set in package.json so that the // project can add additional externals automatically @@ -769,7 +770,7 @@ func CreateDeploymentMutator(ctx BundleContext) util.ZipDirCallbackMutator { } func Bundle(ctx BundleContext) error { - theproject := project.NewProject() + theproject := iproject.NewProject() if err := theproject.Load(ctx.ProjectDir); err != nil { return fmt.Errorf("failed to load project from %s: %w", ctx.ProjectDir, err) } diff --git a/internal/bundler/types.go b/internal/bundler/types.go index 80d317de..32b4c12d 100644 --- a/internal/bundler/types.go +++ b/internal/bundler/types.go @@ -4,8 +4,8 @@ import ( "context" "io" - "github.com/agentuity/cli/internal/project" "github.com/agentuity/go-common/logger" + "github.com/agentuity/go-common/project" ) // BundleContext holds the context for bundling operations diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 1f71d655..4c885cd7 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -6,10 +6,11 @@ import ( "time" "github.com/agentuity/cli/internal/bundler" - "github.com/agentuity/cli/internal/project" + iproject "github.com/agentuity/cli/internal/project" "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/logger" + "github.com/agentuity/go-common/project" ) type EnvFile struct { @@ -59,9 +60,9 @@ type DeployPreflightCheckData struct { // Project is the project data Project *project.Project // ProjectData is the project data loaded from the backend - ProjectData *project.ProjectData + ProjectData *iproject.ProjectData // Config is the deployment configuration - Config *project.DeploymentConfig + Config *iproject.DeploymentConfig // PromptHelpers are a set of funcs to assist in prompting the user on the command line PromptHelpers PromptHelpers // OS Environment as a map diff --git a/internal/dev/dev.go b/internal/dev/dev.go index d7d999f0..781f777c 100644 --- a/internal/dev/dev.go +++ b/internal/dev/dev.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net" + "net/url" "os" "os/exec" "strconv" @@ -65,7 +66,7 @@ func isPortAvailable(port int) bool { return false } -func findAvailablePort() (int, error) { +func FindAvailableOpenPort() (int, error) { listener, err := net.Listen("tcp4", "0.0.0.0:0") if err != nil { return 0, err @@ -101,19 +102,21 @@ func FindAvailablePort(p project.ProjectContext, tryPort int) (int, error) { if isPortAvailable(p.Project.Development.Port) { return p.Project.Development.Port, nil } - return findAvailablePort() + return FindAvailableOpenPort() } func CreateRunProjectCmd(ctx context.Context, log logger.Logger, theproject project.ProjectContext, server *Server, dir string, orgId string, port int, stdout io.Writer, stderr io.Writer) (*exec.Cmd, error) { // set the vars projectServerCmd := exec.CommandContext(ctx, theproject.Project.Development.Command, theproject.Project.Development.Args...) projectServerCmd.Env = os.Environ()[:] - projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_OTLP_BEARER_TOKEN=%s", server.otelToken)) - projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_OTLP_URL=%s", server.otelUrl)) + telemetryURL := server.TelemetryURL() + telemetryAPIKey := server.TelemetryAPIKey() + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_OTLP_URL=%s", telemetryURL)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_OTLP_BEARER_TOKEN=%s", telemetryAPIKey)) projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_URL=%s", theproject.APIURL)) projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_TRANSPORT_URL=%s", theproject.TransportURL)) - - projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_CLOUD_DEPLOYMENT_ID=%s", server.ID)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_CLOUD_DEPLOYMENT_ID=%s", server.client.EndpointID())) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_ENDPOINT_ID=%s", server.client.EndpointID())) projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_CLOUD_PROJECT_ID=%s", theproject.Project.ProjectId)) projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_CLOUD_ORG_ID=%s", orgId)) @@ -148,3 +151,30 @@ func CreateRunProjectCmd(ctx context.Context, log logger.Logger, theproject proj return projectServerCmd, nil } + +type Endpoint struct { + ID string `json:"id"` + Hostname string `json:"hostname"` +} + +type Response[T any] struct { + Success bool `json:"success"` + Message string `json:"message"` + Data T `json:"data"` +} + +func GetDevModeEndpoint(ctx context.Context, logger logger.Logger, baseUrl string, token string, projectId string, hostname string) (*Endpoint, error) { + client := util.NewAPIClient(ctx, logger, baseUrl, token) + + var resp Response[Endpoint] + body := map[string]string{ + "hostname": hostname, + } + if err := client.Do("POST", fmt.Sprintf("/cli/devmode/2/%s", url.PathEscape(projectId)), body, &resp); err != nil { + return nil, fmt.Errorf("error fetching devmode endpoint: %s", err) + } + if !resp.Success { + return nil, fmt.Errorf("error fetching devmode endpoint: %s", resp.Message) + } + return &resp.Data, nil +} diff --git a/internal/dev/server.go b/internal/dev/server.go index 3e2c7c39..f9a879e7 100644 --- a/internal/dev/server.go +++ b/internal/dev/server.go @@ -1,604 +1,57 @@ package dev import ( - "context" - "crypto/tls" - "encoding/json" - "errors" "fmt" - "io" - "math" - "net/http" - "net/http/httputil" - "net/url" - "strings" - "sync" - "time" - "github.com/agentuity/cli/internal/project" - "github.com/agentuity/cli/internal/util" - "github.com/agentuity/go-common/logger" - "github.com/agentuity/go-common/message" - cstr "github.com/agentuity/go-common/string" - "github.com/agentuity/go-common/telemetry" + "github.com/agentuity/cli/internal/gravity" "github.com/agentuity/go-common/tui" "github.com/charmbracelet/lipgloss" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" - "golang.org/x/net/http2" ) -const ( - maxConnectionFailures = 20 - maxReconnectBaseDelay = time.Millisecond * 250 - maxReconnectMaxDelay = time.Second * 10 -) - -var propagator propagation.TraceContext - type Server struct { - ID string - otelToken string - otelUrl string - Project project.ProjectContext - orgId string - userId string - apiurl string - transportUrl string - apiKey string - ctx context.Context - cancel context.CancelFunc - logger logger.Logger - tracer trace.Tracer - version string - once sync.Once - apiclient *util.APIClient - publicUrl string - port int - connected chan error - expiresAt *time.Time - tlsCertificate *tls.Certificate - conn *tls.Conn - wg sync.WaitGroup - cleanup func() - - // Connection state - connectionLock sync.Mutex - reconnectFailures int - connectionFailed time.Time - connectionStarted time.Time - reconnectMutex sync.Mutex - hostname string + args ServerArgs + client *gravity.Client + config *gravity.Config } type ServerArgs struct { - Ctx context.Context - Logger logger.Logger - LogLevel logger.LogLevel - APIURL string - TransportURL string - APIKey string - Project project.ProjectContext - OrgId string - UserId string - Version string - Port int -} - -type ConnectionResponse struct { - Success bool `json:"success"` - Error string `json:"message"` - Data struct { - Certificate string `json:"certificate"` - PrivateKey string `json:"private_key"` - Domain string `json:"domain"` - ExpiresAt string `json:"expires_at"` - OtelUrl string `json:"otlp_url"` - OtelBearerToken string `json:"otlp_token"` - Hostname string `json:"hostname,omitempty"` - } `json:"data"` + APIURL string + APIKey string + Hostname string + *gravity.Config } // Close closes the bridge client and cleans up the connection func (s *Server) Close() error { - s.logger.Debug("closing connection") - s.once.Do(func() { - s.closeConnection() - s.cancel() - s.wg.Wait() - if s.conn != nil { - s.conn.Close() - s.conn = nil - } - if s.cleanup != nil { - s.cleanup() - } - }) - return nil -} - -func (s *Server) closeConnection() { - if err := s.apiclient.Do("DELETE", "/cli/devmode/"+s.Project.Project.ProjectId+"/"+s.ID, nil, nil); err != nil { - s.logger.Error("failed to send close connection: %s", err) - } -} - -func (s *Server) refreshConnection() error { - var resp ConnectionResponse - if err := s.apiclient.Do("GET", "/cli/devmode/"+s.Project.Project.ProjectId+"/"+s.ID, nil, &resp); err != nil { - return fmt.Errorf("failed to refresh connection: %w", err) - } - s.otelUrl = resp.Data.OtelUrl - s.otelToken = resp.Data.OtelBearerToken - tv, err := time.Parse(time.RFC3339, resp.Data.ExpiresAt) - if err != nil { - return fmt.Errorf("failed to parse expires at: %w", err) - } - s.expiresAt = &tv - s.publicUrl = fmt.Sprintf("https://%s", resp.Data.Domain) - cert, err := tls.X509KeyPair([]byte(resp.Data.Certificate), []byte(resp.Data.PrivateKey)) - if err != nil { - return fmt.Errorf("failed to create tls key pair: %w", err) - } - s.tlsCertificate = &cert - if s.cleanup == nil { - ctx, logger, cleanup, err := telemetry.NewWithAPIKey(s.ctx, "@agentuity/cli", s.otelUrl, s.otelToken, s.logger) - if err != nil { - return fmt.Errorf("failed to create OTLP telemetry trace: %w", err) - } - s.ctx = ctx - s.logger = logger - s.cleanup = cleanup - } - s.hostname = resp.Data.Hostname - - return nil -} - -func (s *Server) reconnect() { - if s.conn != nil { - s.conn.Close() - s.conn = nil - } - go s.connect(false) -} - -func (s *Server) connect(initial bool) { - var gerr error - - s.logger.Trace("connecting to devmode server") - - // hold a connection lock to prevent multiple go routines from trying to reconnect - // before the previous connect goroutine has finished - s.connectionLock.Lock() - defer s.connectionLock.Unlock() - - defer func() { - if initial && gerr != nil { - s.connected <- gerr - } - s.logger.Debug("connection closed") - select { - case <-s.ctx.Done(): - return - default: - var count int - var started time.Time - s.reconnectMutex.Lock() - if s.reconnectFailures == 0 { - s.connectionFailed = time.Now() - s.logger.Warn("lost connection to the dev server, reconnecting ...") - } - s.reconnectFailures++ - started = s.connectionFailed - count = s.reconnectFailures - s.reconnectMutex.Unlock() - if count >= maxConnectionFailures { - s.logger.Fatal("Too many connection failures, giving up after %d attempts (%s). You may need to re-run `agentuity dev`. If this error persists, please contact support.", count, time.Since(started)) - return - } - baseDelay := maxReconnectBaseDelay - wait := baseDelay * time.Duration(math.Pow(2, float64(count-1))) - if wait > maxReconnectMaxDelay { - wait = maxReconnectMaxDelay - } - s.logger.Debug("reconnecting in %s after %d connection failures (%s)", wait, count, time.Since(started)) - time.Sleep(wait) - s.reconnect() - } - }() - - s.logger.Trace("refreshing connection metadata") - refreshStart := time.Now() - if err := s.refreshConnection(); err != nil { - if !initial { - s.logger.Error("failed to refresh connection: %s", err) - } - // initial will bubble this up - gerr = err - return - } - - s.logger.Trace("refreshed connection metadata in %v", time.Since(refreshStart)) - - var tlsConfig tls.Config - tlsConfig.Certificates = []tls.Certificate{*s.tlsCertificate} - tlsConfig.NextProtos = []string{"h2"} - - hostname := s.hostname - - if strings.Contains(hostname, "localhost") || strings.Contains(hostname, "127.0.0.1") { - tlsConfig.InsecureSkipVerify = true - } - - if !strings.Contains(hostname, ":") { - hostname = fmt.Sprintf("%s:443", hostname) - } - - s.logger.Trace("dialing devmode server: %s", hostname) - dialStart := time.Now() - conn, err := tls.Dial("tcp", hostname, &tlsConfig) - if err != nil { - gerr = err - s.logger.Warn("failed to dial devmode server: %s (%s), will retry ...", hostname, err) - return - } - s.conn = conn - s.logger.Trace("dialed devmode server in %v", time.Since(dialStart)) - - if initial { - s.connected <- nil - } - - // if we successfully connect, reset our connection failures - s.reconnectMutex.Lock() - if s.reconnectFailures > 0 && !s.connectionFailed.IsZero() { - s.logger.Debug("reconnection successful after %s (%d attempts)", time.Since(s.connectionFailed), s.reconnectFailures) - s.logger.Info("✅ connection to the dev server re-established") - } - s.reconnectFailures = 0 - s.connectionStarted = time.Now() - s.connectionFailed = time.Time{} - s.reconnectMutex.Unlock() - - s.logger.Debug("connection established to %s", hostname) - - // HTTP/2 server to accept proxied requests over the tunnel connection - h2s := &http2.Server{} - - h2s.ServeConn(conn, &http2.ServeConnOpts{ - Handler: http.HandlerFunc(s.handleStream), - Context: s.ctx, - }) - -} - -type AgentWelcome struct { - project.AgentConfig - Welcome -} - -type AgentsControlResponse struct { - ProjectID string `json:"projectId"` - ProjectName string `json:"projectName"` - Agents []AgentWelcome `json:"agents"` -} - -func (s *Server) getAgents(ctx context.Context, project *project.Project) (*AgentsControlResponse, error) { - var resp = &AgentsControlResponse{ - ProjectID: project.ProjectId, - ProjectName: project.Name, - } - welcome, err := s.getWelcome(ctx, s.port) - if err != nil { - return nil, err - } - for _, agent := range project.Agents { - var w Welcome - if welcome != nil { - w = welcome[agent.ID] - } - resp.Agents = append(resp.Agents, AgentWelcome{ - AgentConfig: agent, - Welcome: w, - }) - } - return resp, nil -} - -func sendCORSHeaders(headers http.Header) { - headers.Set("access-control-allow-origin", "*") - headers.Set("access-control-expose-headers", "Content-Type") - headers.Set("access-control-allow-headers", "Content-Type, Authorization") - headers.Set("access-control-allow-methods", "GET, POST, OPTIONS") -} - -func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) { - s.wg.Add(1) - defer s.wg.Done() - - s.logger.Trace("handleStream: %s %s", r.Method, r.URL) - - if r.Method == "OPTIONS" { - sendCORSHeaders(w.Header()) - w.WriteHeader(http.StatusOK) - return - } - - switch r.URL.Path { - case "/": - message.CustomErrorResponse(w, "Agents, Not Humans, Live Here", "Hi! I'm an Agentuity Agent running in development mode.", "", http.StatusOK) - return - case "/_health": - w.WriteHeader(http.StatusOK) - return - case "/_agents": - sendCORSHeaders(w.Header()) - agents, err := s.getAgents(r.Context(), s.Project.Project) - if err != nil { - s.logger.Error("failed to marshal agents control response: %s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - io.WriteString(w, cstr.JSONStringify(agents)) - return - case "/_control": - sendCORSHeaders(w.Header()) - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.WriteHeader(http.StatusOK) - rc := http.NewResponseController(w) - rc.Flush() - s.HealthCheck(fmt.Sprintf("http://127.0.0.1:%d", s.port)) // make sure the server is running - w.Write([]byte("event: start\ndata: connected\n\n")) - agents, err := s.getAgents(r.Context(), s.Project.Project) - if err != nil { - s.logger.Error("failed to marshal agents control response: %s", err) - w.Write([]byte(fmt.Sprintf("event: error\ndata: %q\n\n", err.Error()))) - rc.Flush() - return - } - w.Write([]byte(fmt.Sprintf("event: agents\ndata: %s\n\n", cstr.JSONStringify(agents)))) - rc.Flush() - select { - case <-s.ctx.Done(): - case <-r.Context().Done(): - } - w.Write([]byte("event: stop\ndata: disconnected\n\n")) - rc.Flush() - return - } - - agentId := r.URL.Path[1:] - var found bool - for _, agent := range s.Project.Project.Agents { - if agent.ID == agentId || strings.TrimLeft(agent.ID, "agent_") == agentId { - found = true - agentId = agent.ID - r.URL.Path = fmt.Sprintf("/%s", agentId) // in case we used the short version of the agent id - break - } - } - - if !found { - s.logger.Error("agent not found with id: %s", agentId) - sendCORSHeaders(w.Header()) - w.WriteHeader(http.StatusNotFound) - return - } - - if r.Method == "GET" { - message.CustomErrorResponse(w, "Agents, Not Humans, Live Here", "Hi! I'm an Agentuity Agent running in development mode.", "", http.StatusOK) - return - } - - sctx, logger, span := telemetry.StartSpan(r.Context(), s.logger, s.tracer, "TriggerRun", - trace.WithAttributes( - attribute.Bool("@agentuity/devmode", true), - attribute.String("trigger", "manual"), - attribute.String("@agentuity/deploymentId", s.ID), - ), - trace.WithSpanKind(trace.SpanKindConsumer), - ) - - var err error - started := time.Now() - - defer func() { - // only end the span if there was an error - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - } else { - span.SetStatus(codes.Ok, "") - } - span.SetAttributes( - attribute.Int64("@agentuity/cpu_time", time.Since(started).Milliseconds()), - ) - span.End() - s.logger.Info("processed sess_%s in %s", span.SpanContext().TraceID(), time.Since(started)) - }() - - span.SetAttributes( - attribute.String("@agentuity/agentId", agentId), - attribute.String("@agentuity/orgId", s.orgId), - attribute.String("@agentuity/projectId", s.Project.Project.ProjectId), - attribute.String("@agentuity/env", "development"), - ) - - spanContext := span.SpanContext() - traceState := spanContext.TraceState() - traceState, err = traceState.Insert("id", agentId) - if err != nil { - logger.Error("failed to insert agent id into trace state: %s", err) - err = fmt.Errorf("failed to insert agent id into trace state: %w", err) - return - } - traceState, err = traceState.Insert("oid", s.orgId) - if err != nil { - logger.Error("failed to insert org id into trace state: %s", err) - err = fmt.Errorf("failed to insert org id into trace state: %w", err) - return - } - traceState, err = traceState.Insert("pid", s.Project.Project.ProjectId) - if err != nil { - logger.Error("failed to insert project id into trace state: %s", err) - err = fmt.Errorf("failed to insert project id into trace state: %w", err) - return - } - traceState, err = traceState.Insert("d", "1") - if err != nil { - logger.Error("failed to insert devmode status into trace state: %s", err) - err = fmt.Errorf("failed to insert devmode status into trace state: %w", err) - return - } - - newctx := trace.ContextWithSpanContext(sctx, spanContext.WithTraceState(traceState)) - - nr := r.WithContext(newctx) - nr.Header = r.Header.Clone() - nr.Header.Set("x-agentuity-trigger", "manual") - nr.Header.Set("User-Agent", "Agentuity CLI/"+s.version) - propagator.Inject(newctx, propagation.HeaderCarrier(nr.Header)) - - url, err := url.Parse(r.URL.String()) - if err != nil { - logger.Error("failed to parse url: %s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - url.Scheme = "http" - url.Host = fmt.Sprintf("127.0.0.1:%d", s.port) - url.Path = "" // proxy sets so this acts like the base - - logger.Trace("sending to: %s", url) - - proxy := httputil.NewSingleHostReverseProxy(url) - proxy.FlushInterval = -1 // no buffering so we can stream - proxy.ServeHTTP(w, nr) + return s.client.Close() } func (s *Server) WebURL(appUrl string) string { - return fmt.Sprintf("%s/devmode/%s", appUrl, s.ID) + return fmt.Sprintf("%s/devmode/%s", appUrl, s.args.EndpointID) } func (s *Server) PublicURL(appUrl string) string { - return s.publicUrl + return fmt.Sprintf("https://%s", s.args.Hostname) } func (s *Server) AgentURL(agentId string) string { - return fmt.Sprintf("http://127.0.0.1:%d/%s", s.port, agentId) -} - -func isConnectionErrorRetryable(err error) bool { - if strings.Contains(err.Error(), "connection refused") { - return true - } - if strings.Contains(err.Error(), "connection reset by peer") { - return true - } - if strings.Contains(err.Error(), "No connection could be made because the target machine actively refused it") { // windows - return true - } - return false + return fmt.Sprintf("http://127.0.0.1:%d/%s", s.config.AgentPort, agentId) } -type Welcome struct { - Message string `json:"welcome"` - Prompts []struct { - Data string `json:"data"` - ContentType string `json:"contentType"` - } `json:"prompts,omitempty"` +func (s *Server) TelemetryURL() string { + return s.client.TelemetryURL() } -func (s *Server) getWelcome(ctx context.Context, port int) (map[string]Welcome, error) { - url := fmt.Sprintf("http://127.0.0.1:%d/welcome", port) - for i := 0; i < 5; i++ { - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - if isConnectionErrorRetryable(err) { - time.Sleep(time.Millisecond * time.Duration(100*i+1)) - continue - } - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode == 404 { - return nil, nil // this is ok, just means no agents have inspect - } - res := make(map[string]Welcome) - if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - return res, nil - } - return nil, fmt.Errorf("failed to inspect agents after 5 attempts") +func (s *Server) TelemetryAPIKey() string { + return s.client.TelemetryAPIKey() } func (s *Server) HealthCheck(devModeUrl string) error { - started := time.Now() - var i int - for time.Since(started) < 30*time.Second { - i++ - s.logger.Trace("health check request [#%d (%s)]: %s", i, time.Since(started), fmt.Sprintf("%s/_health", devModeUrl)) - req, err := http.NewRequestWithContext(s.ctx, "GET", fmt.Sprintf("%s/_health", devModeUrl), nil) - if err != nil { - return fmt.Errorf("failed to create health check request: %w", err) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - if errors.Is(err, context.Canceled) { - return err - } - s.logger.Trace("health check request failed: %s", err) - dur := time.Millisecond * 150 * time.Duration(math.Pow(float64(i), 2)) - time.Sleep(dur) - continue - } - s.logger.Trace("health check request returned status code: %d", resp.StatusCode) - if resp.StatusCode != http.StatusOK { - s.logger.Trace("health check returned status code: %d", resp.StatusCode) - dur := time.Millisecond * 150 * time.Duration(math.Pow(float64(i), 2)) - time.Sleep(dur) - continue - } - return nil - } - return fmt.Errorf("health check failed after %s", time.Since(started)) + return s.client.HealthCheck(devModeUrl) } func (s *Server) Connect() error { - err := <-s.connected - close(s.connected) - if err != nil { - return err - } - return nil -} - -func (s *Server) monitor() { - t := time.NewTicker(time.Minute * 10) - defer t.Stop() - for { - select { - case <-s.ctx.Done(): - return - case <-t.C: - if s.expiresAt != nil && time.Now().After(*s.expiresAt) { - s.logger.Debug("connection expired, reconnecting") - s.reconnect() - } - } - } + return s.client.Start() } var ( @@ -640,39 +93,10 @@ func (s *Server) GenerateInfoBox(publicUrl string, appUrl string, devModeUrl str } func New(args ServerArgs) (*Server, error) { - id := cstr.NewHash(args.Project.Project.ProjectId, args.UserId) - tracer := otel.Tracer("@agentuity/cli", trace.WithInstrumentationAttributes( - attribute.String("id", id), - attribute.String("@agentuity/orgId", args.OrgId), - attribute.String("@agentuity/userId", args.UserId), - attribute.String("@agentuity/projectId", args.Project.Project.ProjectId), - attribute.Bool("@agentuity/devmode", true), - attribute.String("name", "@agentuity/cli"), - attribute.String("version", args.Version), - ), trace.WithInstrumentationVersion(args.Version)) - - ctx, cancel := context.WithCancel(args.Ctx) - server := &Server{ - ID: id, - logger: args.Logger, - ctx: ctx, - cancel: cancel, - apiurl: args.APIURL, - transportUrl: args.TransportURL, - apiKey: args.APIKey, - Project: args.Project, - orgId: args.OrgId, - userId: args.UserId, - tracer: tracer, - version: args.Version, - port: args.Port, - apiclient: util.NewAPIClient(context.WithoutCancel(ctx), args.Logger, args.APIURL, args.APIKey), - connected: make(chan error, 1), + args: args, + config: args.Config, + client: gravity.New(*args.Config), } - - go server.connect(true) - go server.monitor() - return server, nil } diff --git a/internal/envutil/envutil.go b/internal/envutil/envutil.go index 713c117e..772ae093 100644 --- a/internal/envutil/envutil.go +++ b/internal/envutil/envutil.go @@ -10,10 +10,11 @@ import ( "github.com/agentuity/cli/internal/deployer" "github.com/agentuity/cli/internal/errsystem" - "github.com/agentuity/cli/internal/project" + iproject "github.com/agentuity/cli/internal/project" util "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/logger" + "github.com/agentuity/go-common/project" cstr "github.com/agentuity/go-common/string" "github.com/agentuity/go-common/tui" "github.com/charmbracelet/lipgloss" @@ -79,7 +80,7 @@ func ShouldSyncToProduction(isLocalDev bool) bool { } // ProcessEnvFiles handles .env and template env processing -func ProcessEnvFiles(ctx context.Context, logger logger.Logger, dir string, theproject *project.Project, projectData *project.ProjectData, apiUrl, token string, force bool, isLocalDev bool) (*deployer.EnvFile, *project.ProjectData) { +func ProcessEnvFiles(ctx context.Context, logger logger.Logger, dir string, theproject *project.Project, projectData *iproject.ProjectData, apiUrl, token string, force bool, isLocalDev bool) (*deployer.EnvFile, *iproject.ProjectData) { envfilename, err := DetermineEnvFilename(dir, isLocalDev) if err != nil { errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to create .env.development file")).ShowErrorAndExit() @@ -177,10 +178,10 @@ func HandleMissingTemplateEnvs(logger logger.Logger, dir, envfilename string, le } // HandleMissingProjectEnvs handles missing envs in project -func HandleMissingProjectEnvs(ctx context.Context, logger logger.Logger, le []env.EnvLineComment, projectData *project.ProjectData, theproject *project.Project, apiUrl, token string, force bool, envFilename string) *project.ProjectData { +func HandleMissingProjectEnvs(ctx context.Context, logger logger.Logger, le []env.EnvLineComment, projectData *iproject.ProjectData, theproject *project.Project, apiUrl, token string, force bool, envFilename string) *iproject.ProjectData { if projectData == nil { - projectData = &project.ProjectData{} + projectData = &iproject.ProjectData{} } keyvalue := map[string]string{} for _, ev := range le { @@ -243,7 +244,7 @@ func HandleMissingProjectEnvs(ctx context.Context, logger logger.Logger, le []en projectData.Env[key] = val } } - _, err := theproject.SetProjectEnv(ctx, logger, apiUrl, token, projectData.Env, projectData.Secrets) + _, err := iproject.SetProjectEnv(ctx, logger, apiUrl, token, theproject.ProjectId, projectData.Env, projectData.Secrets) if err != nil { errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithUserMessage("Failed to save project settings")).ShowErrorAndExit() } diff --git a/internal/errsystem/errorcodes.go b/internal/errsystem/errorcodes.go index c2e0812c..bbc95e95 100644 --- a/internal/errsystem/errorcodes.go +++ b/internal/errsystem/errorcodes.go @@ -114,4 +114,8 @@ var ( Code: "CLI-0028", Message: "Failed to delete API key", } + ErrRetrieveDevmodeEndpoint = errorType{ + Code: "CLI-0029", + Message: "Failed to retrieve devmode endpoint", + } ) diff --git a/internal/gravity/gravity.go b/internal/gravity/gravity.go new file mode 100644 index 00000000..d2e51e57 --- /dev/null +++ b/internal/gravity/gravity.go @@ -0,0 +1,625 @@ +package gravity + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" + "sync" + "time" + + "github.com/agentuity/cli/internal/project" + "github.com/agentuity/go-common/gravity" + "github.com/agentuity/go-common/gravity/proto" + "github.com/agentuity/go-common/logger" + cnet "github.com/agentuity/go-common/network" + cproject "github.com/agentuity/go-common/project" + cstr "github.com/agentuity/go-common/string" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/waiter" +) + +const ( + nicID = 1 + mtu = 1500 +) + +type Client struct { + context context.Context + logger logger.Logger + version string + orgID string + projectID string + project project.ProjectContext + endpointID string + url string + sdkKey string + proxyPort uint + agentPort uint + ephemeral bool + clientname string + dynamicHostname bool + dynamicProject bool + server *http.Server + client *gravity.GravityClient + once sync.Once + stack *stack.Stack + endpoint *channel.Endpoint + provider *cliProvider +} + +type Config struct { + Context context.Context + Logger logger.Logger + Version string // of the cli + OrgID string + Project project.ProjectContext + EndpointID string + URL string + SDKKey string + ProxyPort uint + AgentPort uint + Ephemeral bool + ClientName string + DynamicHostname bool + DynamicProject bool +} + +func New(config Config) *Client { + return &Client{ + context: config.Context, + logger: config.Logger.WithPrefix("[gravity]"), + version: config.Version, + orgID: config.OrgID, + projectID: config.Project.Project.ProjectId, + project: config.Project, + endpointID: config.EndpointID, + url: config.URL, + sdkKey: config.SDKKey, + ephemeral: config.Ephemeral, + proxyPort: config.ProxyPort, + agentPort: config.AgentPort, + clientname: config.ClientName, + dynamicHostname: config.DynamicHostname, + dynamicProject: config.DynamicProject, + } +} + +// APIURL returns the API URL of the client. +func (c *Client) APIURL() string { + return c.client.GetAPIURL() +} + +// APIKey returns the API key of the client. +func (c *Client) APIKey() string { + return c.client.GetSecret() +} + +// TelemetryURL returns the telemetry URL of the client. +func (c *Client) TelemetryURL() string { + return c.provider.config.TelemetryURL +} + +// TelemetryAPIKey returns the telemetry API key of the client. +func (c *Client) TelemetryAPIKey() string { + return c.provider.config.TelemetryAPIKey +} + +// Hostname returns the hostname of the client. +func (c *Client) Hostname() string { + return c.provider.config.Hostname +} + +// OrgID returns the organization ID of the client. +func (c *Client) OrgID() string { + return c.provider.config.OrgID +} + +// EndpointID returns the endpoint ID of the client. +func (c *Client) EndpointID() string { + return c.endpointID +} + +// For each TCP connection: connect to local HTTPS server and proxy bytes. +func (c *Client) bridgeToLocalTLS(remote *gonet.TCPConn) { + logger := c.logger + logger.Debug("bridgeToLocalTLS: starting...") + defer remote.Close() + addr := fmt.Sprintf("127.0.0.1:%d", c.proxyPort) + logger.Debug("bridgeToLocalTLS: attempting to dial %s...", addr) + local, err := net.Dial("tcp", addr) + + if err != nil { + logger.Error("dial error: %v", err) + return + } + logger.Info("connected to local HTTPS server: %s", local.RemoteAddr().String()) + defer local.Close() + + logger.Trace("bridgeToLocalTLS: starting copy operations...") + go func() { + logger.Trace("bridgeToLocalTLS: copying netstack -> local server...") + n, err := io.Copy(local, remote) + logger.Trace("bridgeToLocalTLS: netstack -> local server finished (copied %d bytes, err: %v)", n, err) + }() + logger.Trace("bridgeToLocalTLS: copying local server -> netstack...") + n, err := io.Copy(remote, local) + logger.Trace("bridgeToLocalTLS: local server -> netstack finished (copied %d bytes, err: %v)", n, err) +} + +// Close will close the client and all the associated services. +func (c *Client) Close() error { + var err error + c.once.Do(func() { + c.logger.Debug("closing client") + err = c.cleanup() + c.logger.Debug("closed") + }) + return err +} + +// Close will close the client and all the associated services. +func (c *Client) cleanup() error { + var err error + if c.client != nil { + c.client.Close() + c.client = nil + } + if c.server != nil { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err = c.server.Shutdown(ctx) + c.server = nil + } + if c.endpoint != nil { + c.endpoint.Close() + c.endpoint = nil + } + if c.stack != nil { + c.stack.Close() + c.stack = nil + } + return err +} + +type AgentWelcome struct { + cproject.AgentConfig + Welcome +} + +type AgentsControlResponse struct { + ProjectID string `json:"projectId"` + ProjectName string `json:"projectName"` + Agents []AgentWelcome `json:"agents"` +} + +type Welcome struct { + Message string `json:"welcome"` + Prompts []struct { + Data string `json:"data"` + ContentType string `json:"contentType"` + } `json:"prompts,omitempty"` +} + +func (c *Client) getWelcome(ctx context.Context, port int) (map[string]Welcome, error) { + url := fmt.Sprintf("http://127.0.0.1:%d/welcome", port) + for i := range 5 { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + if isConnectionErrorRetryable(err) { + time.Sleep(time.Millisecond * time.Duration(100*i+1)) + continue + } + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode == 404 { + return nil, nil // this is ok, just means no agents have inspect + } + res := make(map[string]Welcome) + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return nil, err + } + return res, nil + } + return nil, fmt.Errorf("failed to inspect agents after 5 attempts") +} + +func (c *Client) getAgents(ctx context.Context, project *cproject.Project) (*AgentsControlResponse, error) { + var resp = &AgentsControlResponse{ + ProjectID: project.ProjectId, + ProjectName: project.Name, + } + welcome, err := c.getWelcome(ctx, int(c.agentPort)) + if err != nil { + return nil, err + } + for _, agent := range project.Agents { + var w Welcome + if welcome != nil { + w = welcome[agent.ID] + } + resp.Agents = append(resp.Agents, AgentWelcome{ + AgentConfig: agent, + Welcome: w, + }) + } + return resp, nil +} + +func (c *Client) HealthCheck(devModeUrl string) error { + started := time.Now() + var i int + for time.Since(started) < 30*time.Second { + i++ + c.logger.Trace("health check request [#%d (%s)]: %s", i, time.Since(started), fmt.Sprintf("%s/_health", devModeUrl)) + req, err := http.NewRequestWithContext(c.context, "GET", fmt.Sprintf("%s/_health", devModeUrl), nil) + if err != nil { + return fmt.Errorf("failed to create health check request: %w", err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + if errors.Is(err, context.Canceled) { + return err + } + c.logger.Trace("health check request failed: %s", err) + dur := time.Millisecond * 150 * time.Duration(math.Pow(float64(i), 2)) + time.Sleep(dur) + continue + } + c.logger.Trace("health check request returned status code: %d", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + c.logger.Trace("health check returned status code: %d", resp.StatusCode) + dur := time.Millisecond * 150 * time.Duration(math.Pow(float64(i), 2)) + time.Sleep(dur) + continue + } + return nil + } + return fmt.Errorf("health check failed after %s", time.Since(started)) +} + +// Start will start the client and all the associated services. +func (c *Client) Start() error { + var success bool + + defer func() { + if !success { + c.Close() + } + }() + + c.logger.Debug("proxy port: %d, agent port: %d", c.proxyPort, c.agentPort) + + ipv4addr, err := getPrivateIPv4() + if err != nil { + return err + } + ipv6Address := cnet.NewIPv6Address(cnet.GetRegion(""), cnet.NetworkHadron, c.orgID, c.endpointID, ipv4addr) + + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("error getting hostname: %w", err) + } + + var dynamicProjectRouting string + if c.dynamicProject { + dynamicProjectRouting = c.projectID + } + + capabilities := &proto.ClientCapabilities{ + DynamicHostname: c.dynamicHostname, + DynamicProjectRouting: dynamicProjectRouting, + } + + resp, err := gravity.Provision(gravity.ProvisionRequest{ + Context: c.context, + GravityURL: c.url, + InstanceID: c.endpointID, + Region: "unknown", + Provider: "other", // TODO: change this to support this provider + PrivateIP: ipv4addr, + Token: c.sdkKey, + Hostname: hostname, + Ephemeral: c.ephemeral, + Capabilities: capabilities, + }) + if err != nil { + return fmt.Errorf("failed to provision machine: %w", err) + } + + // FIXME: cert expires + + log := c.logger + + log.Debug("machine provisioned") + + cert, err := tls.X509KeyPair(resp.Certificate, resp.PrivateKey) + if err != nil { + return fmt.Errorf("failed to load client certificate: %w", err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(resp.CaCertificate) { + return fmt.Errorf("failed to parse CA certificate") + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + MinVersion: tls.VersionTLS13, + CurvePreferences: []tls.CurveID{tls.X25519, tls.X25519MLKEM768, tls.CurveP256}, + NextProtos: []string{"h2", "http/1.1"}, // Prefer HTTP/2 + } + + upstreamURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", c.agentPort)) + if err != nil { + return fmt.Errorf("failed to parse upstream URL: %w", err) + } + + proxy := httputil.NewSingleHostReverseProxy(upstreamURL) + proxy.FlushInterval = -1 + + server := &http.Server{ + Addr: fmt.Sprintf(":%d", c.proxyPort), + TLSConfig: tlsConfig, + DisableGeneralOptionsHandler: true, + ReadTimeout: 0, + WriteTimeout: 0, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Debug("incoming request: %s %s", r.Method, r.URL.Path) + if c.ephemeral { + switch r.URL.Path { + case "/_health": + sendCORSHeaders(w.Header()) + w.WriteHeader(http.StatusOK) + return + case "/_agents": + sendCORSHeaders(w.Header()) + agents, err := c.getAgents(r.Context(), c.project.Project) + if err != nil { + c.logger.Error("failed to marshal agents control response: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, cstr.JSONStringify(agents)) + return + case "/_control": + sendCORSHeaders(w.Header()) + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.WriteHeader(http.StatusOK) + rc := http.NewResponseController(w) + rc.Flush() + c.HealthCheck(fmt.Sprintf("http://127.0.0.1:%d", c.agentPort)) // make sure the server is running + io.WriteString(w, "event: start\ndata: connected\n\n") + agents, err := c.getAgents(r.Context(), c.project.Project) + if err != nil { + c.logger.Error("failed to marshal agents control response: %s", err) + io.WriteString(w, fmt.Sprintf("event: error\ndata: %q\n\n", err.Error())) + rc.Flush() + return + } + io.WriteString(w, fmt.Sprintf("event: agents\ndata: %s\n\n", cstr.JSONStringify(agents))) + rc.Flush() + select { + case <-c.context.Done(): + case <-r.Context().Done(): + } + io.WriteString(w, "event: stop\ndata: disconnected\n\n") + rc.Flush() + return + default: + } + } + started := time.Now() + proxy.ServeHTTP(w, r) + tp := r.Header.Get("traceparent") + if tp != "" { + tok := strings.Split(tp, "-") + c.logger.Info("%s %s (sess_%s) in %s", r.Method, r.URL.Path, tok[1], time.Since(started)) + } + }), + } + c.server = server + + go func() { + if err := server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.Fatal("failed to start gravity proxy HTTPS server: %v", err) + } + }() + + // Create netstack. + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol}, + }) + c.stack = s + + // NIC that we can write raw packets into. + linkEP := channel.New(1024, mtu, "") + if err := s.CreateNIC(nicID, linkEP); err != nil { + return fmt.Errorf("failed to create virtual NIC: %s", err) + } + c.endpoint = linkEP + ipBytes := net.ParseIP(ipv6Address.String()).To16() + var addr [16]byte + copy(addr[:], ipBytes) + if err := s.AddProtocolAddress(nicID, + tcpip.ProtocolAddress{ + Protocol: ipv6.ProtocolNumber, + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: tcpip.AddrFrom16(addr), + PrefixLen: 64, + }, + }, + stack.AddressProperties{}, + ); err != nil { + return fmt.Errorf("failed to create protocol address: %s", err) + } + + // Add default route + subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(make([]byte, 16)), tcpip.MaskFromBytes(make([]byte, 16))) + if err != nil { + return fmt.Errorf("failed to create subnet: %w", err) + } + s.SetRouteTable([]tcpip.Route{ + { + Destination: subnet, + NIC: nicID, + }, + }) + + // Start a TCP forwarder for every incoming connection using working pattern + fwd := tcp.NewForwarder(s, 1024, 1024, func(r *tcp.ForwarderRequest) { + wq := new(waiter.Queue) + id := r.ID() + log.Debug("incoming TCP connection: %s → %s", id.RemoteAddress, id.LocalAddress) + + log.Debug("about to call CreateEndpoint...") + ep, err := r.CreateEndpoint(wq) + log.Debug("CreateEndpoint returned: err=%v", err) + + if err != nil { + log.Error("endpoint creation error: %v", err) + r.Complete(true) + return + } + + r.Complete(false) + + tcpConn := gonet.NewTCPConn(wq, ep) + log.Debug("created TCP conn, starting bridge to local server") + go c.bridgeToLocalTLS(tcpConn) + }) + s.SetTransportProtocolHandler(tcp.ProtocolNumber, fwd.HandlePacket) + + var network networkProvider + var provider cliProvider + provider.logger = log + provider.ep = linkEP + c.provider = &provider + provider.connected = make(chan struct{}, 1) + + // Add egress pump to drain outbound packets from the channel endpoint + go func() { + log.Debug("starting egress pump...") + for { + select { + case <-c.context.Done(): + return + default: + pkt := linkEP.ReadContext(c.context) + if pkt == nil { + continue + } + // Extract the raw packet data + buf := pkt.ToBuffer() + data := buf.Flatten() + log.Trace("sending outbound packet (%d bytes)", len(data)) + // Send the raw IP packet to Gravity + _, err := network.Write(data) + pkt.DecRef() // free gvisor buffer + if err != nil { + log.Error("failed to send outbound packet: %v", err) + } + } + } + }() + + client, err := gravity.New(gravity.GravityConfig{ + Context: c.context, + Logger: log, + URL: c.url, + ClientName: c.clientname, + ClientVersion: c.version, + AuthToken: resp.ClientToken, + Cert: string(resp.Certificate), + Key: string(resp.PrivateKey), + CACert: string(resp.CaCertificate), + InstanceID: c.endpointID, + ReportStats: false, + WorkingDir: ".", + ConnectionPoolConfig: &gravity.ConnectionPoolConfig{ + PoolSize: 1, + StreamsPerConnection: 1, + AllocationStrategy: gravity.WeightedRoundRobin, + HealthCheckInterval: time.Second * 30, + FailoverTimeout: time.Second, + }, + Capabilities: capabilities, + NetworkInterface: &network, + Provider: &provider, + IP4Address: ipv4addr, + IP6Address: ipv6Address.String(), + SkipAutoReconnect: true, + }) + if err != nil { + return fmt.Errorf("failed to create gravity client: %w", err) + } + c.client = client + network.client = client + + if err := client.Start(); err != nil { + return fmt.Errorf("failed to start the gravity client: %w", err) + } + + select { + case <-c.context.Done(): + return c.context.Err() + case <-time.After(time.Second * 10): + return fmt.Errorf("timed out waiting for provider connection") + case <-provider.connected: + break + } + + go func() { + log.Debug("waiting on provider disconnect") + client.Disconnected(c.context) + log.Debug("provider disconnected") + select { + case <-c.context.Done(): + log.Debug("provider disconnected but context canceled") + c.Close() + return + default: + log.Debug("provider disconnected, restarting") + c.cleanup() + log.Info("reconnecting to server ... one moment") + select { + case <-c.context.Done(): + return + case <-time.After(2 * time.Second): + } + if err := c.Start(); err != nil { + log.Fatal("failed to re-connect to the devmode server: %s", err) + } + log.Info("reconnected to devmode server") + } + }() + + success = true + + return nil +} diff --git a/internal/gravity/provider.go b/internal/gravity/provider.go new file mode 100644 index 00000000..b3450ff3 --- /dev/null +++ b/internal/gravity/provider.go @@ -0,0 +1,96 @@ +package gravity + +import ( + "context" + + "github.com/agentuity/go-common/gravity" + "github.com/agentuity/go-common/gravity/provider" + "github.com/agentuity/go-common/logger" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type networkProvider struct { + client *gravity.GravityClient +} + +// RouteTraffic configures routing for the specified network ranges +func (p *networkProvider) RouteTraffic(nets []string) error { + return nil +} + +// UnrouteTraffic removes all routing configurations +func (p *networkProvider) UnrouteTraffic() error { + return nil +} + +// Read reads a packet from the TUN interface into the provided buffer +func (p *networkProvider) Read(buffer []byte) (int, error) { + return 0, nil +} + +// Write writes a packet to the TUN interface +func (p *networkProvider) Write(packet []byte) (int, error) { + if err := p.client.WritePacket(packet); err != nil { + return 0, err + } + return len(packet), nil +} + +// Running returns true if the TUN interface is currently running +func (p *networkProvider) Running() bool { + return true +} + +// Start starts the TUN interface and calls the handler with each outbound packet. +// The packet passed to the handler is NOT a copy, so it must be copied if used after the handler returns. +func (p *networkProvider) Start(handler func(packet []byte)) { +} + +type cliProvider struct { + ep *channel.Endpoint + logger logger.Logger + config provider.Configuration + connected chan struct{} +} + +// Configure will be called to configure the provider with the given configuration +func (p *cliProvider) Configure(config provider.Configuration) error { + p.logger.Trace("configuring devmode provider with config: %+v", config) + p.config = config + p.connected <- struct{}{} + return nil +} + +// Provision provisions a resource for a given spec +func (p *cliProvider) Provision(ctx context.Context, request *provider.ProvisionRequest) (*provider.Resource, error) { + return nil, nil +} + +// Deprovision deprovisions a provisioned resource +func (p *cliProvider) Deprovision(ctx context.Context, resourceID string, reason provider.DeprovisionReason) error { + return nil +} + +// Resources returns a list of all resources regardless of state +func (p *cliProvider) Resources() []*provider.Resource { + return nil +} + +// SetMetricsCollector sets the metrics collector for runtime stats collection +func (p *cliProvider) SetMetricsCollector(collector provider.ProjectRuntimeStatsCollector) { +} + +// ProcessInPacket processes an inbound packet from the gravity server +func (p *cliProvider) ProcessInPacket(payload []byte) { + if p.ep == nil { + return + } + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{}) + view := buffer.NewView(len(payload)) + view.Write(payload) + pkt.Data().AppendView(view) + p.ep.InjectInbound(ipv6.ProtocolNumber, pkt) +} diff --git a/internal/gravity/util.go b/internal/gravity/util.go new file mode 100644 index 00000000..ae7e011c --- /dev/null +++ b/internal/gravity/util.go @@ -0,0 +1,42 @@ +package gravity + +import ( + "fmt" + "net" + "net/http" + "strings" +) + +func getPrivateIPv4() (string, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "", fmt.Errorf("failed to get private IPv4: %w", err) + } + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { + return ipnet.IP.String(), nil + } + } + return "", fmt.Errorf("no private IPv4 address found") +} + +func sendCORSHeaders(headers http.Header) { + headers.Set("access-control-allow-origin", "*") + headers.Set("access-control-expose-headers", "Content-Type") + headers.Set("access-control-allow-headers", "Content-Type, Authorization") + headers.Set("access-control-allow-methods", "GET, POST, OPTIONS") +} + +func isConnectionErrorRetryable(err error) bool { + if strings.Contains(err.Error(), "connection refused") { + return true + } + if strings.Contains(err.Error(), "connection reset by peer") { + return true + } + if strings.Contains(err.Error(), "No connection could be made because the target machine actively refused it") { // windows + return true + } + return false +} diff --git a/internal/mcp/agent.go b/internal/mcp/agent.go index 65d96b41..ce84f529 100644 --- a/internal/mcp/agent.go +++ b/internal/mcp/agent.go @@ -6,7 +6,7 @@ import ( "slices" "github.com/agentuity/cli/internal/agent" - "github.com/agentuity/cli/internal/project" + "github.com/agentuity/go-common/project" mcp_golang "github.com/agentuity/mcp-golang/v2" ) diff --git a/internal/mcp/env.go b/internal/mcp/env.go index 6b910b6f..15f374b9 100644 --- a/internal/mcp/env.go +++ b/internal/mcp/env.go @@ -42,9 +42,9 @@ func init() { } var err error if args.IsSecret { - _, err = c.Project.SetProjectEnv(ctx, c.Logger, c.APIURL, c.APIKey, map[string]string{}, map[string]string{args.Key: args.Value}) + _, err = project.SetProjectEnv(ctx, c.Logger, c.APIURL, c.APIKey, c.Project.ProjectId, map[string]string{}, map[string]string{args.Key: args.Value}) } else { - _, err = c.Project.SetProjectEnv(ctx, c.Logger, c.APIURL, c.APIKey, map[string]string{args.Key: args.Value}, map[string]string{}) + _, err = project.SetProjectEnv(ctx, c.Logger, c.APIURL, c.APIKey, c.Project.ProjectId, map[string]string{args.Key: args.Value}, map[string]string{}) } if err != nil { return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Error setting environment variable: %s", err))), nil @@ -84,7 +84,7 @@ func init() { if resp := ensureProject(&c); resp != nil { return resp, nil } - if err := c.Project.DeleteProjectEnv(ctx, c.Logger, c.APIURL, c.APIKey, args.Keys, args.Keys); err != nil { + if err := project.DeleteProjectEnv(ctx, c.Logger, c.APIURL, c.APIKey, c.Project.ProjectId, args.Keys, args.Keys); err != nil { return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Error deleting environment variable: %s", err))), nil } if err := project.RemoveEnvValues(ctx, c.Logger, c.ProjectDir, args.Keys...); err != nil { diff --git a/internal/mcp/mcp.go b/internal/mcp/mcp.go index 36a1ba75..6fdb8ba1 100644 --- a/internal/mcp/mcp.go +++ b/internal/mcp/mcp.go @@ -3,8 +3,8 @@ package mcp import ( "context" - "github.com/agentuity/cli/internal/project" "github.com/agentuity/go-common/logger" + "github.com/agentuity/go-common/project" mcp_golang "github.com/agentuity/mcp-golang/v2" "github.com/spf13/cobra" ) diff --git a/internal/project/project.go b/internal/project/project.go index 37740d49..9b981ab1 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strings" "github.com/Masterminds/semver" @@ -15,13 +16,13 @@ import ( "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/logger" + "github.com/agentuity/go-common/project" + "github.com/agentuity/go-common/slice" cstr "github.com/agentuity/go-common/string" "github.com/agentuity/go-common/tui" "github.com/spf13/cobra" "github.com/spf13/viper" - yc "github.com/zijiren233/yaml-comment" "gopkg.in/yaml.v3" - "k8s.io/apimachinery/pkg/api/resource" ) const ( @@ -30,11 +31,6 @@ const ( var Version string -var ( - ErrProjectNotFound = errors.New("project not found") - ErrProjectMissingProjectId = errors.New("missing project_id value") -) - type initProjectResult struct { Success bool `json:"success"` Data ProjectData `json:"data"` @@ -42,14 +38,14 @@ type initProjectResult struct { } type ProjectData struct { - APIKey string `json:"api_key"` - ProjectKey string `json:"projectKey"` - ProjectId string `json:"id"` - OrgId string `json:"orgId"` - Env map[string]string `json:"env"` - Secrets map[string]string `json:"secrets"` - WebhookAuthToken string `json:"webhookAuthToken,omitempty"` - Agents []AgentConfig `json:"agents"` + APIKey string `json:"api_key"` + ProjectKey string `json:"projectKey"` + ProjectId string `json:"id"` + OrgId string `json:"orgId"` + Env map[string]string `json:"env"` + Secrets map[string]string `json:"secrets"` + WebhookAuthToken string `json:"webhookAuthToken,omitempty"` + Agents []project.AgentConfig `json:"agents"` } type InitProjectArgs struct { @@ -61,7 +57,7 @@ type InitProjectArgs struct { Name string Description string EnableWebhookAuth bool - Agents []AgentConfig + Agents []project.AgentConfig AuthType string Framework string } @@ -98,162 +94,6 @@ func InitProject(ctx context.Context, logger logger.Logger, args InitProjectArgs return &result.Data, nil } -func getFilename(dir string) string { - return filepath.Join(dir, "agentuity.yaml") -} - -func ProjectExists(dir string) bool { - fn := getFilename(dir) - return util.Exists(fn) -} - -type Resources struct { - Memory string `json:"memory,omitempty" yaml:"memory,omitempty" hc:"The memory requirements"` - CPU string `json:"cpu,omitempty" yaml:"cpu,omitempty" hc:"The CPU requirements"` - Disk string `json:"disk,omitempty" yaml:"disk,omitempty" hc:"The disk size requirements"` - - CPUQuantity resource.Quantity `json:"-" yaml:"-"` - MemoryQuantity resource.Quantity `json:"-" yaml:"-"` - DiskQuantity resource.Quantity `json:"-" yaml:"-"` -} - -type Mode struct { - Type string `json:"type" yaml:"type" hc:"on-demand or provisioned"` // on-demand or provisioned - Idle *string `json:"idle,omitempty" yaml:"idle,omitempty" hc:"duration in seconds if on-demand, optional"` // duration in seconds if on-demand, optional -} - -type Deployment struct { - Command string `json:"command" yaml:"command"` - Args []string `json:"args" yaml:"args"` - Resources *Resources `json:"resources" yaml:"resources" hc:"You should tune the resources for the deployment"` - Mode *Mode `json:"mode,omitempty" yaml:"mode,omitempty" hc:"The deployment mode"` - Dependencies []string `json:"dependencies,omitempty" yaml:"dependencies,omitempty" hc:"The dependencies to install before running the deployment"` -} - -type Watch struct { - Enabled bool `json:"enabled" yaml:"enabled" hc:"Whether to watch for changes and automatically restart the server"` - Files []string `json:"files" yaml:"files" hc:"Rules for files to watch for changes"` -} - -type Development struct { - Port int `json:"port" yaml:"port" hc:"The port to run the development server on which can be overridden by setting the PORT environment variable"` - Watch Watch `json:"watch" yaml:"watch"` - Command string `json:"command" yaml:"command" hc:"The command to run the development server"` - Args []string `json:"args" yaml:"args" hc:"The arguments to pass to the development server"` -} - -type AgentConfig struct { - ID string `json:"id" yaml:"id" hc:"The ID of the Agent which is automatically generated"` - Name string `json:"name" yaml:"name" hc:"The name of the Agent which is editable"` - Description string `json:"description,omitempty" yaml:"description,omitempty" hc:"The description of the Agent which is editable"` - Types []string `json:"io_types,omitempty" yaml:"io_types,omitempty" hc:"The IO types of the Agent which is editable"` -} - -type Project struct { - Version string `json:"version" yaml:"version" hc:"The version semver range required to run this project"` - ProjectId string `json:"project_id" yaml:"project_id" hc:"The ID of the project which is automatically generated"` - Name string `json:"name" yaml:"name" hc:"The name of the project which is editable"` - Description string `json:"description" yaml:"description" hc:"The description of the project which is editable"` - Development *Development `json:"development,omitempty" yaml:"development,omitempty" hc:"The development configuration for the project"` - Deployment *Deployment `json:"deployment,omitempty" yaml:"deployment,omitempty"` - Bundler *Bundler `json:"bundler,omitempty" yaml:"bundler,omitempty" hc:"You should not need to change these value"` - Agents []AgentConfig `json:"agents" yaml:"agents" hc:"The agents that are part of this project"` -} - -func (p *Project) SafeFilename() string { - return util.SafeProjectFilename(p.Name, p.IsPython()) -} - -func (p *Project) IsPython() bool { - return p.Bundler.Language == "python" || p.Bundler.Language == "py" -} - -// Load will load the project from a file in the given directory. -func (p *Project) Load(dir string) error { - fn := getFilename(dir) - if !util.Exists(fn) { - return nil - } - of, err := os.Open(fn) - if err != nil { - return fmt.Errorf("failed to open project file: %s. %w", fn, err) - } - defer of.Close() - if err := yaml.NewDecoder(of).Decode(p); err != nil { - return fmt.Errorf("failed to decode YAML project file: %s. %w", fn, err) - } - if p.ProjectId == "" { - return ErrProjectMissingProjectId - } - if p.Bundler == nil { - return fmt.Errorf("missing bundler value, please run `agentuity new` to create a new project") - } - if p.Bundler.Language == "" { - return fmt.Errorf("missing bundler.language value, please run `agentuity new` to create a new project") - } - switch p.Bundler.Language { - case "js", "javascript", "typescript": - if p.Bundler.Runtime != "bunjs" && p.Bundler.Runtime != "nodejs" { - return fmt.Errorf("invalid bundler.runtime value: %s. only bunjs and nodejs are supported", p.Bundler.Runtime) - } - case "py", "python": - if p.Bundler.Runtime != "uv" { - return fmt.Errorf("invalid bundler.runtime value: %s. only uv is supported", p.Bundler.Runtime) - } - default: - return fmt.Errorf("invalid bundler.language value: %s. only js or py are supported", p.Bundler.Language) - } - if p.Bundler.AgentConfig.Dir == "" { - return fmt.Errorf("missing bundler.Agents.dir value (or its empty), please run `agentuity new` to create a new project") - } - if p.Deployment != nil { - if p.Deployment.Resources != nil { - val, err := resource.ParseQuantity(p.Deployment.Resources.CPU) - if err != nil { - return fmt.Errorf("error validating deploy cpu value '%s'. %w", p.Deployment.Resources.CPU, err) - } - p.Deployment.Resources.CPUQuantity = val - val, err = resource.ParseQuantity(p.Deployment.Resources.Memory) - if err != nil { - return fmt.Errorf("error validating deploy memory value '%s'. %w", p.Deployment.Resources.Memory, err) - } - p.Deployment.Resources.MemoryQuantity = val - val, err = resource.ParseQuantity(p.Deployment.Resources.Disk) - if err != nil { - return fmt.Errorf("error validating deploy disk value '%s'. %w", p.Deployment.Resources.Disk, err) - } - p.Deployment.Resources.DiskQuantity = val - } - if p.Deployment.Mode != nil { - if p.Deployment.Mode.Type != "on-demand" && p.Deployment.Mode.Type != "provisioned" { - return fmt.Errorf("invalid deployment mode value: %s. only on-demand or provisioned are supported", p.Deployment.Mode.Type) - } - } - } - return nil -} - -// Save will save the project to a file in the given directory. -func (p *Project) Save(dir string) error { - fn := getFilename(dir) - of, err := os.Create(fn) - if err != nil { - return err - } - defer of.Close() - of.WriteString("# yaml-language-server: $schema=https://raw.githubusercontent.com/agentuity/cli/refs/heads/main/agentuity.schema.json\n") - of.WriteString("\n") - of.WriteString("# ------------------------------------------------\n") - of.WriteString("# This file is generated by Agentuity\n") - of.WriteString("# You should check this file into version control\n") - of.WriteString("# ------------------------------------------------\n") - of.WriteString("\n") - enc := yaml.NewEncoder(of) - enc.SetIndent(2) - yenc := yc.NewEncoder(enc) - return yenc.Encode(p) -} - const ( defaultMemory = "1Gi" defaultCPU = "1000M" @@ -261,17 +101,17 @@ const ( ) // NewProject will create a new project that is empty. -func NewProject() *Project { +func NewProject() *project.Project { var version string if Version == "" || Version == "dev" { version = ">=0.0.0" // should only happen in dev cli } else { version = ">=" + Version } - return &Project{ + return &project.Project{ Version: version, - Deployment: &Deployment{ - Resources: &Resources{ + Deployment: &project.Deployment{ + Resources: &project.Resources{ Memory: defaultMemory, CPU: defaultCPU, Disk: defaultDisk, @@ -385,18 +225,18 @@ func DeleteProjects(ctx context.Context, logger logger.Logger, baseUrl string, t return resp.Data, nil } -func (p *Project) GetProject(ctx context.Context, logger logger.Logger, baseUrl string, token string, shouldMask bool, includeProjectKeys bool) (*ProjectData, error) { - if p.ProjectId == "" { - return nil, ErrProjectNotFound +func GetProject(ctx context.Context, logger logger.Logger, baseUrl string, token string, projectId string, shouldMask bool, includeProjectKeys bool) (*ProjectData, error) { + if projectId == "" { + return nil, project.ErrProjectNotFound } client := util.NewAPIClient(ctx, logger, baseUrl, token) var projectResponse ProjectResponse - if err := client.Do("GET", fmt.Sprintf("/cli/project/%s?mask=%t&includeProjectKeys=%t", p.ProjectId, shouldMask, includeProjectKeys), nil, &projectResponse); err != nil { + if err := client.Do("GET", fmt.Sprintf("/cli/project/%s?mask=%t&includeProjectKeys=%t", projectId, shouldMask, includeProjectKeys), nil, &projectResponse); err != nil { var apiErr *util.APIError if errors.As(err, &apiErr) { if apiErr.Status == 404 { - return nil, ErrProjectNotFound + return nil, project.ErrProjectNotFound } } return nil, fmt.Errorf("error getting project env: %w", err) @@ -407,7 +247,7 @@ func (p *Project) GetProject(ctx context.Context, logger logger.Logger, baseUrl return &projectResponse.Data, nil } -func (p *Project) SetProjectEnv(ctx context.Context, logger logger.Logger, baseUrl string, token string, env map[string]string, secrets map[string]string) (*ProjectData, error) { +func SetProjectEnv(ctx context.Context, logger logger.Logger, baseUrl string, token string, projectId string, env map[string]string, secrets map[string]string) (*ProjectData, error) { client := util.NewAPIClient(ctx, logger, baseUrl, token) var projectResponse ProjectResponse _env := make(map[string]string) @@ -422,7 +262,7 @@ func (p *Project) SetProjectEnv(ctx context.Context, logger logger.Logger, baseU _secrets[k] = v } } - if err := client.Do("PUT", fmt.Sprintf("/cli/project/%s/env", p.ProjectId), map[string]any{ + if err := client.Do("PUT", fmt.Sprintf("/cli/project/%s/env", projectId), map[string]any{ "env": _env, "secrets": _secrets, }, &projectResponse); err != nil { @@ -434,10 +274,10 @@ func (p *Project) SetProjectEnv(ctx context.Context, logger logger.Logger, baseU return &projectResponse.Data, nil } -func (p *Project) DeleteProjectEnv(ctx context.Context, logger logger.Logger, baseUrl string, token string, env []string, secrets []string) error { +func DeleteProjectEnv(ctx context.Context, logger logger.Logger, baseUrl string, token string, projectId string, env []string, secrets []string) error { client := util.NewAPIClient(ctx, logger, baseUrl, token) var projectResponse ProjectResponse - if err := client.Do("DELETE", fmt.Sprintf("/cli/project/%s/env", p.ProjectId), map[string]any{ + if err := client.Do("DELETE", fmt.Sprintf("/cli/project/%s/env", projectId), map[string]any{ "env": env, "secrets": secrets, }, &projectResponse); err != nil { @@ -450,24 +290,24 @@ func (p *Project) DeleteProjectEnv(ctx context.Context, logger logger.Logger, ba } type ProjectImportRequest struct { - Name string `json:"name"` - Description string `json:"description"` - Provider string `json:"provider"` - OrgId string `json:"orgId"` - Agents []AgentConfig `json:"agents"` - EnableWebhookAuth bool `json:"enableWebhookAuth"` - CopiedFromProjectId string `json:"copiedFromProjectId"` + Name string `json:"name"` + Description string `json:"description"` + Provider string `json:"provider"` + OrgId string `json:"orgId"` + Agents []project.AgentConfig `json:"agents"` + EnableWebhookAuth bool `json:"enableWebhookAuth"` + CopiedFromProjectId string `json:"copiedFromProjectId"` } type ProjectImportResponse struct { - ID string `json:"id"` - Agents []AgentConfig `json:"agents"` - APIKey string `json:"apiKey"` - ProjectKey string `json:"projectKey"` - IOAuthToken string `json:"ioAuthToken"` + ID string `json:"id"` + Agents []project.AgentConfig `json:"agents"` + APIKey string `json:"apiKey"` + ProjectKey string `json:"projectKey"` + IOAuthToken string `json:"ioAuthToken"` } -func (p *Project) Import(ctx context.Context, logger logger.Logger, baseUrl string, token string, orgId string, enableWebhookAuth bool) (*ProjectImportResponse, error) { +func ProjectImport(ctx context.Context, logger logger.Logger, baseUrl string, token string, orgId string, p *project.Project, enableWebhookAuth bool) (*ProjectImportResponse, error) { client := util.NewAPIClient(ctx, logger, baseUrl, token) var resp Response[ProjectImportResponse] @@ -538,7 +378,7 @@ func (c *DeploymentConfig) Write(logger logger.Logger, dir string) error { type ProjectContext struct { Logger logger.Logger - Project *Project + Project *project.Project Dir string APIURL string APPURL string @@ -550,7 +390,7 @@ type ProjectContext struct { func LoadProject(logger logger.Logger, dir string, apiUrl string, appUrl string, transportUrl, token string) ProjectContext { theproject := NewProject() if err := theproject.Load(dir); err != nil { - if err == ErrProjectMissingProjectId { + if err == project.ErrProjectMissingProjectId { return ProjectContext{ Logger: logger, Dir: dir, @@ -576,8 +416,10 @@ func LoadProject(logger logger.Logger, dir string, apiUrl string, appUrl string, } } +var isGitShaRE = regexp.MustCompile(`^[a-f0-9]{40}$`) + func isVersionCheckRequired(ver string) bool { - if ver != "" && ver != "dev" && !strings.Contains(ver, "-next") { + if ver != "" && ver != "dev" && !strings.Contains(ver, "-next") && !isGitShaRE.MatchString(ver) { return true } return false @@ -586,7 +428,10 @@ func isVersionCheckRequired(ver string) bool { func EnsureProject(ctx context.Context, cmd *cobra.Command) ProjectContext { logger := env.NewLogger(cmd) dir := ResolveProjectDir(logger, cmd, true) - apiUrl, appUrl, transportUrl := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + appUrl := urls.App + transportUrl := urls.Transport var token string // if the --api-key flag is used, we only need to verify the api key if cmd.Flags().Changed("api-key") { @@ -612,7 +457,10 @@ func EnsureProject(ctx context.Context, cmd *cobra.Command) ProjectContext { func TryProject(ctx context.Context, cmd *cobra.Command) ProjectContext { logger := env.NewLogger(cmd) dir := ResolveProjectDir(logger, cmd, false) - apiUrl, appUrl, transportUrl := util.GetURLs(logger) + urls := util.GetURLs(logger) + apiUrl := urls.API + appUrl := urls.App + transportUrl := urls.Transport var token string // if the --api-key flag is used, we only need to verify the api key if cmd.Flags().Changed("api-key") { @@ -623,7 +471,7 @@ func TryProject(ctx context.Context, cmd *cobra.Command) ProjectContext { token = apikey } } - if token == "" || dir == "" || !util.Exists(filepath.Join(dir, "agentuity.yaml")) { + if token == "" || dir == "" || !util.Exists(project.GetProjectFilename(dir)) { return ProjectContext{ Logger: logger, Dir: dir, @@ -669,9 +517,9 @@ func ResolveProjectDir(logger logger.Logger, cmd *cobra.Command, required bool) errsystem.New(errsystem.ErrEnvironmentVariablesNotSet, err, errsystem.WithUserMessage("Failed to get absolute path to %s: %s", dir, err)).ShowErrorAndExit() } - if !ProjectExists(abs) && required { + if !project.ProjectExists(abs) && required { dir = viper.GetString("preferences.project_dir") - if ProjectExists(dir) { + if project.ProjectExists(dir) { tui.ShowWarning("Using your last used project directory (%s). You should change into the correct directory or use the --dir flag.", dir) os.Chdir(dir) return dir @@ -684,7 +532,7 @@ func ResolveProjectDir(logger logger.Logger, cmd *cobra.Command, required bool) } os.Exit(1) } - if ProjectExists(abs) { + if project.ProjectExists(abs) { // if we are successful, set the project dir in the config viper.Set("preferences.project_dir", abs) viper.WriteConfig() @@ -722,13 +570,7 @@ func RemoveEnvValues(ctx context.Context, logger logger.Logger, dir string, keys } newenvs := make([]env.EnvLine, 0) for _, env := range envs { - var found bool - for _, k := range keys { - if env.Key == k { - found = true - break - } - } + found := slice.Contains(keys, env.Key) if !found { newenvs = append(newenvs, env) } diff --git a/internal/run/run.go b/internal/run/run.go new file mode 100644 index 00000000..07533aac --- /dev/null +++ b/internal/run/run.go @@ -0,0 +1,54 @@ +package run + +import ( + "context" + "fmt" + "os" + "os/exec" + + "github.com/agentuity/cli/internal/project" + "github.com/agentuity/cli/internal/util" + "github.com/agentuity/go-common/logger" +) + +type Config struct { + Context context.Context + Logger logger.Logger + Project project.ProjectContext + TelemetryURL string + TelemetryAPIKey string + APIURL string + TransportURL string + OrgId string + AgentPort int + WorkingDir string +} + +func CreateRunProjectCmd(config Config) (*exec.Cmd, error) { + // set the vars + projectServerCmd := exec.CommandContext(config.Context, config.Project.Project.Deployment.Command, config.Project.Project.Deployment.Args...) + projectServerCmd.Env = os.Environ()[:] + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_OTLP_URL=%s", config.TelemetryURL)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_OTLP_BEARER_TOKEN=%s", config.TelemetryAPIKey)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_URL=%s", config.APIURL)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_TRANSPORT_URL=%s", config.TransportURL)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_CLOUD_PROJECT_ID=%s", config.Project.Project.ProjectId)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_CLOUD_ORG_ID=%s", config.OrgId)) + + projectServerCmd.Env = append(projectServerCmd.Env, "AGENTUITY_ENV=production") + + if config.Project.Project.Bundler.Language == "javascript" { + projectServerCmd.Env = append(projectServerCmd.Env, "NODE_ENV=production") + } + + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("AGENTUITY_CLOUD_PORT=%d", config.AgentPort)) + projectServerCmd.Env = append(projectServerCmd.Env, fmt.Sprintf("PORT=%d", config.AgentPort)) + + projectServerCmd.Stdout = os.Stdout + projectServerCmd.Stderr = os.Stderr + projectServerCmd.Dir = config.WorkingDir + + util.ProcessSetup(projectServerCmd) + + return projectServerCmd, nil +} diff --git a/internal/templates/templates.go b/internal/templates/templates.go index 43c95527..74554386 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -14,8 +14,8 @@ import ( "time" "github.com/Masterminds/semver" - "github.com/agentuity/cli/internal/project" "github.com/agentuity/cli/internal/util" + cproject "github.com/agentuity/go-common/project" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) @@ -161,7 +161,7 @@ type Template struct { Requirements []Requirement `yaml:"requirements"` } -func (t *Template) NewProject(ctx TemplateContext) (*TemplateRules, []project.AgentConfig, error) { +func (t *Template) NewProject(ctx TemplateContext) (*TemplateRules, []cproject.AgentConfig, error) { rules, err := LoadTemplateRuleForIdentifier(ctx.TemplateDir, t.Identifier) if err != nil { return nil, nil, err @@ -178,9 +178,9 @@ func (t *Template) NewProject(ctx TemplateContext) (*TemplateRules, []project.Ag } // check to see if the project already exists from the template used and if so, // we are going to use the agents from the project template - existingProject := project.ProjectExists(ctx.ProjectDir) + existingProject := cproject.ProjectExists(ctx.ProjectDir) if existingProject { - var p project.Project + var p cproject.Project if err := p.Load(ctx.ProjectDir); err != nil { return nil, nil, err } @@ -223,7 +223,8 @@ func (t *Template) AddGitHubAction(ctx TemplateContext) error { if err := os.MkdirAll(outdir, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", outdir, err) } - outfile := filepath.Join(outdir, "agentuity.yaml") + + outfile := cproject.GetProjectFilename(outdir) if err := os.WriteFile(outfile, buf, 0644); err != nil { return fmt.Errorf("failed to write file %s: %w", outfile, err) } diff --git a/internal/util/api.go b/internal/util/api.go index 88b074a2..a8c2062e 100644 --- a/internal/util/api.go +++ b/internal/util/api.go @@ -259,10 +259,18 @@ func TransformUrl(urlString string) string { return urlString } -func GetURLs(logger logger.Logger) (string, string, string) { +type CLIUrls struct { + API string + App string + Transport string + Gravity string +} + +func GetURLs(logger logger.Logger) CLIUrls { appUrl := viper.GetString("overrides.app_url") apiUrl := viper.GetString("overrides.api_url") transportUrl := viper.GetString("overrides.transport_url") + gravityUrl := viper.GetString("overrides.gravity_url") if apiUrl == "https://api.agentuity.com" && appUrl != "https://app.agentuity.com" { logger.Debug("switching app url to production since the api url is production") appUrl = "https://app.agentuity.com" @@ -270,6 +278,9 @@ func GetURLs(logger logger.Logger) (string, string, string) { logger.Debug("switching app url to dev since the api url is dev") appUrl = "https://app.agentuity.io" } + if gravityUrl == "" { + gravityUrl = "grpc://gravity.agentuity.com" + } if apiUrl == "https://api.agentuity.com" && transportUrl != "https://agentuity.ai" { logger.Debug("switching transport url to production since the api url is production") transportUrl = "https://agentuity.ai" @@ -277,7 +288,16 @@ func GetURLs(logger logger.Logger) (string, string, string) { logger.Debug("switching transport url to dev since the api url is dev") transportUrl = "https://ai.agentuity.io" } - return TransformUrl(apiUrl), TransformUrl(appUrl), TransformUrl(transportUrl) + if apiUrl == "https://api.agentuity.io" { + logger.Debug("switching gravity url to dev since the api url is dev") + gravityUrl = "grpc://gravity.agentuity.io:8443" + } + return CLIUrls{ + API: TransformUrl(apiUrl), + App: TransformUrl(appUrl), + Transport: TransformUrl(transportUrl), + Gravity: TransformUrl(gravityUrl), + } } func run(ctx context.Context, c *cobra.Command, command string, args ...string) { diff --git a/internal/util/process_nix.go b/internal/util/process_nix.go index 1ee7acff..9c8c3e0e 100644 --- a/internal/util/process_nix.go +++ b/internal/util/process_nix.go @@ -8,6 +8,9 @@ import ( ) func ProcessKill(cmd *exec.Cmd) { + if cmd.Process == nil { + return + } syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }