fix: respect Codex requirements for app-server defaults (#79151)

* fix(codex): honor requirements for app-server defaults

* test(codex): harden requirements policy coverage

* fix(codex): match requirements sandbox constraints

* fix(codex): honor approval requirements in defaults

* fix(codex): honor reviewer requirements in defaults

* fix(codex): honor remote sandbox requirements
This commit is contained in:
Kevin Lin
2026-05-07 21:16:08 -07:00
committed by GitHub
parent 36f847a60e
commit f62618f805
6 changed files with 705 additions and 39 deletions

View File

@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
- Runtime/install: raise the supported Node 22 floor to `22.16+` so native SQLite query handling can rely on the `node:sqlite` statement metadata API while continuing to recommend Node 24. (#78921)
- Discord/voice: include a bounded one-line STT transcript preview in verbose voice logs so live voice debugging shows what speakers said before the agent reply.
- Codex app-server: pin the managed Codex harness and Codex CLI smoke package to `@openai/codex@0.129.0`, defer OpenClaw integration dynamic tools behind Codex tool search by default, and accept current Codex service-tier values so legacy `fast` settings survive the stable harness upgrade as `priority`.
- Codex app-server: default implicit local stdio app-server permissions to guardian when Codex system requirements disallow the YOLO approval, reviewer, or sandbox value, including hostname-scoped remote sandbox entries, avoiding turn-start failures on managed hosts that permit only reviewed approval or narrower sandboxes.
- Discord/voice: stream ElevenLabs TTS directly into Discord playback and send ElevenLabs latency optimization as the documented query parameter so spoken replies can start sooner.
- Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics.
- Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana.

View File

@@ -484,7 +484,13 @@ By default, OpenClaw starts local Codex harness sessions in YOLO mode:
`approvalPolicy: "never"`, `approvalsReviewer: "user"`, and
`sandbox: "danger-full-access"`. This is the trusted local operator posture used
for autonomous heartbeats: Codex can use shell and network tools without
stopping on native approval prompts that nobody is around to answer.
stopping on native approval prompts that nobody is around to answer. On local
stdio Codex app-server installs where Codex's system requirements file
disallows the implicit YOLO approval, reviewer, or sandbox value, OpenClaw
treats the implicit default as guardian instead and selects allowed guardian
permissions so it does not send an override that Codex app-server will reject.
Hostname-matching `[[remote_sandbox_config]]` entries in the same requirements
file are honored for the sandbox default decision.
To opt in to Codex guardian-reviewed approvals, set `appServer.mode:
"guardian"`:
@@ -635,22 +641,22 @@ Supported top-level Codex plugin fields:
Supported `appServer` fields:
| Field | Default | Meaning |
| ----------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. `CODEX_HOME` and `HOME` are reserved for OpenClaw's per-agent Codex isolation on local launches. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after a turn-scoped Codex app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `mode` | `"yolo"` | Preset for YOLO or guardian-reviewed execution. |
| `approvalPolicy` | `"never"` | Native Codex approval policy sent to thread start/resume/turn. |
| `sandbox` | `"danger-full-access"` | Native Codex sandbox mode sent to thread start/resume. |
| `approvalsReviewer` | `"user"` | Use `"auto_review"` to let Codex review native approval prompts. `guardian_subagent` remains a legacy alias. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. |
| Field | Default | Meaning |
| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. `CODEX_HOME` and `HOME` are reserved for OpenClaw's per-agent Codex isolation on local launches. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after a turn-scoped Codex app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. |
OpenClaw-owned dynamic tool calls are bounded independently from
`appServer.requestTimeoutMs`: each Codex `item/tool/call` request must receive

View File

@@ -12,9 +12,15 @@ import {
resolveCodexPluginsPolicy,
} from "./config.js";
type RuntimeOptionsParams = NonNullable<Parameters<typeof resolveCodexAppServerRuntimeOptions>[0]>;
function resolveRuntimeForTest(params: RuntimeOptionsParams = {}) {
return resolveCodexAppServerRuntimeOptions({ env: {}, requirementsToml: null, ...params });
}
describe("Codex app-server config", () => {
it("parses typed plugin config before falling back to environment knobs", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {
appServer: {
mode: "guardian",
@@ -51,7 +57,7 @@ describe("Codex app-server config", () => {
});
it("ignores app-server environment clearing for websocket transports", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {
appServer: {
transport: "websocket",
@@ -66,7 +72,7 @@ describe("Codex app-server config", () => {
});
it("normalizes app-server environment variables to clear", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {
appServer: {
clearEnv: [" OPENAI_API_KEY ", "", " "],
@@ -83,7 +89,7 @@ describe("Codex app-server config", () => {
});
it("normalizes legacy service tiers without discarding the rest of the config", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {
appServer: {
mode: "guardian",
@@ -130,7 +136,7 @@ describe("Codex app-server config", () => {
it("requires a websocket url when websocket transport is configured", () => {
expect(() =>
resolveCodexAppServerRuntimeOptions({
resolveRuntimeForTest({
pluginConfig: { appServer: { transport: "websocket" } },
env: {},
}),
@@ -138,9 +144,8 @@ describe("Codex app-server config", () => {
});
it("defaults native Codex approvals to unchained local execution", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {},
env: {},
});
expect(runtime).toEqual(
@@ -156,6 +161,298 @@ describe("Codex app-server config", () => {
);
});
it("defaults native Codex approvals to guardian when requirements disallow full access", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: 'allowed_sandbox_modes = ["read-only", "workspace-write"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("uses read-only sandbox for guardian defaults when requirements only allow read-only", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: 'allowed_sandbox_modes = ["read-only"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "read-only",
approvalsReviewer: "auto_review",
}),
);
});
it("defaults native Codex approvals to guardian when requirements disallow never approval", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: 'allowed_approval_policies = ["on-request"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("selects an allowed guardian approval policy when on-request is unavailable", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: 'allowed_approval_policies = ["on-failure"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-failure",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("keeps native Codex approvals unchained when requirements allow never approval", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: 'allowed_approval_policies = ["never"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "never",
sandbox: "danger-full-access",
approvalsReviewer: "user",
}),
);
});
it("defaults native Codex approvals to guardian when requirements disallow user reviewer", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: 'allowed_approvals_reviewers = ["auto_review"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("selects an allowed reviewer when sandbox requirements force guardian defaults", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml:
'allowed_sandbox_modes = ["read-only", "workspace-write"]\nallowed_approvals_reviewers = ["user"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "user",
}),
);
});
it("ignores quoted sandbox modes inside requirements comments", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: `allowed_sandbox_modes = [
"read-only",
# "danger-full-access",
"workspace-write",
]
`,
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("applies the first matching remote sandbox requirements before resolving local stdio defaults", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
hostName: "BUILD-01.EXAMPLE.COM.",
requirementsToml: `[[remote_sandbox_config]]
hostname_patterns = ["build-*.example.com"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
[[remote_sandbox_config]]
hostname_patterns = ["build-01.example.com"]
allowed_sandbox_modes = ["read-only", "danger-full-access"]
`,
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("ignores non-matching remote-only sandbox requirements when resolving local stdio defaults", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
hostName: "laptop.example.com",
requirementsToml: `[[remote_sandbox_config]]
hostname_patterns = ["build-*.example.com"]
allowed_sandbox_modes = ["read-only", "workspace-write"]
`,
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "never",
sandbox: "danger-full-access",
approvalsReviewer: "user",
}),
);
});
it("reads local requirements policy from the configured requirements path", () => {
const readPaths: string[] = [];
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: {},
env: {},
requirementsPath: "/custom/codex/requirements.toml",
readRequirementsFile: (path) => {
readPaths.push(path);
return 'allowed_sandbox_modes = ["read-only", "workspace-write"]\n';
},
});
expect(readPaths).toEqual(["/custom/codex/requirements.toml"]);
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("reads local requirements policy from the Codex Windows requirements path", () => {
const readPaths: string[] = [];
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: {},
env: { ProgramData: "D:\\ManagedData" },
platform: "win32",
readRequirementsFile: (path) => {
readPaths.push(path);
return 'allowed_sandbox_modes = ["read-only", "workspace-write"]\n';
},
});
expect(readPaths).toEqual(["D:\\ManagedData\\OpenAI\\Codex\\requirements.toml"]);
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "workspace-write",
approvalsReviewer: "auto_review",
}),
);
});
it("keeps native Codex approvals unchained when requirements allow full access", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml:
'allowed_sandbox_modes = ["ReadOnly", "WorkspaceWrite", "DangerFullAccess"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "never",
sandbox: "danger-full-access",
approvalsReviewer: "user",
}),
);
});
it("keeps native Codex approvals unchained when requirements are malformed", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {},
requirementsToml: "allowed_sandbox_modes = [read-only]\n",
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "never",
sandbox: "danger-full-access",
approvalsReviewer: "user",
}),
);
});
it("does not apply local requirements policy to websocket app-server transports", () => {
const runtime = resolveRuntimeForTest({
pluginConfig: {
appServer: {
transport: "websocket",
url: "ws://127.0.0.1:39175",
},
},
requirementsToml: 'allowed_sandbox_modes = ["read-only", "workspace-write"]\n',
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "never",
sandbox: "danger-full-access",
approvalsReviewer: "user",
}),
);
});
it("keeps explicit yolo mode when requirements disallow full access", () => {
const requirementsToml = 'allowed_sandbox_modes = ["read-only", "workspace-write"]\n';
expect(
resolveRuntimeForTest({
pluginConfig: { appServer: { mode: "yolo" } },
requirementsToml,
}),
).toEqual(
expect.objectContaining({
approvalPolicy: "never",
sandbox: "danger-full-access",
approvalsReviewer: "user",
}),
);
expect(
resolveRuntimeForTest({
pluginConfig: {},
env: { OPENCLAW_CODEX_APP_SERVER_MODE: "yolo" },
requirementsToml,
}),
).toEqual(
expect.objectContaining({
approvalPolicy: "never",
sandbox: "danger-full-access",
approvalsReviewer: "user",
}),
);
});
it("parses dynamic tool profile controls", () => {
expect(
readCodexPluginConfig({
@@ -237,7 +534,7 @@ describe("Codex app-server config", () => {
it("treats configured and environment commands as explicit overrides", () => {
expect(
resolveCodexAppServerRuntimeOptions({
resolveRuntimeForTest({
pluginConfig: { appServer: { command: "/opt/codex/bin/codex" } },
env: { OPENCLAW_CODEX_APP_SERVER_BIN: "/usr/local/bin/codex" },
}).start,
@@ -249,7 +546,7 @@ describe("Codex app-server config", () => {
);
expect(
resolveCodexAppServerRuntimeOptions({
resolveRuntimeForTest({
pluginConfig: {},
env: { OPENCLAW_CODEX_APP_SERVER_BIN: "/usr/local/bin/codex" },
}).start,
@@ -304,7 +601,7 @@ describe("Codex app-server config", () => {
});
it("allows plugin config to opt in to guardian-reviewed local execution", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {
appServer: {
mode: "guardian",
@@ -323,7 +620,7 @@ describe("Codex app-server config", () => {
});
it("allows environment mode fallback to opt in to guardian-reviewed local execution", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {},
env: { OPENCLAW_CODEX_APP_SERVER_MODE: "guardian" },
});
@@ -339,13 +636,13 @@ describe("Codex app-server config", () => {
it("accepts the latest auto_review reviewer and legacy guardian_subagent alias", () => {
expect(
resolveCodexAppServerRuntimeOptions({
resolveRuntimeForTest({
pluginConfig: { appServer: { approvalsReviewer: "auto_review" } },
env: {},
}).approvalsReviewer,
).toBe("auto_review");
expect(
resolveCodexAppServerRuntimeOptions({
resolveRuntimeForTest({
pluginConfig: { appServer: { approvalsReviewer: "guardian_subagent" } },
env: {},
}).approvalsReviewer,
@@ -353,7 +650,7 @@ describe("Codex app-server config", () => {
});
it("ignores removed OPENCLAW_CODEX_APP_SERVER_GUARDIAN fallback", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {},
env: { OPENCLAW_CODEX_APP_SERVER_GUARDIAN: "1" },
});
@@ -368,7 +665,7 @@ describe("Codex app-server config", () => {
});
it("lets explicit policy fields override guardian mode", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
const runtime = resolveRuntimeForTest({
pluginConfig: {
appServer: {
mode: "guardian",

View File

@@ -1,11 +1,21 @@
import { createHmac, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";
import { hostname as readHostName } from "node:os";
import { z } from "openclaw/plugin-sdk/zod";
import type { CodexSandboxPolicy, CodexServiceTier } from "./protocol.js";
const START_OPTIONS_KEY_SECRET = randomBytes(32);
const UNIX_CODEX_REQUIREMENTS_PATH = "/etc/codex/requirements.toml";
const WINDOWS_CODEX_REQUIREMENTS_SUFFIX = "\\OpenAI\\Codex\\requirements.toml";
type CodexAppServerTransportMode = "stdio" | "websocket";
type CodexAppServerPolicyMode = "yolo" | "guardian";
type CodexAppServerDefaultPolicy = {
mode: CodexAppServerPolicyMode;
approvalPolicy?: CodexAppServerApprovalPolicy;
approvalsReviewer?: CodexAppServerApprovalsReviewer;
sandbox?: CodexAppServerSandboxMode;
};
export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure" | "untrusted";
export type CodexAppServerEffectiveApprovalPolicy =
| CodexAppServerApprovalPolicy
@@ -305,6 +315,11 @@ export function resolveCodexAppServerRuntimeOptions(
params: {
pluginConfig?: unknown;
env?: NodeJS.ProcessEnv;
requirementsToml?: string | null;
requirementsPath?: string;
readRequirementsFile?: (path: string) => string | undefined;
platform?: NodeJS.Platform;
hostName?: string;
} = {},
): CodexAppServerRuntimeOptions {
const env = params.env ?? process.env;
@@ -323,10 +338,20 @@ export function resolveCodexAppServerRuntimeOptions(
const clearEnv = normalizeStringList(config.clearEnv);
const authToken = readNonEmptyString(config.authToken);
const url = readNonEmptyString(config.url);
const policyMode =
resolvePolicyMode(config.mode) ??
resolvePolicyMode(env.OPENCLAW_CODEX_APP_SERVER_MODE) ??
"yolo";
const explicitPolicyMode =
resolvePolicyMode(config.mode) ?? resolvePolicyMode(env.OPENCLAW_CODEX_APP_SERVER_MODE);
const defaultPolicy = explicitPolicyMode
? undefined
: resolveDefaultCodexAppServerPolicy({
transport,
env,
requirementsToml: params.requirementsToml,
requirementsPath: params.requirementsPath,
readRequirementsFile: params.readRequirementsFile,
platform: params.platform,
hostName: params.hostName,
});
const policyMode = explicitPolicyMode ?? defaultPolicy?.mode ?? "yolo";
const serviceTier = normalizeCodexServiceTier(config.serviceTier);
if (transport === "websocket" && !url) {
throw new Error(
@@ -353,13 +378,16 @@ export function resolveCodexAppServerRuntimeOptions(
approvalPolicy:
resolveApprovalPolicy(config.approvalPolicy) ??
resolveApprovalPolicy(env.OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY) ??
defaultPolicy?.approvalPolicy ??
(policyMode === "guardian" ? "on-request" : "never"),
sandbox:
resolveSandbox(config.sandbox) ??
resolveSandbox(env.OPENCLAW_CODEX_APP_SERVER_SANDBOX) ??
defaultPolicy?.sandbox ??
(policyMode === "guardian" ? "workspace-write" : "danger-full-access"),
approvalsReviewer:
resolveApprovalsReviewer(config.approvalsReviewer) ??
defaultPolicy?.approvalsReviewer ??
(policyMode === "guardian" ? "auto_review" : "user"),
...(serviceTier ? { serviceTier } : {}),
};
@@ -502,6 +530,333 @@ function resolvePolicyMode(value: unknown): CodexAppServerPolicyMode | undefined
return value === "guardian" || value === "yolo" ? value : undefined;
}
function resolveDefaultCodexAppServerPolicy(params: {
transport: CodexAppServerTransportMode;
env?: NodeJS.ProcessEnv;
requirementsToml?: string | null;
requirementsPath?: string;
readRequirementsFile?: (path: string) => string | undefined;
platform?: NodeJS.Platform;
hostName?: string;
}): CodexAppServerDefaultPolicy {
if (params.transport !== "stdio") {
return { mode: "yolo" };
}
const content = readCodexRequirementsToml(params);
if (content === undefined) {
return { mode: "yolo" };
}
const allowedSandboxModes = parseAllowedSandboxModesFromCodexRequirements(
content,
readNonEmptyString(params.hostName) ?? readHostName(),
);
const allowedApprovalPolicies = parseAllowedApprovalPoliciesFromCodexRequirements(content);
const allowedApprovalsReviewers = parseAllowedApprovalsReviewersFromCodexRequirements(content);
const yoloSandboxAllowed =
allowedSandboxModes === undefined || allowedSandboxModes.has("danger-full-access");
const yoloApprovalAllowed =
allowedApprovalPolicies === undefined || allowedApprovalPolicies.has("never");
const yoloReviewerAllowed =
allowedApprovalsReviewers === undefined || allowedApprovalsReviewers.has("user");
if (yoloSandboxAllowed && yoloApprovalAllowed && yoloReviewerAllowed) {
return { mode: "yolo" };
}
return {
mode: "guardian",
approvalPolicy: selectGuardianApprovalPolicy(allowedApprovalPolicies),
approvalsReviewer: selectGuardianApprovalsReviewer(allowedApprovalsReviewers),
sandbox: selectGuardianSandbox(allowedSandboxModes),
};
}
function readCodexRequirementsToml(params: {
env?: NodeJS.ProcessEnv;
requirementsToml?: string | null;
requirementsPath?: string;
readRequirementsFile?: (path: string) => string | undefined;
platform?: NodeJS.Platform;
}): string | undefined {
if (params.requirementsToml !== undefined) {
return params.requirementsToml ?? undefined;
}
const path =
readNonEmptyString(params.requirementsPath) ??
resolveCodexRequirementsPath(params.env ?? process.env, params.platform ?? process.platform);
try {
if (params.readRequirementsFile) {
return params.readRequirementsFile(path);
}
return readFileSync(path, "utf8");
} catch {
return undefined;
}
}
function resolveCodexRequirementsPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string {
if (platform === "win32") {
const programData = readNonEmptyString(env.ProgramData) ?? "C:\\ProgramData";
return `${programData.replace(/[\\/]+$/, "")}${WINDOWS_CODEX_REQUIREMENTS_SUFFIX}`;
}
return UNIX_CODEX_REQUIREMENTS_PATH;
}
function parseAllowedSandboxModesFromCodexRequirements(
content: string,
hostName: string,
): Set<CodexAppServerSandboxMode> | undefined {
const remoteSandboxModes = parseMatchingRemoteSandboxModesFromCodexRequirements(
content,
hostName,
);
if (remoteSandboxModes !== undefined) {
return remoteSandboxModes;
}
const values = parseTopLevelRequirementsStringArray(content, "allowed_sandbox_modes");
return parseRequirementsSandboxModes(values);
}
function parseAllowedApprovalPoliciesFromCodexRequirements(
content: string,
): Set<CodexAppServerApprovalPolicy> | undefined {
const values = parseTopLevelRequirementsStringArray(content, "allowed_approval_policies");
if (values === undefined) {
return undefined;
}
const normalizedPolicies = values
.map((entry) => normalizeRequirementsApprovalPolicy(entry))
.filter((entry): entry is CodexAppServerApprovalPolicy => entry !== undefined);
return normalizedPolicies.length > 0 ? new Set(normalizedPolicies) : undefined;
}
function parseAllowedApprovalsReviewersFromCodexRequirements(
content: string,
): Set<CodexAppServerApprovalsReviewer> | undefined {
const values = parseTopLevelRequirementsStringArray(content, "allowed_approvals_reviewers");
if (values === undefined) {
return undefined;
}
const normalizedReviewers = values
.map((entry) => normalizeRequirementsApprovalsReviewer(entry))
.filter((entry): entry is CodexAppServerApprovalsReviewer => entry !== undefined);
return normalizedReviewers.length > 0 ? new Set(normalizedReviewers) : undefined;
}
function parseMatchingRemoteSandboxModesFromCodexRequirements(
content: string,
hostName: string,
): Set<CodexAppServerSandboxMode> | undefined {
const normalizedHostName = normalizeRequirementsHostName(hostName);
if (normalizedHostName === undefined) {
return undefined;
}
for (const section of parseTomlArrayTableSections(content, "remote_sandbox_config")) {
const patterns = parseRequirementsStringArray(section, "hostname_patterns");
if (!patterns || !requirementsHostNameMatchesAnyPattern(normalizedHostName, patterns)) {
continue;
}
return parseRequirementsSandboxModes(
parseRequirementsStringArray(section, "allowed_sandbox_modes"),
);
}
return undefined;
}
function parseRequirementsSandboxModes(
values: string[] | undefined,
): Set<CodexAppServerSandboxMode> | undefined {
if (values === undefined) {
return undefined;
}
const normalizedModes = values
.map((entry) => normalizeRequirementsSandboxMode(entry))
.filter((entry): entry is CodexAppServerSandboxMode => entry !== undefined);
return normalizedModes.length > 0 ? new Set(normalizedModes) : undefined;
}
function parseTopLevelRequirementsStringArray(content: string, key: string): string[] | undefined {
const topLevelContent = stripTomlLineComments(content).slice(0, firstTomlTableOffset(content));
return parseRequirementsStringArray(topLevelContent, key);
}
function parseRequirementsStringArray(content: string, key: string): string[] | undefined {
const match = content.match(new RegExp(`(?:^|\\n)\\s*${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`));
if (!match) {
return undefined;
}
const arrayBody = match[1] ?? "";
const stringMatches = [...arrayBody.matchAll(/"([^"\\]*(?:\\.[^"\\]*)*)"|'([^']*)'/g)];
if (stringMatches.length === 0 && arrayBody.trim().length > 0) {
return undefined;
}
return stringMatches.map((entry) => entry[1] ?? entry[2] ?? "");
}
function parseTomlArrayTableSections(content: string, table: string): string[] {
const strippedContent = stripTomlLineComments(content);
const escapedTable = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const headerPattern = new RegExp(`^\\s*\\[\\[\\s*${escapedTable}\\s*\\]\\]\\s*$`, "gm");
const sections: string[] = [];
for (
let match = headerPattern.exec(strippedContent);
match;
match = headerPattern.exec(strippedContent)
) {
const sectionStart = headerPattern.lastIndex;
const rest = strippedContent.slice(sectionStart);
const nextTableOffset = rest.search(/^\s*\[/m);
sections.push(nextTableOffset === -1 ? rest : rest.slice(0, nextTableOffset));
}
return sections;
}
function firstTomlTableOffset(content: string): number {
const match = content.match(/^\s*\[[^\]\n]/m);
return match?.index ?? content.length;
}
function stripTomlLineComments(value: string): string {
let output = "";
let quote: '"' | "'" | undefined;
let escaped = false;
for (let index = 0; index < value.length; index += 1) {
const char = value[index] ?? "";
if (quote) {
output += char;
if (quote === '"' && escaped) {
escaped = false;
continue;
}
if (quote === '"' && char === "\\") {
escaped = true;
continue;
}
if (char === quote) {
quote = undefined;
}
continue;
}
if (char === '"' || char === "'") {
quote = char;
output += char;
continue;
}
if (char === "#") {
while (index < value.length && value[index] !== "\n") {
index += 1;
}
if (value[index] === "\n") {
output += "\n";
}
continue;
}
output += char;
}
return output;
}
function normalizeRequirementsSandboxMode(value: string): CodexAppServerSandboxMode | undefined {
const compact = value.replace(/[\s_-]/g, "").toLowerCase();
if (compact === "readonly") {
return "read-only";
}
if (compact === "workspacewrite") {
return "workspace-write";
}
if (compact === "dangerfullaccess") {
return "danger-full-access";
}
return undefined;
}
function normalizeRequirementsHostName(value: string): string | undefined {
const normalized = value.trim().replace(/\.+$/g, "").toLowerCase();
return normalized.length > 0 ? normalized : undefined;
}
function requirementsHostNameMatchesAnyPattern(hostName: string, patterns: string[]): boolean {
return patterns.some((pattern) => {
const normalizedPattern = normalizeRequirementsHostName(pattern);
return normalizedPattern !== undefined && globPatternMatches(hostName, normalizedPattern);
});
}
function globPatternMatches(value: string, pattern: string): boolean {
let regex = "^";
for (const char of pattern) {
if (char === "*") {
regex += ".*";
} else if (char === "?") {
regex += ".";
} else {
regex += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
}
regex += "$";
return new RegExp(regex).test(value);
}
function normalizeRequirementsApprovalPolicy(
value: string,
): CodexAppServerApprovalPolicy | undefined {
const normalized = value.trim().toLowerCase();
return resolveApprovalPolicy(normalized);
}
function normalizeRequirementsApprovalsReviewer(
value: string,
): CodexAppServerApprovalsReviewer | undefined {
const normalized = value.trim().toLowerCase();
return resolveApprovalsReviewer(normalized);
}
function selectGuardianApprovalPolicy(
allowedApprovalPolicies: Set<CodexAppServerApprovalPolicy> | undefined,
): CodexAppServerApprovalPolicy {
if (allowedApprovalPolicies === undefined || allowedApprovalPolicies.has("on-request")) {
return "on-request";
}
if (allowedApprovalPolicies.has("on-failure")) {
return "on-failure";
}
if (allowedApprovalPolicies.has("untrusted")) {
return "untrusted";
}
if (allowedApprovalPolicies.has("never")) {
return "never";
}
return "on-request";
}
function selectGuardianApprovalsReviewer(
allowedApprovalsReviewers: Set<CodexAppServerApprovalsReviewer> | undefined,
): CodexAppServerApprovalsReviewer {
if (allowedApprovalsReviewers === undefined || allowedApprovalsReviewers.has("auto_review")) {
return "auto_review";
}
if (allowedApprovalsReviewers.has("guardian_subagent")) {
return "guardian_subagent";
}
if (allowedApprovalsReviewers.has("user")) {
return "user";
}
return "auto_review";
}
function selectGuardianSandbox(
allowedSandboxModes: Set<CodexAppServerSandboxMode> | undefined,
): CodexAppServerSandboxMode {
if (allowedSandboxModes === undefined || allowedSandboxModes.has("workspace-write")) {
return "workspace-write";
}
if (allowedSandboxModes.has("read-only")) {
return "read-only";
}
if (allowedSandboxModes.has("danger-full-access")) {
return "danger-full-access";
}
return "workspace-write";
}
function resolveApprovalPolicy(value: unknown): CodexAppServerApprovalPolicy | undefined {
return value === "on-request" ||
value === "on-failure" ||

View File

@@ -674,8 +674,10 @@ describe("runCodexAppServerAttempt", () => {
params.sourceReplyDeliveryMode = "message_tool_only";
params.toolsAllow = ["message", "web_search", "heartbeat_respond"];
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start", 60_000);
const run = runCodexAppServerAttempt(params, {
pluginConfig: { appServer: { mode: "yolo" } },
});
await harness.waitForMethod("turn/start", 120_000);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -1953,6 +1955,7 @@ describe("runCodexAppServerAttempt", () => {
const { waitForMethod } = createStartedThreadHarness();
const run = runCodexAppServerAttempt(
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
{ pluginConfig: { appServer: { mode: "yolo" } } },
);
await waitForMethod("turn/start");
@@ -1974,6 +1977,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
{ pluginConfig: { appServer: { mode: "yolo" } } },
);
await waitForMethod("turn/start");
@@ -3107,7 +3111,9 @@ describe("runCodexAppServerAttempt", () => {
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
const { requests, waitForMethod, completeTurn } = createResumeHarness();
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), {
pluginConfig: { appServer: { mode: "yolo" } },
});
await waitForMethod("turn/start");
await completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;

View File

@@ -30,6 +30,7 @@ export function resolveCodexPromptSnapshotAppServerOptions(
return resolveCodexAppServerRuntimeOptions({
pluginConfig,
env: {},
requirementsToml: null,
});
}