From ed646ed3e2144c6d873e6e2cfcbb777ea537d295 Mon Sep 17 00:00:00 2001 From: Corey Feng Date: Tue, 9 Jun 2026 22:49:39 +0800 Subject: [PATCH 1/2] chore: remove legacy CLI entrypoint and GoReleaser workflow The shim + core binary architecture is now the primary distribution. Removes: - cmd/agent-sandbox/ (superseded by cmd/agent-sandbox-core/) - .goreleaser.yml (no more Go CLI releases) - .github/workflows/release.yml (GoReleaser workflow) --- .github/workflows/release.yml | 28 -- .goreleaser.yml | 31 -- cmd/agent-sandbox/audit.go | 369 ---------------------- cmd/agent-sandbox/generate.go | 137 -------- cmd/agent-sandbox/main.go | 385 ----------------------- cmd/agent-sandbox/schema_comment_test.go | 66 ---- 6 files changed, 1016 deletions(-) delete mode 100644 .github/workflows/release.yml delete mode 100644 .goreleaser.yml delete mode 100644 cmd/agent-sandbox/audit.go delete mode 100644 cmd/agent-sandbox/generate.go delete mode 100644 cmd/agent-sandbox/main.go delete mode 100644 cmd/agent-sandbox/schema_comment_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 5f4f2c7..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v6 - with: - go-version: "1.26" - - - uses: goreleaser/goreleaser-action@v6 - with: - version: latest - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index b293d6f..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: 2 - -builds: - - main: ./cmd/agent-sandbox - binary: agent-sandbox - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - ldflags: - - -s -w -X main.version={{.Version}} - -archives: - - formats: - - tar.gz - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" - -checksum: - name_template: "checksums.txt" - -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" - - "^chore:" diff --git a/cmd/agent-sandbox/audit.go b/cmd/agent-sandbox/audit.go deleted file mode 100644 index 81679e7..0000000 --- a/cmd/agent-sandbox/audit.go +++ /dev/null @@ -1,369 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/donbader/agent-sandbox/internal/config" - "github.com/spf13/cobra" -) - -// auditCheck represents a single audit check result. -type auditCheck struct { - Name string - Passed bool - Detail string -} - -func auditCmd(dir *string) *cobra.Command { - cmd := &cobra.Command{ - Use: "audit", - Short: "Validate a running sandbox meets the security contract", - Long: `Runs checks against a running sandbox to verify: - - Agent can reach external HTTPS endpoints through gateway - - Agent env does not contain real secrets - - Gateway injects auth headers into outbound requests - - DNS resolves through gateway - - Gateway CA certificate is trusted - - Traffic interception rules are active - - Default route goes through gateway - -The sandbox must be running (agent-sandbox compose up) before auditing.`, - RunE: func(cmd *cobra.Command, args []string) error { - return runAudit(*dir) - }, - } - - return cmd -} - -func runAudit(dir string) error { - absDir, err := filepath.Abs(dir) - if err != nil { - return fmt.Errorf("resolve project dir: %w", err) - } - projectName := filepath.Base(absDir) - - cfg, err := config.Load(dir) - if err != nil { - // Try fleet mode - fleet, ferr := config.LoadFleet(dir) - if ferr != nil { - return fmt.Errorf("cannot load agent.yaml or fleet.yaml: %w", err) - } - // Audit each agent in fleet - var allChecks []auditCheck - for _, agentDir := range fleet.Agents { - agentCfg, err := config.Load(filepath.Join(dir, agentDir)) - if err != nil { - return fmt.Errorf("loading agent %s: %w", agentDir, err) - } - checks := auditAgent(agentCfg, projectName, dir) - printAgentResults(agentCfg.Name, checks) - allChecks = append(allChecks, checks...) - } - return printSummary(allChecks) - } - - checks := auditAgent(cfg, projectName, dir) - printAgentResults(cfg.Name, checks) - return printSummary(checks) -} - -func auditAgent(cfg *config.Config, projectName, dir string) []auditCheck { - agentContainer := fmt.Sprintf("%s-%s-1", projectName, cfg.Name) - gatewayContainer := fmt.Sprintf("%s-%s-gateway-1", projectName, cfg.Name) - - var checks []auditCheck - - // Check containers are running - if !containerRunning(agentContainer) { - checks = append(checks, auditCheck{ - Name: "Agent container running", - Passed: false, - Detail: fmt.Sprintf("container %s is not running — run 'agent-sandbox compose up -d' first", agentContainer), - }) - return checks - } - if !containerRunning(gatewayContainer) { - checks = append(checks, auditCheck{ - Name: "Gateway container running", - Passed: false, - Detail: fmt.Sprintf("container %s is not running", gatewayContainer), - }) - return checks - } - - checks = append(checks, checkHTTPS(agentContainer)) - checks = append(checks, checkSecretIsolation(agentContainer, cfg, dir)) - checks = append(checks, checkDNS(agentContainer)) - checks = append(checks, checkCACert(agentContainer)) - checks = append(checks, checkDNATRules(agentContainer)) - checks = append(checks, checkDefaultRoute(agentContainer)) - - return checks -} - -func containerRunning(name string) bool { - rt := runtimeFromEnv() - out, err := exec.Command(rt, "inspect", "-f", "{{.State.Running}}", name).Output() - if err != nil { - return false - } - return strings.TrimSpace(string(out)) == "true" -} - -func containerExec(container string, args ...string) (string, error) { - rt := runtimeFromEnv() - cmdArgs := append([]string{"exec", container}, args...) - out, err := exec.Command(rt, cmdArgs...).CombinedOutput() - return string(out), err -} - -// runtimeFromEnv returns the container runtime binary from env or default. -func runtimeFromEnv() string { - if rt := os.Getenv("AGENT_SANDBOX_RUNTIME"); rt != "" { - return rt - } - return "docker" -} - -// checkHTTPS verifies the agent can reach an external HTTPS endpoint. -func checkHTTPS(container string) auditCheck { - // Use -o /dev/null -w %{http_code} to check connectivity regardless of HTTP status. - // Any HTTP response (even 4xx/5xx) proves the TLS proxy chain works. - out, err := containerExec(container, "curl", "-so", "/dev/null", "-w", "%{http_code}", "--max-time", "15", "https://httpbin.org/get") - if err != nil { - // Retry once — first request may be slow due to cold DNS + TLS - out, err = containerExec(container, "curl", "-so", "/dev/null", "-w", "%{http_code}", "--max-time", "15", "https://httpbin.org/get") - } - if err != nil { - return auditCheck{ - Name: "Agent can reach external HTTPS", - Passed: false, - Detail: "curl to httpbin.org failed or timed out", - } - } - code := strings.TrimSpace(out) - if code != "000" && code != "" { - return auditCheck{ - Name: "Agent can reach external HTTPS", - Passed: true, - Detail: fmt.Sprintf("reached https://httpbin.org through gateway (HTTP %s)", code), - } - } - return auditCheck{ - Name: "Agent can reach external HTTPS", - Passed: false, - Detail: "no HTTP response received", - } -} - -// checkSecretIsolation verifies the agent env doesn't contain real secrets from .env. -func checkSecretIsolation(container string, cfg *config.Config, dir string) auditCheck { - // Load secrets from .env file - envPath := filepath.Join(dir, ".env") - secrets, err := loadEnvSecrets(envPath) - if err != nil || len(secrets) == 0 { - // No .env file or empty — check passes (nothing to leak) - return auditCheck{ - Name: "Secret isolation", - Passed: true, - Detail: "no .env file found (nothing to verify)", - } - } - - // Get the agent container's environment - agentEnv, err := containerExec(container, "env") - if err != nil { - return auditCheck{ - Name: "Secret isolation", - Passed: false, - Detail: fmt.Sprintf("cannot read agent env: %v", err), - } - } - - // Check if any .env secret values appear in the agent's environment - leakedVars := []string{} - for name, value := range secrets { - if value == "" { - continue - } - // Check if the actual secret value appears in any env var inside the container - for _, line := range strings.Split(agentEnv, "\n") { - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 && parts[1] == value { - leakedVars = append(leakedVars, fmt.Sprintf("%s (from .env %s)", parts[0], name)) - } - } - } - - if len(leakedVars) > 0 { - return auditCheck{ - Name: "Secret isolation", - Passed: false, - Detail: fmt.Sprintf("agent env contains real secrets: %s", strings.Join(leakedVars, ", ")), - } - } - return auditCheck{ - Name: "Secret isolation", - Passed: true, - Detail: "no .env secrets leaked to agent environment", - } -} - -// loadEnvSecrets reads a .env file and returns key=value pairs. -func loadEnvSecrets(path string) (map[string]string, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - secrets := make(map[string]string) - for _, line := range strings.Split(string(data), "\n") { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - // Strip surrounding quotes - val = strings.Trim(val, `"'`) - secrets[key] = val - } - } - return secrets, nil -} - -// checkDNS verifies DNS resolves MITM domains to the gateway. -func checkDNS(container string) auditCheck { - // Verify a MITM domain resolves (proves the gateway DNS intercept works). - // Use getent which is available on slim images. - out, err := containerExec(container, "getent", "hosts", "agent-gateway.stx-ai.net") - if err != nil { - // Fallback: try ping-based resolution - out, err = containerExec(container, "sh", "-c", "ping -c1 -W2 agent-gateway.stx-ai.net 2>/dev/null | head -1") - } - if err != nil || strings.TrimSpace(out) == "" { - return auditCheck{ - Name: "DNS through gateway", - Passed: false, - Detail: "cannot resolve MITM domain (agent-gateway.stx-ai.net)", - } - } - return auditCheck{ - Name: "DNS through gateway", - Passed: true, - Detail: fmt.Sprintf("MITM domain resolves: %s", strings.TrimSpace(strings.Split(out, "\n")[0])), - } -} - -// checkCACert verifies the gateway CA is installed in the agent. -func checkCACert(container string) auditCheck { - _, err := containerExec(container, "test", "-f", "/usr/local/share/ca-certificates/gateway-ca.crt") - if err != nil { - return auditCheck{ - Name: "Gateway CA trusted", - Passed: false, - Detail: "CA certificate not found at /usr/local/share/ca-certificates/gateway-ca.crt", - } - } - return auditCheck{ - Name: "Gateway CA trusted", - Passed: true, - Detail: "gateway CA certificate present in trust store", - } -} - -// checkDNATRules verifies OUTPUT DNAT rules are active in the agent. -func checkDNATRules(container string) auditCheck { - out, err := containerExec(container, "iptables", "-t", "nat", "-L", "OUTPUT", "-n") - if err != nil { - return auditCheck{ - Name: "Traffic interception rules", - Passed: false, - Detail: fmt.Sprintf("cannot read iptables: %v", err), - } - } - if strings.Contains(out, "DNAT") && strings.Contains(out, "tcp dpt:443") { - return auditCheck{ - Name: "Traffic interception rules", - Passed: true, - Detail: "OUTPUT DNAT rule for port 443 active", - } - } - return auditCheck{ - Name: "Traffic interception rules", - Passed: false, - Detail: "missing OUTPUT DNAT rule for port 443", - } -} - -// checkDefaultRoute verifies traffic reaches the gateway by confirming the DNAT -// target IP matches an active gateway container on the network. -func checkDefaultRoute(container string) auditCheck { - // Read the iptables DNAT target to find where traffic goes - out, err := containerExec(container, "iptables", "-t", "nat", "-L", "OUTPUT", "-n") - if err != nil { - return auditCheck{ - Name: "Default route to gateway", - Passed: false, - Detail: fmt.Sprintf("cannot read iptables: %v", err), - } - } - - // Look for "to::8443" in the DNAT rule - for _, line := range strings.Split(out, "\n") { - if strings.Contains(line, "DNAT") && strings.Contains(line, ":8443") { - // Extract the target IP - for _, field := range strings.Fields(line) { - if strings.HasPrefix(field, "to:") { - target := strings.TrimPrefix(field, "to:") - ip := strings.Split(target, ":")[0] - return auditCheck{ - Name: "Default route to gateway", - Passed: true, - Detail: fmt.Sprintf("HTTPS traffic routed to gateway at %s", ip), - } - } - } - } - } - - return auditCheck{ - Name: "Default route to gateway", - Passed: false, - Detail: "no DNAT target found in iptables OUTPUT chain", - } -} - -func printAgentResults(name string, checks []auditCheck) { - fmt.Printf("Auditing %s...\n\n", name) - for _, c := range checks { - if c.Passed { - fmt.Printf(" \033[32m✓\033[0m %s\n", c.Name) - } else { - fmt.Printf(" \033[31m✗\033[0m %s\n", c.Name) - fmt.Printf(" %s\n", c.Detail) - } - } - fmt.Println() -} - -func printSummary(checks []auditCheck) error { - passed := 0 - for _, c := range checks { - if c.Passed { - passed++ - } - } - fmt.Printf("%d/%d checks passed\n", passed, len(checks)) - if passed < len(checks) { - os.Exit(1) - } - return nil -} diff --git a/cmd/agent-sandbox/generate.go b/cmd/agent-sandbox/generate.go deleted file mode 100644 index 766b580..0000000 --- a/cmd/agent-sandbox/generate.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/donbader/agent-sandbox/internal/config" - "github.com/donbader/agent-sandbox/internal/release" - "github.com/donbader/agent-sandbox/internal/dotenv" - v1 "github.com/donbader/agent-sandbox/internal/generate/v1" - "github.com/spf13/cobra" -) - -func generateV1Cmd(dir *string) *cobra.Command { - var coreFlag string - - cmd := &cobra.Command{ - Use: "generate", - Short: "Generate build artifacts from agent.yaml or fleet.yaml", - RunE: func(cmd *cobra.Command, args []string) error { - projectDir, err := filepath.Abs(*dir) - if err != nil { - return fmt.Errorf("resolve dir: %w", err) - } - - // If --core is set, resolve to absolute path and use directly. - if coreFlag != "" { - abs, err := filepath.Abs(coreFlag) - if err != nil { - return fmt.Errorf("resolve --core path: %w", err) - } - if _, err := os.Stat(abs); err != nil { - return fmt.Errorf("--core path does not exist: %s", abs) - } - coreOverride = abs - fmt.Fprintf(os.Stderr, "Using local core: %s\n", abs) - } - - // Load .env file so secrets are available for auth-header baking. - dotenv.Load(filepath.Join(projectDir, ".env")) - - // Try single-agent first, then fleet mode - cfg, loadErr := config.Load(projectDir) - if loadErr == nil { - return generateSingleAgent(cfg, projectDir) - } - - // Try fleet mode - _, agents, fleetErr := config.LoadFleetAgents(projectDir) - if fleetErr != nil { - return fmt.Errorf("cannot load agent.yaml or fleet.yaml:\n agent: %w\n fleet: %v", loadErr, fleetErr) - } - - return generateFleet(agents, projectDir) - }, - } - - cmd.Flags().StringVar(&coreFlag, "core", "", "Path to local core directory (skips GitHub release fetch)") - return cmd -} - -// coreOverride is set when --core flag is provided, bypassing release fetch. -var coreOverride string - -func generateSingleAgent(cfg *config.Config, projectDir string) error { - coreDir, err := fetchCore(cfg.CoreVersion) - if err != nil { - return err - } - - g := v1.NewGeneratorWithCore(projectDir, coreDir) - if err := g.RunWithConfig(cfg, projectDir); err != nil { - return err - } - - _ = ensureSchemaComment(filepath.Join(projectDir, "agent.yaml"), ".build/schema.json") - fmt.Fprintf(os.Stderr, "Generated .build/ in %s\n", projectDir) - return nil -} - -func generateFleet(agents []config.FleetAgent, projectDir string) error { - // Fleet uses first agent's core_version (or "latest" if not set) - version := "latest" - if len(agents) > 0 && agents[0].Config.CoreVersion != "" { - version = agents[0].Config.CoreVersion - } - - coreDir, err := fetchCore(version) - if err != nil { - return err - } - - g := v1.NewGeneratorWithCore(projectDir, coreDir) - if err := g.RunFleet(agents); err != nil { - return err - } - - _ = ensureSchemaComment(filepath.Join(projectDir, "fleet.yaml"), ".build/fleet-schema.json") - for _, agent := range agents { - agentYAML := filepath.Join(agent.Dir, "agent.yaml") - relSchema, err := filepath.Rel(agent.Dir, filepath.Join(projectDir, ".build", "schema.json")) - if err != nil { - relSchema = ".build/schema.json" - } - _ = ensureSchemaComment(agentYAML, relSchema) - } - - fmt.Fprintf(os.Stderr, "Generated .build/ for %d agents in %s\n", len(agents), projectDir) - return nil -} - -// fetchCore resolves a core version and returns the cache directory. -// If --core was provided, returns that path directly. -// "latest" queries GitHub for the newest core-v* release. -// Any other value fetches that specific version. -func fetchCore(version string) (string, error) { - if coreOverride != "" { - return coreOverride, nil - } - - if version == "" || version == "latest" { - dir, err := release.FetchLatest() - if err != nil { - return "", fmt.Errorf("fetch latest core: %w", err) - } - fmt.Fprintf(os.Stderr, "Using latest core from %s\n", dir) - return dir, nil - } - - dir, err := release.Fetch(version) - if err != nil { - return "", fmt.Errorf("fetch core %s: %w", version, err) - } - fmt.Fprintf(os.Stderr, "Using core %s from %s\n", version, dir) - return dir, nil -} diff --git a/cmd/agent-sandbox/main.go b/cmd/agent-sandbox/main.go deleted file mode 100644 index 66d95e3..0000000 --- a/cmd/agent-sandbox/main.go +++ /dev/null @@ -1,385 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/donbader/agent-sandbox/internal/config" - "github.com/spf13/cobra" -) - -var version = "dev" - -func main() { - var dir string - - root := &cobra.Command{ - Use: "agent-sandbox", - Short: "Opinionated agent sandbox orchestrator", - Version: version, - TraverseChildren: true, - } - - root.PersistentFlags().StringVarP(&dir, "dir", "C", ".", "Project directory containing agent.yaml") - - root.AddCommand(generateV1Cmd(&dir)) - root.AddCommand(composeCmd(&dir)) - root.AddCommand(auditCmd(&dir)) - root.AddCommand(initCmd()) - root.AddCommand(upgradeCmd()) - root.AddCommand(gatewayURLCmd(&dir)) - - if err := root.Execute(); err != nil { - os.Exit(1) - } -} - -// ensureSchemaComment ensures the yaml-language-server schema comment is correct -// in the given YAML file. Inserts or replaces the first line if needed. -func ensureSchemaComment(yamlPath string, schemaRelPath string) error { - data, err := os.ReadFile(yamlPath) - if err != nil { - return err - } - - expected := fmt.Sprintf("# yaml-language-server: $schema=%s", schemaRelPath) - lines := strings.SplitAfter(string(data), "\n") - - if len(lines) > 0 && strings.TrimSpace(lines[0]) == expected { - return nil // already correct - } - - // Check if first line is an existing schema comment that needs replacing - if len(lines) > 0 && strings.HasPrefix(strings.TrimSpace(lines[0]), "# yaml-language-server: $schema=") { - lines[0] = expected + "\n" - } else { - lines = append([]string{expected + "\n"}, lines...) - } - - return os.WriteFile(yamlPath, []byte(strings.Join(lines, "")), 0644) -} - -func composeCmd(dir *string) *cobra.Command { - cmd := &cobra.Command{ - Use: "compose", - Short: "Compose passthrough (auto-injects -f .build/docker-compose.yml)", - DisableFlagParsing: true, - RunE: func(cmd *cobra.Command, args []string) error { - composePath := filepath.Join(*dir, ".build", "docker-compose.yml") - if _, err := os.Stat(composePath); os.IsNotExist(err) { - return fmt.Errorf("%s not found — run 'agent-sandbox generate' first", composePath) - } - - // Use the project folder name as the compose project name. - absDir, err := filepath.Abs(*dir) - if err != nil { - return fmt.Errorf("resolve project dir: %w", err) - } - projectName := filepath.Base(absDir) - - composeArgs := []string{"-f", composePath, "--project-name", projectName} - // Auto-inject --env-file if .env exists in project dir - envPath := filepath.Join(*dir, ".env") - if _, err := os.Stat(envPath); err == nil { - composeArgs = append(composeArgs, "--env-file", envPath) - } - composeArgs = append(composeArgs, args...) - - runtime := runtimeBinary(*dir) - c := exec.Command(runtime, append([]string{"compose"}, composeArgs...)...) - c.Stdin = os.Stdin - c.Stdout = os.Stdout - c.Stderr = os.Stderr - - return c.Run() - }, - } - - return cmd -} - -func gatewayURLCmd(dir *string) *cobra.Command { - cmd := &cobra.Command{ - Use: "gateway-url", - Short: "Print the gateway's public URL (resolves dynamic port)", - RunE: func(cmd *cobra.Command, args []string) error { - composePath := filepath.Join(*dir, ".build", "docker-compose.yml") - if _, err := os.Stat(composePath); os.IsNotExist(err) { - return fmt.Errorf("%s not found — run 'agent-sandbox generate' first", composePath) - } - - absDir, err := filepath.Abs(*dir) - if err != nil { - return fmt.Errorf("resolve project dir: %w", err) - } - projectName := filepath.Base(absDir) - - // Load config to get the agent name (gateway service = -gateway) - cfg, err := config.Load(*dir) - if err != nil { - return fmt.Errorf("load config: %w", err) - } - - gatewayService := cfg.Name + "-gateway" - - runtime := runtimeBinary(*dir) - c := exec.Command(runtime, "compose", - "-f", composePath, - "--project-name", projectName, - "port", gatewayService, "8080", - ) - out, err := c.Output() - if err != nil { - return fmt.Errorf("gateway not running or port not exposed — is 'agent-sandbox compose up' running?") - } - - hostPort := strings.TrimSpace(string(out)) - if hostPort == "" { - return fmt.Errorf("could not resolve gateway port") - } - - // docker compose port returns "0.0.0.0:PORT" or ":::PORT" - // Normalize to localhost - hostPort = strings.Replace(hostPort, "0.0.0.0:", "localhost:", 1) - if strings.HasPrefix(hostPort, ":::") { - hostPort = "localhost:" + strings.TrimPrefix(hostPort, ":::") - } - - fmt.Printf("http://%s\n", hostPort) - return nil - }, - } - return cmd -} - -// runtimeBinary determines the container runtime CLI to use. -// Priority: AGENT_SANDBOX_RUNTIME env var > agent.yaml runtime_engine > "docker" -func runtimeBinary(dir string) string { - if rt := os.Getenv("AGENT_SANDBOX_RUNTIME"); rt != "" { - return rt - } - // Try to load config for runtime_engine setting - cfg, err := loadConfigSafe(dir) - if err == nil && cfg.RuntimeEngine != "" { - return cfg.RuntimeEngineBinary() - } - return "docker" -} - -// loadConfigSafe attempts to load config without failing fatally. -func loadConfigSafe(dir string) (*config.Config, error) { - return config.Load(dir) -} - -func initCmd() *cobra.Command { - return &cobra.Command{ - Use: "init", - Short: "Initialize a new agent-sandbox project (interactive)", - RunE: func(cmd *cobra.Command, args []string) error { - // Check if agent.yaml or fleet.yaml already exists - if _, err := os.Stat("agent.yaml"); err == nil { - return fmt.Errorf("agent.yaml already exists in this directory") - } - if _, err := os.Stat("fleet.yaml"); err == nil { - return fmt.Errorf("fleet.yaml already exists in this directory") - } - - reader := bufio.NewReader(os.Stdin) - - // Fleet mode selection - agentCountStr := prompt(reader, "How many agents? [1]: ") - agentCount := 1 - if agentCountStr != "" { - if _, err := fmt.Sscanf(agentCountStr, "%d", &agentCount); err != nil || agentCount < 1 { - return fmt.Errorf("invalid agent count: %q (must be a positive integer)", agentCountStr) - } - } - - if agentCount == 1 { - return initSingleAgent(reader) - } - return initFleet(reader, agentCount) - }, - } -} - -func initSingleAgent(reader *bufio.Reader) error { - dirName := filepath.Base(mustCwd()) - name := prompt(reader, fmt.Sprintf("Agent name [%s]: ", dirName)) - if name == "" { - name = dirName - } - - rt := selectRuntime(reader) - - var b strings.Builder - b.WriteString("# yaml-language-server: $schema=.build/schema.json\n") - _, _ = fmt.Fprintf(&b, "name: %s\n", name) - b.WriteString("core_version: latest\n") - b.WriteString("runtime:\n") - _, _ = fmt.Fprintf(&b, " image: \"@builtin/%s\"\n", rt) - b.WriteString(" entrypoint: [\"sleep\", \"infinity\"]\n") - b.WriteString("gateway:\n") - b.WriteString(" services: []\n") - b.WriteString("installations: []\n") - - if err := os.WriteFile("agent.yaml", []byte(b.String()), 0644); err != nil { - return fmt.Errorf("writing agent.yaml: %w", err) - } - - fmt.Println("\nCreated agent.yaml") - fmt.Println("\nNext steps:") - fmt.Println(" 1. Add gateway services and plugins to agent.yaml") - fmt.Println(" 2. Create .env with your secrets") - fmt.Println(" 3. agent-sandbox generate") - fmt.Println(" 4. agent-sandbox compose up --build -d") - return nil -} - -func initFleet(reader *bufio.Reader, count int) error { - rt := selectRuntime(reader) - - // Generate fleet.yaml - var fleet strings.Builder - fleet.WriteString("# yaml-language-server: $schema=.build/fleet-schema.json\n") - fleet.WriteString("agents:\n") - for i := 1; i <= count; i++ { - _, _ = fmt.Fprintf(&fleet, " - agent-%03d\n", i) - } - fleet.WriteString("\nshared:\n") - fleet.WriteString(" gateway:\n") - fleet.WriteString(" services: []\n") - fleet.WriteString(" installations: []\n") - - if err := os.WriteFile("fleet.yaml", []byte(fleet.String()), 0644); err != nil { - return fmt.Errorf("writing fleet.yaml: %w", err) - } - - // Generate per-agent directories - for i := 1; i <= count; i++ { - agentName := fmt.Sprintf("agent-%03d", i) - if err := os.MkdirAll(agentName, 0755); err != nil { - return fmt.Errorf("creating %s/: %w", agentName, err) - } - - var agent strings.Builder - agent.WriteString("# yaml-language-server: $schema=../.build/schema.json\n") - _, _ = fmt.Fprintf(&agent, "name: %s\n", agentName) - agent.WriteString("core_version: latest\n") - agent.WriteString("runtime:\n") - _, _ = fmt.Fprintf(&agent, " image: \"@builtin/%s\"\n", rt) - agent.WriteString("installations: []\n") - - agentPath := filepath.Join(agentName, "agent.yaml") - if err := os.WriteFile(agentPath, []byte(agent.String()), 0644); err != nil { - return fmt.Errorf("writing %s: %w", agentPath, err) - } - } - - // Generate .env.example - if err := os.WriteFile(".env.example", []byte("# Shared secrets\n"), 0644); err != nil { - return fmt.Errorf("writing .env.example: %w", err) - } - - fmt.Printf("\nCreated fleet.yaml with %d agents\n", count) - for i := 1; i <= count; i++ { - fmt.Printf(" agent-%03d/agent.yaml\n", i) - } - fmt.Println("\nNext steps:") - fmt.Println(" 1. Add shared gateway services and plugins to fleet.yaml") - fmt.Println(" 2. Customize per-agent config in each agent-NNN/agent.yaml") - fmt.Println(" 3. Create .env with your secrets") - fmt.Println(" 4. agent-sandbox generate") - fmt.Println(" 5. agent-sandbox compose up --build -d") - return nil -} - -func selectRuntime(reader *bufio.Reader) string { - fmt.Println("\nAvailable runtimes:") - fmt.Println(" 1) codex — OpenAI Codex") - fmt.Println(" 2) claude-code — Anthropic Claude Code") - fmt.Println(" 3) pi — Pi coding agent") - choice := prompt(reader, "Runtime [1]: ") - switch strings.TrimSpace(choice) { - case "2": - return "claude-code" - case "3": - return "pi" - default: - return "codex" - } -} - -func prompt(reader *bufio.Reader, message string) string { - fmt.Print(message) - line, _ := reader.ReadString('\n') - return strings.TrimSpace(line) -} - -func mustCwd() string { - dir, err := os.Getwd() - if err != nil { - return "agent" - } - return dir -} - -const upgradeRepo = "donbader/agent-sandbox" - -func upgradeCmd() *cobra.Command { - return &cobra.Command{ - Use: "upgrade", - Short: "Migrate to the shim-based CLI", - RunE: func(cmd *cobra.Command, args []string) error { - // Determine sandbox home directory - sandboxHome := os.Getenv("AGENT_SANDBOX_HOME") - if sandboxHome == "" { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("determining home directory: %w", err) - } - sandboxHome = filepath.Join(home, ".agent-sandbox") - } - - // Check if already migrated - shimPath := filepath.Join(sandboxHome, "bin", "agent-sandbox") - if _, err := os.Stat(shimPath); err == nil { - fmt.Println("Already migrated.") - return nil - } - - // Download and run the install script - installURL := fmt.Sprintf("https://raw.githubusercontent.com/%s/main/scripts/install.sh", upgradeRepo) - fmt.Println("Installing shim-based CLI...") - installCmd := exec.Command("sh", "-c", fmt.Sprintf("curl -fsSL '%s' | sh", installURL)) - installCmd.Stdin = os.Stdin - installCmd.Stdout = os.Stdout - installCmd.Stderr = os.Stderr - if err := installCmd.Run(); err != nil { - return fmt.Errorf("install script failed: %w", err) - } - - // Resolve current binary path - execPath, err := os.Executable() - if err != nil { - return fmt.Errorf("finding current binary: %w", err) - } - execPath, err = filepath.EvalSymlinks(execPath) - if err != nil { - return fmt.Errorf("resolving binary path: %w", err) - } - - // Print migration instructions - fmt.Println("\nMigration complete.") - fmt.Println("Add ~/.agent-sandbox/bin to your PATH (before your current binary location).") - fmt.Println("Then remove the old agent-sandbox binary:") - fmt.Printf(" rm %s\n", execPath) - - return nil - }, - } -} diff --git a/cmd/agent-sandbox/schema_comment_test.go b/cmd/agent-sandbox/schema_comment_test.go deleted file mode 100644 index 57314b7..0000000 --- a/cmd/agent-sandbox/schema_comment_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEnsureSchemaComment(t *testing.T) { - t.Run("inserts comment when missing", func(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "agent.yaml") - require.NoError(t, os.WriteFile(path, []byte("name: coder\nruntime: codex\n"), 0644)) - - err := ensureSchemaComment(path, ".build/schema.json") - require.NoError(t, err) - - data, _ := os.ReadFile(path) - assert.Equal(t, "# yaml-language-server: $schema=.build/schema.json\nname: coder\nruntime: codex\n", string(data)) - }) - - t.Run("replaces wrong schema path", func(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "fleet.yaml") - require.NoError(t, os.WriteFile(path, []byte("# yaml-language-server: $schema=.build/schema.json\nagents:\n - coder\n"), 0644)) - - err := ensureSchemaComment(path, ".build/fleet-schema.json") - require.NoError(t, err) - - data, _ := os.ReadFile(path) - assert.Equal(t, "# yaml-language-server: $schema=.build/fleet-schema.json\nagents:\n - coder\n", string(data)) - }) - - t.Run("no-op when comment already correct", func(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "agent.yaml") - content := "# yaml-language-server: $schema=.build/schema.json\nname: coder\n" - require.NoError(t, os.WriteFile(path, []byte(content), 0644)) - - err := ensureSchemaComment(path, ".build/schema.json") - require.NoError(t, err) - - data, _ := os.ReadFile(path) - assert.Equal(t, content, string(data)) - }) - - t.Run("handles file with other comments at top", func(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "agent.yaml") - require.NoError(t, os.WriteFile(path, []byte("# My agent config\nname: coder\n"), 0644)) - - err := ensureSchemaComment(path, ".build/schema.json") - require.NoError(t, err) - - data, _ := os.ReadFile(path) - assert.Equal(t, "# yaml-language-server: $schema=.build/schema.json\n# My agent config\nname: coder\n", string(data)) - }) - - t.Run("returns error for non-existent file", func(t *testing.T) { - err := ensureSchemaComment("/nonexistent/path.yaml", ".build/schema.json") - assert.Error(t, err) - }) -} From 8914ee118f546087f5bf9d9317516121b2970858 Mon Sep 17 00:00:00 2001 From: Corey Feng Date: Tue, 9 Jun 2026 22:53:12 +0800 Subject: [PATCH 2/2] ci: remove references to legacy CLI binary --- .github/workflows/ci.yml | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11291f3..dff1fa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,9 +89,6 @@ jobs: - name: Test run: go test -race ./... - - name: Build CLI - run: go build -o agent-sandbox ./cmd/agent-sandbox/ - - name: Build core binary run: go build -o agent-sandbox-core ./cmd/agent-sandbox-core/ @@ -103,13 +100,6 @@ jobs: echo "shellcheck not available, skipping" fi - - name: Upload CLI binary - uses: actions/upload-artifact@v4 - with: - name: agent-sandbox-linux - path: agent-sandbox - retention-days: 1 - - name: Upload core binary uses: actions/upload-artifact@v4 with: @@ -138,23 +128,15 @@ jobs: run: | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o core/gateway/bin/gateway-linux-amd64 ./core/gateway/cmd/gateway/ - - name: Download CLI binary - uses: actions/download-artifact@v4 - with: - name: agent-sandbox-linux - - name: Download core binary uses: actions/download-artifact@v4 with: name: agent-sandbox-core-linux - name: Make binaries executable - run: chmod +x ./agent-sandbox ./agent-sandbox-core + run: chmod +x ./agent-sandbox-core - name: Generate build artifacts - run: ./agent-sandbox generate --core=./core -C examples/${{ matrix.example }} - - - name: Generate build artifacts (core binary) run: ./agent-sandbox-core generate --core=./core -C examples/${{ matrix.example }} - name: Start containers (smoke test) @@ -162,7 +144,7 @@ jobs: STX_LLM_GATEWAY_API_KEY: test-stub TELEGRAM_BOT_TOKEN: test-stub run: | - ./agent-sandbox -C examples/${{ matrix.example }} compose up -d --build || true + ./agent-sandbox-core -C examples/${{ matrix.example }} compose up -d --build || true # Wait for gateway(s) to be healthy — proves mounts, config, and binary work. # Agent and sidecars with stub credentials may crash; that's expected in CI. for i in $(seq 1 30); do @@ -173,19 +155,19 @@ jobs: sleep 2 done echo "ERROR: No healthy gateway after 60s" - ./agent-sandbox -C examples/${{ matrix.example }} compose ps - ./agent-sandbox -C examples/${{ matrix.example }} compose logs + ./agent-sandbox-core -C examples/${{ matrix.example }} compose ps + ./agent-sandbox-core -C examples/${{ matrix.example }} compose logs exit 1 - name: Wait for agent entrypoint run: sleep 5 - name: Audit security contract - run: ./agent-sandbox -C examples/${{ matrix.example }} audit || true + run: ./agent-sandbox-core -C examples/${{ matrix.example }} audit || true - name: Teardown if: always() - run: ./agent-sandbox -C examples/${{ matrix.example }} compose down -v + run: ./agent-sandbox-core -C examples/${{ matrix.example }} compose down -v sandbox: name: Sandbox (integration) @@ -207,16 +189,16 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Download CLI binary + - name: Download core binary uses: actions/download-artifact@v4 with: - name: agent-sandbox-linux + name: agent-sandbox-core-linux - name: Make CLI executable - run: chmod +x ./agent-sandbox + run: chmod +x ./agent-sandbox-core - name: Run sandbox integration tests env: - CLI_PATH: ${{ github.workspace }}/agent-sandbox + CLI_PATH: ${{ github.workspace }}/agent-sandbox-core CORE_PATH: ${{ github.workspace }}/core run: tests/integration/sandbox/run.sh