diff --git a/docs/configuration.md b/docs/configuration.md index 6a6f607bd..1f45e7f31 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -115,6 +115,7 @@ Subagent providers are explicit. Omitted providers are disabled: "enabled": true, "model": "gpt-5.4", "effort": "high", + "networkAccess": false, }, { "id": "claude", @@ -126,6 +127,17 @@ Subagent providers are explicit. Omitted providers are disabled: } ``` +For Codex workers that need package downloads or HTTP requests, set +`"networkAccess": true` on the `codex` provider entry. It defaults to `false` and +only enables network access in Codex's `workspace-write` sandbox; filesystem +write restrictions remain in place. Read-only and full-access modes are +unchanged. This owner setting applies to all Codex profiles; it is not a +per-task CLI or profile override. Other providers reject this option. + +After changing it, let running agents finish, then run +`devspace agents daemon stop`. The next agent command starts a daemon with the +updated configuration. + Profiles are loaded from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`. `devspace agents targets` prints the configured targets available in the current workspace. diff --git a/schema/v1/devspace.schema.json b/schema/v1/devspace.schema.json index e7c18466e..d0719f638 100644 --- a/schema/v1/devspace.schema.json +++ b/schema/v1/devspace.schema.json @@ -192,6 +192,9 @@ "effort": { "type": "string", "minLength": 1 + }, + "networkAccess": { + "type": "boolean" } }, "required": [ diff --git a/src/config.test.ts b/src/config.test.ts index 5fd24b490..17ad4af59 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -53,7 +53,7 @@ try { skills: { enabled: false, paths: ["~/skills"], agentDir: "~/agent" }, subagents: { enabled: true, - providers: [{ id: "codex", enabled: true }], + providers: [{ id: "codex", enabled: true, networkAccess: true }], }, logging: { level: "debug", @@ -96,6 +96,7 @@ try { assert.deepEqual(configured.skillPaths, ["~/skills"]); assert.equal(configured.agentDir, resolve(homedir(), "agent")); assert.equal(configured.subagents.enabled, true); + assert.equal(configured.subagents.providers[0]?.networkAccess, true); assert.equal(configured.oauth.ownerToken, "persisted-owner-token-long-enough"); assert.equal(configured.oauth.accessTokenTtlSeconds, 120); assert.deepEqual(configured.oauth.scopes, ["devspace", "admin"]); diff --git a/src/local-agent-adapters.ts b/src/local-agent-adapters.ts index 03a5cc40c..42c0b3ec4 100644 --- a/src/local-agent-adapters.ts +++ b/src/local-agent-adapters.ts @@ -27,6 +27,7 @@ export type LocalAgentAdapter = LocalAgentDriver; export interface LocalAgentDriverOptions { env?: NodeJS.ProcessEnv; + codexNetworkAccess?: boolean; claudeQueryFactory?: ClaudeQueryFactory; opencodeFactory?: OpencodeFactory; piSessionFactory?: PiSessionFactory; @@ -36,7 +37,7 @@ export function createLocalAgentDrivers( options: LocalAgentDriverOptions = {}, ): LocalAgentDriver[] { return [ - new CodexLocalAgentDriver(options.env), + new CodexLocalAgentDriver(options.env, undefined, options.codexNetworkAccess), new ClaudeLocalAgentDriver(options.claudeQueryFactory, options.env), new OpencodeLocalAgentDriver(options.opencodeFactory), new PiLocalAgentDriver(options.piSessionFactory), diff --git a/src/local-agent-codex.test.ts b/src/local-agent-codex.test.ts index b5862d2b8..aa32fcd8c 100644 --- a/src/local-agent-codex.test.ts +++ b/src/local-agent-codex.test.ts @@ -73,7 +73,9 @@ readline.createInterface({ input: process.stdin }).on("line", (line) => { output({ method: "turn/completed", params: { threadId: message.params.threadId, turn: { id: turnId, status: "completed", items: [] } } }); return; } - const item = { type: "agentMessage", text: "fake response " + turn }; + const item = { type: "agentMessage", text: message.params.input[0].text === "policy" + ? JSON.stringify(message.params) + : "fake response " + turn }; output({ method: "item/completed", params: { threadId: message.params.threadId, turnId, item } }); output({ method: "turn/completed", params: { threadId: message.params.threadId, turn: { id: turnId, status: "completed", items: [item] } } }); }); @@ -134,6 +136,27 @@ readline.createInterface({ input: process.stdin }).on("line", (line) => { assert.equal("cause" in toAgentErrorPayload(protocolFailure.error), false); } await runtime.releaseSession("thread_new"); + for (const networkAccess of [undefined, false, true]) { + const policyRuntime = new CodexAppServerRuntime({ command, env: process.env, networkAccess }); + try { + await policyRuntime.initialize(); + for (const writeMode of ["allowed", "read_only", "full_access"] as const) { + for (const providerSessionId of [undefined, "thread_new"]) { + const result = await policyRuntime.run({ + prompt: "policy", workspaceRoot: "/tmp/project", writeMode, providerSessionId, + }); + if (result.isErr()) throw result.error; + const params = JSON.parse(result.value.finalResponse); + assert.equal(params.approvalPolicy, "never"); + assert.deepEqual(params.sandboxPolicy, writeMode === "allowed" + ? { type: "workspaceWrite", networkAccess: networkAccess ?? false } + : { type: writeMode === "read_only" ? "readOnly" : "dangerFullAccess" }); + } + } + } finally { + await policyRuntime.close(); + } + } } finally { await runtime.close(); await runtime.close(); diff --git a/src/local-agent-codex.ts b/src/local-agent-codex.ts index dac27cc35..af7c0c1d6 100644 --- a/src/local-agent-codex.ts +++ b/src/local-agent-codex.ts @@ -77,6 +77,7 @@ export interface CodexAppServerRuntimeOptions { command: string; env: NodeJS.ProcessEnv; version?: string; + networkAccess?: boolean; } export class CodexAppServerRuntime implements LocalAgentRuntime { @@ -146,7 +147,7 @@ export class CodexAppServerRuntime implements LocalAgentRuntime { } await callbacks?.onSessionId?.(threadId); - const completed = await this.rpc.runTurn(threadId, turnParams(input, threadId)); + const completed = await this.rpc.runTurn(threadId, turnParams(input, threadId, this.options.networkAccess)); const parsed = parseCompletedTurn(completed.event.params, completed.items); if (parsed.failure) { throw new AgentProviderExecutionError({ @@ -237,13 +238,14 @@ export class CodexLocalAgentDriver implements LocalAgentDriver { constructor( private readonly env: NodeJS.ProcessEnv = process.env, private readonly commandResolver: CodexCommandResolver = resolveCodexCommand, + private readonly networkAccess = false, ) {} runtimeKey(_context: LocalAgentRuntimeContext): string { const command = this.resolveCommand(); const executable = command?.executable ?? this.env.CODEX_COMMAND ?? "codex"; const codexHome = resolve(this.env.CODEX_HOME ?? join(homedir(), ".codex")); - return `codex:${executable}:${codexHome}`; + return `codex:${executable}:${codexHome}:${this.networkAccess}`; } async createRuntime(_context: LocalAgentRuntimeContext) { @@ -274,6 +276,7 @@ export class CodexLocalAgentDriver implements LocalAgentDriver { command: command.executable, env: codexCommandEnvironment(this.env), version: command.version, + networkAccess: this.networkAccess, }); try { await runtime.initialize(); @@ -461,12 +464,16 @@ function threadParams(input: LocalAgentRunInput): Record { }; } -function turnParams(input: LocalAgentRunInput, threadId: string): Record { +function turnParams( + input: LocalAgentRunInput, + threadId: string, + networkAccess = false, +): Record { return { threadId, input: [{ type: "text", text: input.prompt }], approvalPolicy: "never", - sandboxPolicy: sandboxPolicyFor(input.writeMode), + sandboxPolicy: sandboxPolicyFor(input.writeMode, networkAccess), ...(input.model ? { model: input.model } : {}), ...(input.effort ? { effort: input.effort } : {}), }; @@ -481,9 +488,12 @@ export function sandboxFor(writeMode: LocalAgentWriteMode | undefined): string { } } -function sandboxPolicyFor(writeMode: LocalAgentWriteMode | undefined): Record { +function sandboxPolicyFor( + writeMode: LocalAgentWriteMode | undefined, + networkAccess: boolean, +): Record { switch (writeMode) { - case "allowed": return { type: "workspaceWrite" }; + case "allowed": return { type: "workspaceWrite", networkAccess }; case "full_access": return { type: "dangerFullAccess" }; case "read_only": case undefined: return { type: "readOnly" }; diff --git a/src/local-agent-config.test.ts b/src/local-agent-config.test.ts index e37ceafac..b1467e912 100644 --- a/src/local-agent-config.test.ts +++ b/src/local-agent-config.test.ts @@ -19,6 +19,20 @@ assert.deepEqual(config, { { id: "claude", enabled: false, model: "sonnet" }, ], }); +for (const networkAccess of [false, true]) { + const parsed = subagentsConfigSchema.parse({ + enabled: true, + providers: [{ id: "codex", enabled: true, networkAccess }], + }); + assert.equal(subagentProviderConfig(parsed, "codex")?.networkAccess, networkAccess); +} +assert.equal(subagentProviderConfig(config, "codex")?.networkAccess, undefined); +for (const provider of [ + { id: "codex", enabled: true, networkAccess: "true" }, + { id: "claude", enabled: true, networkAccess: true }, +]) { + assert.equal(subagentsConfigSchema.safeParse({ enabled: true, providers: [provider] }).success, false); +} assert.equal(isSubagentProviderEnabled(config, "codex"), true); assert.equal(isSubagentProviderEnabled(config, "claude"), false); assert.equal(isSubagentProviderEnabled(config, "pi"), false); diff --git a/src/local-agent-config.ts b/src/local-agent-config.ts index 62e9c35a0..09fb7740a 100644 --- a/src/local-agent-config.ts +++ b/src/local-agent-config.ts @@ -9,6 +9,7 @@ const providerSchema = z.object({ enabled: z.boolean(), model: z.string().trim().min(1).optional(), effort: z.string().trim().min(1).optional(), + networkAccess: z.boolean().optional(), }).strict(); export const subagentsConfigSchema = z.object({ @@ -25,6 +26,13 @@ export const subagentsConfigSchema = z.object({ }); } seen.add(provider.id); + if (provider.networkAccess !== undefined && provider.id !== "codex") { + context.addIssue({ + code: "custom", + path: ["providers", index, "networkAccess"], + message: "networkAccess is only supported by the codex provider", + }); + } } }); diff --git a/src/local-agent-daemon-main.ts b/src/local-agent-daemon-main.ts index b1e0e09da..b72dea0f1 100644 --- a/src/local-agent-daemon-main.ts +++ b/src/local-agent-daemon-main.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { loadConfig } from "./config.js"; import { createLocalAgentDrivers } from "./local-agent-adapters.js"; +import { subagentProviderConfig } from "./local-agent-config.js"; import { loadLocalAgentProfiles } from "./local-agent-profiles.js"; import { LocalAgentDaemon, writeLocalAgentDaemonLog } from "./local-agent-daemon.js"; import { @@ -22,7 +23,9 @@ const log = ( const store = new LocalAgentStore(paths.stateDir); const manager = new LocalAgentManager({ store, - drivers: createLocalAgentDrivers(), + drivers: createLocalAgentDrivers({ + codexNetworkAccess: subagentProviderConfig(config.subagents, "codex")?.networkAccess, + }), pool: new LocalAgentRuntimePool({ logger: log }), loadProfiles: (workspaceRoot) => loadLocalAgentProfiles(config, workspaceRoot, { includeDisabled: true }), agentDir: config.agentDir,