Skip to content

Commit aedaf4d

Browse files
quickbeardclaude
andcommitted
feat(sandbox): auto-approve permissions inside sandboxes
The injected guest config now always carries permission: "allow" (and is injected even without a gateway login — providers skip the broker when the route table is empty). Everything the agent does is confined to the VM and the one mounted project folder, so prompts buy nothing there; the server stops asking, and the app's existing enableConfiguredDirectory seam surfaces it as directory auto-accept, which the user can still toggle off and a project config can override (project merges after CODEV_CONFIG). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1626d1c commit aedaf4d

6 files changed

Lines changed: 71 additions & 18 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ file is the human-readable summary of what changed and why.
99

1010
### Desktop / Web UI
1111

12+
- **Sandboxes auto-approve permissions** — inside a sandbox, everything the
13+
agent does is confined to the VM and the one mounted project folder, so
14+
permission prompts are skipped by default: sandbox projects behave as if
15+
auto-accept were switched on, which is what lets you hand the sandbox a
16+
task and walk away. The directory-level auto-accept toggle still works to
17+
turn it off for a given project, and a project config inside the workspace
18+
can set stricter rules.
1219
- **Sandboxes can now talk to the model gateway** — each running sandbox gets
1320
a host-side egress broker: the guest (which has no network access of its
1421
own) reaches exactly one brokered path that forwards to the configured LLM

‎packages/codev-sandbox/src/bwrap/provider.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ export function createBwrapProvider(options: BwrapProviderOptions): SandboxProvi
105105
// Listening before boot: the guest-side bridge dials broker.sock per
106106
// connection and drops it when nothing serves the socket.
107107
const egress = (await options.resolveEgress?.()) ?? null
108-
const broker = egress ? createBrokerServer({ routes: egress.routes, fetchImpl: egress.fetchImpl ?? fetchImpl }) : null
108+
// No routes means no egress at all — the config injection below can
109+
// still apply without a listener.
110+
const broker = egress?.routes.length
111+
? createBrokerServer({ routes: egress.routes, fetchImpl: egress.fetchImpl ?? fetchImpl })
112+
: null
109113
if (broker) await broker.listenUnix(join(dirs.runtime, "broker.sock"))
110114
if (egress?.guestConfig) await writeFile(join(dirs.bridge, "codev.json"), egress.guestConfig)
111115

‎packages/codev-sandbox/src/vz/provider.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider {
116116
// dials runtime/broker.sock per guest connection and refuses when
117117
// nothing serves it.
118118
const egress = (await options.resolveEgress?.()) ?? null
119-
const broker = egress ? createBrokerServer({ routes: egress.routes, fetchImpl: egress.fetchImpl ?? fetchImpl }) : null
119+
// No routes means no egress at all — the config injection below can
120+
// still apply without a listener.
121+
const broker = egress?.routes.length
122+
? createBrokerServer({ routes: egress.routes, fetchImpl: egress.fetchImpl ?? fetchImpl })
123+
: null
120124
if (broker) await broker.listenUnix(join(dirs.runtime, "broker.sock"))
121125
if (egress?.guestConfig) await writeFile(join(dirs.guestConfig, "codev.json"), egress.guestConfig)
122126

‎packages/codev-sandbox/src/wsl/provider.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,27 @@ test("egress wires the broker bridge, guest config, and CODEV_CONFIG", async ()
192192
await rm(dir, { recursive: true, force: true })
193193
})
194194

195+
test("routeless egress still injects the guest config but starts no broker", async () => {
196+
const dir = await mkdtemp(join(tmpdir(), "wsl-test-"))
197+
const provider = createWslProvider(
198+
await preparedOptions(dir, {
199+
resolveEgress: async () => ({ routes: [], guestConfig: `{"permission":"allow"}` }),
200+
}),
201+
)
202+
await provider.create(config)
203+
const sandbox = await provider.start(config)
204+
205+
const state = JSON.parse(await Bun.file(join(dir, "state.json")).text())
206+
expect(state.bootScript).not.toContain("socat")
207+
expect(state.bootScript).toContain("/codev-config")
208+
expect(state.codevConfigEnv).toBe("/codev-config/codev.json")
209+
expect(await Bun.file(join(dir, "data", config.name, "config", "codev.json")).text()).toBe(`{"permission":"allow"}`)
210+
211+
sandbox.stop()
212+
await provider.destroy(config)
213+
await rm(dir, { recursive: true, force: true })
214+
})
215+
195216
test("create then start reaches an authenticated health check, and destroy unregisters", async () => {
196217
const dir = await mkdtemp(join(tmpdir(), "wsl-test-"))
197218
const provider = createWslProvider(await preparedOptions(dir))

‎packages/codev-sandbox/src/wsl/provider.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,11 @@ export function createWslProvider(options: WslProviderOptions): SandboxProvider
144144
const port = await reserveLoopbackPort()
145145

146146
const egress = (await options.resolveEgress?.()) ?? null
147-
const broker = egress ? createBrokerServer({ routes: egress.routes, fetchImpl: egress.fetchImpl ?? fetchImpl }) : null
147+
// No routes means no egress at all — the config injection below can
148+
// still apply without a listener.
149+
const broker = egress?.routes.length
150+
? createBrokerServer({ routes: egress.routes, fetchImpl: egress.fetchImpl ?? fetchImpl })
151+
: null
148152
// The guest dials the host at the NAT default gateway address, so the
149153
// broker cannot bind loopback here.
150154
const brokerPort = broker ? await broker.listen("0.0.0.0") : null

‎packages/desktop/src/main/sandbox/egress.ts‎

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,38 @@ import type { SandboxEgress } from "@codev/sandbox"
99
const GUEST_BROKER_BASE = "http://127.0.0.1:8788/llm"
1010

1111
// Resolved at every sandbox start so a rotated gateway token is picked up by
12-
// the next start rather than requiring an app restart. Null (no gateway login
13-
// on this machine) leaves the sandbox with no egress at all: the guest boots
14-
// and serves, but model calls fail — the same behavior as before the broker.
15-
export async function resolveSandboxEgress(): Promise<SandboxEgress | null> {
12+
// the next start rather than requiring an app restart. Without a gateway
13+
// login the routes are empty (no broker, no egress at all), but the guest
14+
// config is still injected for the permission default below.
15+
export async function resolveSandboxEgress(): Promise<SandboxEgress> {
1616
const creds = loadApiKey()
17-
if (!creds?.baseUrl || !creds.model) return null
17+
const gateway = creds?.baseUrl && creds.model ? creds : null
1818
return {
19-
routes: [
19+
routes: gateway
20+
? [
21+
{
22+
prefix: "/llm/v1",
23+
upstream: normalizeGatewayBaseUrl(gateway.baseUrl!),
24+
// The guest config carries a placeholder key; the broker swaps in
25+
// the real one, so the token never enters the guest and rotation
26+
// stays host-side.
27+
headers: { authorization: `Bearer ${gateway.apiKey}` },
28+
},
29+
]
30+
: [],
31+
guestConfig: JSON.stringify(
2032
{
21-
prefix: "/llm/v1",
22-
upstream: normalizeGatewayBaseUrl(creds.baseUrl),
23-
// The guest config carries a placeholder key; the broker swaps in the
24-
// real one, so the token never enters the guest and rotation stays
25-
// host-side.
26-
headers: { authorization: `Bearer ${creds.apiKey}` },
33+
...(gateway
34+
? buildGatewayConfig({ apiKey: "sandbox-broker", baseUrl: GUEST_BROKER_BASE, model: gateway.model! })
35+
: {}),
36+
// Everything the agent does is confined to the VM and the one mounted
37+
// project folder, so permission prompts buy nothing here — auto-allow
38+
// is what makes "hand it a task and walk away" work. The app surfaces
39+
// this as directory auto-accept (enableConfiguredDirectory), which the
40+
// user can still toggle off per directory, and a project config inside
41+
// the workspace can override it (project merges after CODEV_CONFIG).
42+
permission: "allow",
2743
},
28-
],
29-
guestConfig: JSON.stringify(
30-
buildGatewayConfig({ apiKey: "sandbox-broker", baseUrl: GUEST_BROKER_BASE, model: creds.model }),
3144
null,
3245
2,
3346
),

0 commit comments

Comments
 (0)