diff --git a/extensions/codex/package.json b/extensions/codex/package.json index 16a903b5197..0f865d98d76 100644 --- a/extensions/codex/package.json +++ b/extensions/codex/package.json @@ -5,7 +5,8 @@ "type": "module", "dependencies": { "@mariozechner/pi-coding-agent": "0.65.2", - "ws": "^8.20.0" + "ws": "^8.20.0", + "zod": "^4.3.6" }, "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" diff --git a/extensions/codex/src/app-server/config.test.ts b/extensions/codex/src/app-server/config.test.ts new file mode 100644 index 00000000000..4fa01b12722 --- /dev/null +++ b/extensions/codex/src/app-server/config.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js"; + +describe("Codex app-server config", () => { + it("parses typed plugin config before falling back to environment knobs", () => { + const runtime = resolveCodexAppServerRuntimeOptions({ + pluginConfig: { + appServer: { + transport: "websocket", + url: "ws://127.0.0.1:39175", + headers: { "X-Test": "yes" }, + approvalPolicy: "on-request", + sandbox: "danger-full-access", + approvalsReviewer: "guardian_subagent", + serviceTier: "priority", + }, + }, + env: { + OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY: "never", + OPENCLAW_CODEX_APP_SERVER_SANDBOX: "read-only", + }, + }); + + expect(runtime).toEqual( + expect.objectContaining({ + approvalPolicy: "on-request", + sandbox: "danger-full-access", + approvalsReviewer: "guardian_subagent", + serviceTier: "priority", + start: expect.objectContaining({ + transport: "websocket", + url: "ws://127.0.0.1:39175", + headers: { "X-Test": "yes" }, + }), + }), + ); + }); + + it("rejects malformed plugin config instead of treating freeform strings as control values", () => { + expect( + readCodexPluginConfig({ + appServer: { + approvalPolicy: "always", + }, + }), + ).toEqual({}); + }); +}); diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index fa58dcc0207..45bd06e6e26 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export type CodexAppServerTransportMode = "stdio" | "websocket"; export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure" | "untrusted"; export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access"; @@ -41,11 +43,47 @@ export type CodexPluginConfig = { }; }; +const codexAppServerTransportSchema = z.enum(["stdio", "websocket"]); +const codexAppServerApprovalPolicySchema = z.enum([ + "never", + "on-request", + "on-failure", + "untrusted", +]); +const codexAppServerSandboxSchema = z.enum(["read-only", "workspace-write", "danger-full-access"]); +const codexAppServerApprovalsReviewerSchema = z.enum(["user", "guardian_subagent"]); + +const codexPluginConfigSchema = z + .object({ + discovery: z + .object({ + enabled: z.boolean().optional(), + timeoutMs: z.number().positive().optional(), + }) + .strict() + .optional(), + appServer: z + .object({ + transport: codexAppServerTransportSchema.optional(), + command: z.string().optional(), + args: z.union([z.array(z.string()), z.string()]).optional(), + url: z.string().optional(), + authToken: z.string().optional(), + headers: z.record(z.string(), z.string()).optional(), + requestTimeoutMs: z.number().positive().optional(), + approvalPolicy: codexAppServerApprovalPolicySchema.optional(), + sandbox: codexAppServerSandboxSchema.optional(), + approvalsReviewer: codexAppServerApprovalsReviewerSchema.optional(), + serviceTier: z.string().optional(), + }) + .strict() + .optional(), + }) + .strict(); + export function readCodexPluginConfig(value: unknown): CodexPluginConfig { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return {}; - } - return value as CodexPluginConfig; + const parsed = codexPluginConfigSchema.safeParse(value); + return parsed.success ? parsed.data : {}; } export function resolveCodexAppServerRuntimeOptions( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b536839480..f0a373282ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -426,6 +426,9 @@ importers: ws: specifier: ^8.20.0 version: 8.20.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@openclaw/plugin-sdk': specifier: workspace:*