From 4a4e7683655e8c2acfe802e5e8788136b29178fb Mon Sep 17 00:00:00 2001 From: yuanhe Date: Sat, 11 Jul 2026 16:57:15 +0800 Subject: [PATCH 1/2] Rename e2b sandbox image: parsar-daemon-claudecode -> parsar-sandbox-e2b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parsar-daemon-claudecode was a legacy name carried over from the old infra/e2b-templates/parsar-daemon-claudecode/ directory. Now that the sandbox images are just "local" and "e2b" variants of one Dockerfile, parsar-sandbox-e2b matches parsar-sandbox:local's naming instead of an unrelated directory-derived name. Renamed everywhere: the GHCR image name in .github/workflows/sandbox-image-release.yml, the Dockerfile's own build/usage comments, and every reference in server Go code (comments, E2BProviderConfig.Template examples, and TemplateID test fixtures in sandbox_provider_test.go / sandbox_admin_test.go). All are placeholder/ example strings, not hardcoded config gating real behavior (the actual e2b template id an operator uses comes from AGENT_DAEMON_SANDBOX_TEMPLATE at runtime) — safe as a pure rename. gofmt -w on sandbox_provider.go / sandbox_admin_test.go: the shorter string shifted struct-literal field alignment: to reformat (also fixed a pre-existing import-order issue in sandbox_admin_test.go as a side effect of running gofmt, unrelated to this rename). Verified: go build + go vet + go test on both touched packages pass; repo-wide grep confirms no leftover "parsar-daemon-claudecode". Co-Authored-By: Claude Sonnet 5 --- .github/workflows/sandbox-image-release.yml | 6 +- infra/sandbox/Dockerfile | 4 +- .../connector/agentdaemon/sandbox_provider.go | 54 +++++++++--------- .../agentdaemon/sandbox_provider_test.go | 32 +++++------ .../connector/agentdaemon/sandbox_seed.go | 2 +- server/internal/dev/sandbox_admin_test.go | 56 +++++++++---------- 6 files changed, 77 insertions(+), 77 deletions(-) diff --git a/.github/workflows/sandbox-image-release.yml b/.github/workflows/sandbox-image-release.yml index 8770c65..9aa2b5b 100644 --- a/.github/workflows/sandbox-image-release.yml +++ b/.github/workflows/sandbox-image-release.yml @@ -5,7 +5,7 @@ # - parsar-sandbox local docker-backed sandbox provider # (AGENT_DAEMON_SANDBOX_BACKEND=docker), # multi-arch (amd64 + arm64) -# - parsar-daemon-claudecode e2b.app SaaS template, amd64 only +# - parsar-sandbox-e2b e2b.app SaaS template, amd64 only # # Both compile parsar-daemon + the parsar CLI from source in the shared # Dockerfile's builder stage — neither depends on a published @@ -20,7 +20,7 @@ # # Output: # ghcr.io//parsar-sandbox: -# ghcr.io//parsar-daemon-claudecode: +# ghcr.io//parsar-sandbox-e2b: # - latest on default-branch pushes # - on every build # - on tag pushes (e.g. sandbox-v0.1.0) @@ -74,7 +74,7 @@ jobs: platforms_pr: linux/amd64 platforms_publish: linux/amd64,linux/arm64 - variant: e2b - image_name: parsar-daemon-claudecode + image_name: parsar-sandbox-e2b base_image: e2bdev/base:latest # e2b.app sandboxes are amd64 only — no point publishing arm64. platforms_pr: linux/amd64 diff --git a/infra/sandbox/Dockerfile b/infra/sandbox/Dockerfile index 44142cf..f0aec77 100644 --- a/infra/sandbox/Dockerfile +++ b/infra/sandbox/Dockerfile @@ -10,9 +10,9 @@ # shell. Multi-arch: add --platform linux/arm64 to cross-build. # # docker build -f infra/sandbox/Dockerfile \ -# --build-arg BASE_IMAGE=e2bdev/base:latest -t parsar-daemon-claudecode . +# --build-arg BASE_IMAGE=e2bdev/base:latest -t parsar-sandbox-e2b . # e2b.app SaaS template. amd64 only. After build, the SandboxProvider -# only needs to: Sandbox.Create("parsar-daemon-claudecode", {timeout}) +# only needs to: Sandbox.Create("parsar-sandbox-e2b", {timeout}) # → RunCommand: parsar-daemon connect --device-name -b # (URL/token via env) → wait for the gateway to see deviceId dial in. # diff --git a/server/internal/connector/agentdaemon/sandbox_provider.go b/server/internal/connector/agentdaemon/sandbox_provider.go index 9d09ea5..b1964a0 100644 --- a/server/internal/connector/agentdaemon/sandbox_provider.go +++ b/server/internal/connector/agentdaemon/sandbox_provider.go @@ -17,7 +17,7 @@ // first prompt -> Connector hits ErrNotBound -> sees daemon_mode=sandbox // -> SandboxProvider.Acquire(ctx, in): // 1. CreateRuntimePairing(type=agent_daemon) -> token + runtimeID(=deviceID) -// 2. e2b.Create("parsar-daemon-claudecode") -> sandbox handle +// 2. e2b.Create("parsar-sandbox-e2b") -> sandbox handle // 3. RunCommand(parsar-daemon connect -b + env token) // -> daemon pairs, then dials WS in background // 4. Registry.WaitForDevice(deviceID, 45s) -> blocks until WS upgrade lands @@ -197,7 +197,7 @@ type RuntimeMinter interface { // - Store: runtime pairing minter. // - Registry: the gateway registry to WaitForDevice against. // - Binder: persisted conversation->device bindings. -// - Template: the e2b template id (e.g. "parsar-daemon-claudecode"). The +// - Template: the e2b template id (e.g. "parsar-sandbox-e2b"). The // deployment must publish this template before sandbox mode works. // - ServerURL: the public URL the daemon inside the sandbox dials // back to. Must be reachable from inside the sandbox network. @@ -215,12 +215,12 @@ type DeviceOwnerChecker interface { } type E2BProviderConfig struct { - Client E2BClient - Store RuntimeMinter - Registry *gateway.Registry - Binder binding.Binder - Bindings SandboxBindingPersister // nil = memory-only (local dev) - Template string + Client E2BClient + Store RuntimeMinter + Registry *gateway.Registry + Binder binding.Binder + Bindings SandboxBindingPersister // nil = memory-only (local dev) + Template string // Templates maps a sandbox_size label (e.g. "standard", "xl") to the // e2b template id for that size. The agent's agents.config // `sandbox_size` field selects which template gets used on cold start. @@ -300,7 +300,7 @@ func NewE2BSandboxProvider(cfg E2BProviderConfig) (*E2BSandboxProvider, error) { return nil, errors.New("E2BSandboxProvider: Binder required") } if cfg.Template == "" { - return nil, errors.New("E2BSandboxProvider: Template required (e.g. parsar-daemon-claudecode)") + return nil, errors.New("E2BSandboxProvider: Template required (e.g. parsar-sandbox-e2b)") } if cfg.ServerURL == "" { return nil, errors.New("E2BSandboxProvider: ServerURL required (the URL the daemon inside the sandbox should dial back to)") @@ -509,10 +509,10 @@ func (p *E2BSandboxProvider) acquireCrossPod(ctx context.Context, in connector.P cacheKey := "agent_daemon:" + in.AgentID _, templateID := p.resolveTemplate(in) row, won, err := p.cfg.Bindings.ReserveSandboxBindingSlot(ctx, store.ReserveSandboxBindingSlotInput{ - WorkspaceID: in.WorkspaceID, - AgentID: in.AgentID, - CacheKey: cacheKey, - TemplateID: templateID, + WorkspaceID: in.WorkspaceID, + AgentID: in.AgentID, + CacheKey: cacheKey, + TemplateID: templateID, Metadata: map[string]any{ "sandbox_kind": "agent_daemon", "connector": string(p.cfg.Connector), @@ -610,9 +610,9 @@ func (p *E2BSandboxProvider) coldStart(ctx context.Context, in connector.PromptI // owned by the agent, not a human user. TokenTTL: SandboxAcquireTimeout + 30*time.Second, Config: map[string]any{ - "created_by": "sandbox_provider", - "agent_id": in.AgentID, - "sandbox_kind": "agent_daemon_claude_code", + "created_by": "sandbox_provider", + "agent_id": in.AgentID, + "sandbox_kind": "agent_daemon_claude_code", }, }) if err != nil { @@ -626,10 +626,10 @@ func (p *E2BSandboxProvider) coldStart(ctx context.Context, in connector.PromptI TemplateID: templateID, TimeoutSeconds: int(SandboxDefaultTTL.Seconds()), Metadata: map[string]string{ - "parsar.workspace_id": in.WorkspaceID, - "parsar.agent_id": in.AgentID, - "parsar.device_id": deviceID, - "parsar.sandbox_kind": "agent_daemon_claude_code", + "parsar.workspace_id": in.WorkspaceID, + "parsar.agent_id": in.AgentID, + "parsar.device_id": deviceID, + "parsar.sandbox_kind": "agent_daemon_claude_code", }, }) if err != nil { @@ -839,12 +839,12 @@ func (p *E2BSandboxProvider) coldStart(ctx context.Context, in connector.PromptI } } else { binding, bindErr := p.cfg.Bindings.CreateSandboxBinding(bootCtx, store.CreateSandboxBindingInput{ - WorkspaceID: in.WorkspaceID, - AgentID: in.AgentID, - CacheKey: "agent_daemon:" + in.AgentID, - SandboxID: sandbox.SandboxID, - TemplateID: templateID, - Status: store.SandboxBindingStatusActive, + WorkspaceID: in.WorkspaceID, + AgentID: in.AgentID, + CacheKey: "agent_daemon:" + in.AgentID, + SandboxID: sandbox.SandboxID, + TemplateID: templateID, + Status: store.SandboxBindingStatusActive, Metadata: map[string]any{ "sandbox_kind": "agent_daemon", "device_id": deviceID, @@ -1025,7 +1025,7 @@ func (p *E2BSandboxProvider) Reap(ctx context.Context) (int, error) { cutoff := time.Now().UTC().Add(-SandboxIdleReapThreshold) type victim struct { agentID string - entry *sandboxEntry + entry *sandboxEntry } var victims []victim p.cacheMu.Lock() diff --git a/server/internal/connector/agentdaemon/sandbox_provider_test.go b/server/internal/connector/agentdaemon/sandbox_provider_test.go index a29dbee..83c6439 100644 --- a/server/internal/connector/agentdaemon/sandbox_provider_test.go +++ b/server/internal/connector/agentdaemon/sandbox_provider_test.go @@ -82,7 +82,7 @@ func (f *fakeE2BClient) Create(_ context.Context, _ e2b.CreateInput) (e2b.Sandbo if err != nil { return e2b.Sandbox{}, err } - sb := e2b.Sandbox{SandboxID: pad("sbx-", id), TemplateID: "parsar-daemon-claudecode"} + sb := e2b.Sandbox{SandboxID: pad("sbx-", id), TemplateID: "parsar-sandbox-e2b"} if f.onCreate != nil { f.onCreate(sb) } @@ -221,7 +221,7 @@ func TestNewE2BSandboxProvider_RequiredFields(t *testing.T) { Store: &fakeMinter{}, Registry: gateway.NewRegistry(), Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", } good, err := NewE2BSandboxProvider(base) @@ -279,7 +279,7 @@ func TestE2BSandboxProvider_AcquireColdStart(t *testing.T) { Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) if err != nil { @@ -348,7 +348,7 @@ func TestE2BSandboxProvider_AcquireWarmCacheHit(t *testing.T) { Store: &fakeMinter{}, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) // Pre-seed cache as if a prior cold start had populated it. @@ -393,7 +393,7 @@ func TestE2BSandboxProvider_AcquireRecoversFromDeadDevice(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", ServerURL: "https://parsar.example.com", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) // Pre-seed with a stale entry whose device is not in the registry. p.cache["pa-1"] = &sandboxEntry{ @@ -437,7 +437,7 @@ func TestE2BSandboxProvider_AcquireSerialisesConcurrent(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", ServerURL: "https://parsar.example.com", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) var wg sync.WaitGroup @@ -478,7 +478,7 @@ func TestE2BSandboxProvider_AcquireConnectFailureKillsSandbox(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", ServerURL: "https://parsar.example.com", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) _, err := p.Acquire(context.Background(), connector.PromptInput{ AgentID: "pa-1", WorkspaceID: "wks-1", @@ -516,7 +516,7 @@ func TestE2BSandboxProvider_AcquireWaitForDeviceTimeout(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", ServerURL: "https://parsar.example.com", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) _, err := p.Acquire(context.Background(), connector.PromptInput{ AgentID: "pa-1", WorkspaceID: "wks-1", @@ -544,7 +544,7 @@ func TestE2BSandboxProvider_Release(t *testing.T) { }) p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: &fakeMinter{}, Registry: reg, Binder: binder, - Template: "parsar-daemon-claudecode", ServerURL: "https://parsar.example.com", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) p.cache["pa-1"] = &sandboxEntry{ deviceID: "dev-runtime-01", @@ -574,7 +574,7 @@ func TestE2BSandboxProvider_Reap(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: &fakeMinter{}, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", ServerURL: "https://parsar.example.com", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) prev := SandboxIdleReapThreshold SandboxIdleReapThreshold = 1 * time.Minute @@ -648,7 +648,7 @@ func TestE2BSandboxProvider_SeedFailureKillsSandbox(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", ServerURL: "https://parsar.example.com", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) _, err := p.Acquire(context.Background(), connector.PromptInput{ AgentID: "pa-1", WorkspaceID: "wks-1", @@ -699,7 +699,7 @@ func TestE2BSandboxProvider_ColdStartPropagatesTGEnv(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", // Leave Connector zero-value: documented to default to claude // because the only published template is Claude-based. Test @@ -768,7 +768,7 @@ func TestE2BSandboxProvider_ColdStartOmitsEmptyTGEnv(t *testing.T) { p, _ := NewE2BSandboxProvider(E2BProviderConfig{ Client: e2bClient, Store: minter, Registry: reg, Binder: binding.NewInMemoryBinder(), - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", Connector: SandboxConnectorClaude, // explicit, not zero-value }) @@ -908,7 +908,7 @@ func TestE2BSandboxProvider_AcquireWinnerFinalizesReservation(t *testing.T) { Registry: reg, Binder: binding.NewInMemoryBinder(), Bindings: bindings, - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) if err != nil { @@ -982,7 +982,7 @@ func TestE2BSandboxProvider_AcquireLoserReusesWinnerDevice(t *testing.T) { Registry: reg, Binder: binding.NewInMemoryBinder(), Bindings: bindings, - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) if err != nil { @@ -1038,7 +1038,7 @@ func TestE2BSandboxProvider_AcquireWinnerFailureReleasesReservation(t *testing.T Registry: reg, Binder: binding.NewInMemoryBinder(), Bindings: bindings, - Template: "parsar-daemon-claudecode", + Template: "parsar-sandbox-e2b", ServerURL: "https://parsar.example.com", }) if err != nil { diff --git a/server/internal/connector/agentdaemon/sandbox_seed.go b/server/internal/connector/agentdaemon/sandbox_seed.go index b2bf205..cd2e3af 100644 --- a/server/internal/connector/agentdaemon/sandbox_seed.go +++ b/server/internal/connector/agentdaemon/sandbox_seed.go @@ -134,7 +134,7 @@ func seedPlatformConfig(ctx context.Context, client E2BClient, sb e2b.Sandbox, c switch conn { case SandboxConnectorClaude, "": // Empty defaults to Claude because the only template currently - // boots Claude (parsar-daemon-claudecode). + // boots Claude (parsar-sandbox-e2b). return seedClaudeConfig(ctx, client, sb, envdURL) case SandboxConnectorOpenCode: // TODO: wire spec/memory injection via OpenCode hook scripts diff --git a/server/internal/dev/sandbox_admin_test.go b/server/internal/dev/sandbox_admin_test.go index e6db151..9d1c8b1 100644 --- a/server/internal/dev/sandbox_admin_test.go +++ b/server/internal/dev/sandbox_admin_test.go @@ -11,9 +11,9 @@ import ( "testing" "time" - "github.com/go-chi/chi/v5" "github.com/MiniMax-AI-Dev/parsar/server/internal/connector" "github.com/MiniMax-AI-Dev/parsar/server/internal/store" + "github.com/go-chi/chi/v5" ) // fakeSandboxBindingStore is the test-side SandboxBindingStore stand-in. @@ -213,15 +213,15 @@ func TestSandboxAdminStatusReturnsLiveBinding(t *testing.T) { now := time.Now().UTC() storeFake := &fakeSandboxBindingStore{ binding: &store.SandboxBindingRead{ - ID: "11111111-1111-1111-1111-111111111111", - WorkspaceID: "00000000-0000-0000-0000-000000000002", - AgentID: strPtr("00000000-0000-0000-0000-000000000009"), - SandboxID: "sbx_abc", - TemplateID: "parsar-daemon-claudecode", - Status: store.SandboxBindingStatusActive, - CreatedAt: now.Add(-2 * time.Minute), - LastActiveAt: now.Add(-30 * time.Second), - Metadata: map[string]any{"source": "test"}, + ID: "11111111-1111-1111-1111-111111111111", + WorkspaceID: "00000000-0000-0000-0000-000000000002", + AgentID: strPtr("00000000-0000-0000-0000-000000000009"), + SandboxID: "sbx_abc", + TemplateID: "parsar-sandbox-e2b", + Status: store.SandboxBindingStatusActive, + CreatedAt: now.Add(-2 * time.Minute), + LastActiveAt: now.Add(-30 * time.Second), + Metadata: map[string]any{"source": "test"}, }, } router := newSandboxTestRouter(sandboxAdminDeps{store: storeFake}) @@ -377,26 +377,26 @@ func TestSandboxAdminListReturnsActiveBindings(t *testing.T) { storeFake := &fakeSandboxBindingStore{ listRows: []store.SandboxBindingRead{ { - ID: "00000000-0000-0000-0000-000000000aa1", - WorkspaceID: "00000000-0000-0000-0000-000000000002", - AgentID: strPtr("00000000-0000-0000-0000-000000000009"), - SandboxID: "sbx_alpha", - TemplateID: "parsar-daemon-claudecode", - Status: store.SandboxBindingStatusActive, - CreatedAt: now.Add(-5 * time.Minute), - LastActiveAt: now.Add(-30 * time.Second), - Metadata: map[string]any{"sandbox_kind": "agent_daemon"}, + ID: "00000000-0000-0000-0000-000000000aa1", + WorkspaceID: "00000000-0000-0000-0000-000000000002", + AgentID: strPtr("00000000-0000-0000-0000-000000000009"), + SandboxID: "sbx_alpha", + TemplateID: "parsar-sandbox-e2b", + Status: store.SandboxBindingStatusActive, + CreatedAt: now.Add(-5 * time.Minute), + LastActiveAt: now.Add(-30 * time.Second), + Metadata: map[string]any{"sandbox_kind": "agent_daemon"}, }, { - ID: "00000000-0000-0000-0000-000000000aa2", - WorkspaceID: "00000000-0000-0000-0000-000000000002", - AgentID: strPtr("00000000-0000-0000-0000-000000000010"), - SandboxID: "sbx_beta", - TemplateID: "parsar-daemon-claudecode", - Status: store.SandboxBindingStatusSpawning, - CreatedAt: now.Add(-1 * time.Minute), - LastActiveAt: now.Add(-1 * time.Minute), - Metadata: map[string]any{"sandbox_kind": "agent_daemon"}, + ID: "00000000-0000-0000-0000-000000000aa2", + WorkspaceID: "00000000-0000-0000-0000-000000000002", + AgentID: strPtr("00000000-0000-0000-0000-000000000010"), + SandboxID: "sbx_beta", + TemplateID: "parsar-sandbox-e2b", + Status: store.SandboxBindingStatusSpawning, + CreatedAt: now.Add(-1 * time.Minute), + LastActiveAt: now.Add(-1 * time.Minute), + Metadata: map[string]any{"sandbox_kind": "agent_daemon"}, }, }, } From d4ef5b6de96f9caa3a5d58987aefc390ddf94da0 Mon Sep 17 00:00:00 2001 From: yuanhe Date: Sat, 11 Jul 2026 17:08:02 +0800 Subject: [PATCH 2/2] Default the local sandbox image to the GHCR-published one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sandbox-image-release.yml now successfully publishes ghcr.io/minimax-ai-dev/parsar-sandbox on every push to main (verified: pulled it and confirmed all 4 agent CLIs work). Point install.sh / docker-compose.yml at it by default instead of requiring an operator to `docker build` locally first — this is the last piece of the original "sandbox does not start after ./install.sh" investigation: zero-command install now actually includes working sandbox mode. - install.sh: DEFAULT_SANDBOX_IMAGE -> ghcr.io/minimax-ai-dev/parsar-sandbox:latest - docker-compose.yml: AGENT_DAEMON_SANDBOX_DOCKER_IMAGE default -> same - INSTALL.md: drop the "no published default, build one first" note; document PARSAR_SANDBOX_IMAGE=parsar-sandbox:local as the opt-out for a local build instead Verified end to end: `./install.sh` with no --sandbox-image flag and no PARSAR_SANDBOX_IMAGE override writes the GHCR image into .env, and parsar-server's startup log confirms "agent_daemon docker sandbox provider wired" against ghcr.io/minimax-ai-dev/parsar-sandbox:latest. Co-Authored-By: Claude Sonnet 5 --- INSTALL.md | 8 ++++---- docker-compose.yml | 11 ++++++----- install.sh | 9 +++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 93d378e..9319e67 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -23,13 +23,13 @@ user is the administrator. - Docker Engine with Docker Compose v2. - Linux host with access to `/var/run/docker.sock`. The local compose stack - enables Docker-managed agent sandboxes and mounts the Docker socket. -- To actually run Agents in sandbox mode, build the sandbox image first — - there is no published default: + enables Docker-managed agent sandboxes and mounts the Docker socket. The + sandbox image (`ghcr.io/minimax-ai-dev/parsar-sandbox:latest`) is pulled + automatically — build your own instead with: ```bash docker build -f infra/sandbox/Dockerfile -t parsar-sandbox:local . + PARSAR_SANDBOX_IMAGE=parsar-sandbox:local ./install.sh ``` - Everything else in the stack works without it. ## What The Installer Does diff --git a/docker-compose.yml b/docker-compose.yml index 3c3d5b2..f435c26 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -156,16 +156,17 @@ services: # Docker sandbox — Agent runs inside a sandboxed container with # Claude Code + Codex + Pi + parsar-daemon. The server calls docker # to start / exec / stop sandbox containers via the mounted - # /var/run/docker.sock below. There is no published image for this — - # build one locally first (README's 5.2 section): + # /var/run/docker.sock below. Defaults to the GHCR-published image + # (infra/sandbox/Dockerfile, built + pushed by + # .github/workflows/sandbox-image-release.yml). Build your own + # instead with: # docker build -f infra/sandbox/Dockerfile -t parsar-sandbox:local . - # Until you do, sandbox-mode Agent runs fail with "no such image"; - # everything else in this stack works fine without it. + # PARSAR_SANDBOX_IMAGE=parsar-sandbox:local # The network below is pinned to `parsar_default` — the top-level # `name: parsar` + `networks:` block at the head of this file # guarantees that alias regardless of the clone directory. AGENT_DAEMON_SANDBOX_BACKEND: "docker" - AGENT_DAEMON_SANDBOX_DOCKER_IMAGE: "${PARSAR_SANDBOX_IMAGE:-parsar-sandbox:local}" + AGENT_DAEMON_SANDBOX_DOCKER_IMAGE: "${PARSAR_SANDBOX_IMAGE:-ghcr.io/minimax-ai-dev/parsar-sandbox:latest}" AGENT_DAEMON_SANDBOX_DOCKER_NETWORK: "${PARSAR_PROJECT_NAME:-parsar}_default" AGENT_DAEMON_SANDBOX_SERVER_URL: "http://parsar-server:8080" group_add: diff --git a/install.sh b/install.sh index 7fe6066..b1a4a40 100755 --- a/install.sh +++ b/install.sh @@ -3,7 +3,7 @@ set -euo pipefail DEFAULT_COMPOSE_URL="https://raw.githubusercontent.com/MiniMax-AI-Dev/parsar/main/docker-compose.yml" DEFAULT_SERVER_IMAGE="ghcr.io/minimax-ai-dev/parsar-server:latest" -DEFAULT_SANDBOX_IMAGE="parsar-sandbox:local" +DEFAULT_SANDBOX_IMAGE="ghcr.io/minimax-ai-dev/parsar-sandbox:latest" usage() { cat <<'EOF' @@ -20,10 +20,11 @@ Options: when present, otherwise download the published template. --image IMAGE parsar-server image. Default: ghcr.io/minimax-ai-dev/parsar-server:latest - --sandbox-image IMAGE Docker sandbox image. There is no published - default — build one first: + --sandbox-image IMAGE Docker sandbox image. + Default: ghcr.io/minimax-ai-dev/parsar-sandbox:latest + To use your own build instead: docker build -f infra/sandbox/Dockerfile -t parsar-sandbox:local . - Default: parsar-sandbox:local + --sandbox-image parsar-sandbox:local --port PORT Web UI host port. Default: 18080 --pg-port PORT Postgres host port. Default: 15432 --bind ADDR Web UI bind address. Default: 127.0.0.1