fix: constrain Codex app-server sandbox

This commit is contained in:
Peter Steinberger
2026-05-11 16:26:03 +01:00
parent 68609ea3bd
commit 694f40fcee
6 changed files with 58 additions and 8 deletions

View File

@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
- Control UI/performance: scope Nodes polling to the active Nodes tab, debounce stale session-list reconciliation, and bound chat-side session refreshes so long-running dashboards avoid background reload churn. Thanks @BunsDev.
- Plugins/channels: explain bundled channel entry files that reach the legacy plugin loader as setup-runtime loader mismatches instead of generic missing-register failures. Thanks @chinar-amrutkar.
- Plugins/session-end: fire a typed `session_end` plugin hook with reason `shutdown` (or `restart` when a restart is expected) for every session that was still active when the gateway process stops. Previously SIGTERM/SIGINT/restart paths closed the gateway without enumerating active sessions, leaving downstream `session_end` plugins (e.g. claude-mem) with ghost rows accumulating across restarts. The new shutdown finalizer drains an in-memory tracker that is populated by `session_start` and forgotten by replace / reset / delete / compaction emitters, so previously-finalized sessions are never double-fired. The drain is bounded to a 2 s total budget so a slow plugin cannot block process exit. Adds `"shutdown"` and `"restart"` to `PluginHookSessionEndReason`. Fixes #57790. Thanks @pandadev66.
- Codex app-server: clamp Codex code-mode sandboxing to workspace-write when an OpenClaw sandbox is active, preventing Docker gateway socket access from becoming a danger-full-access Codex turn.
- Bonjour/Gateway: treat active ciao probing and fresh name-conflict renames as in-progress so the mDNS watchdog waits for probe settlement before retrying, preventing rapid re-advertise loops on Windows, WSL, and other multicast-hostile hosts. (#74778) Refs #74242. Thanks @fuller-stack-dev.
- Providers/MiniMax: send a minimal Anthropic-compatible user fallback when message conversion filters a turn to an empty payload, so MiniMax M2.7 no longer returns `chat content is empty` after tool-heavy sessions. Fixes #74589. Thanks @neeravmakwana and @DerekEXS.
- Tools/media: preserve implicit allow-all semantics from `tools.alsoAllow`-only policies when preconstructing built-in media generation and PDF tools, so configured media tools become live without forcing `tools.allow: ["*", ...]`. Fixes #77841. Thanks @trialanderrorstudios.

View File

@@ -98,6 +98,7 @@ If you deploy the OpenClaw Gateway itself as a Docker container, it orchestrates
- **Config requires host paths**: The `openclaw.json` `workspace` configuration MUST contain the **Host's absolute path** (e.g. `/home/user/.openclaw/workspaces`), not the internal Gateway container path. When OpenClaw asks the Docker daemon to spawn a sandbox, the daemon evaluates paths relative to the Host OS namespace, not the Gateway namespace.
- **FS bridge parity (identical volume map)**: The OpenClaw Gateway native process also writes heartbeat and bridge files to the `workspace` directory. Because the Gateway evaluates the exact same string (the host path) from within its own containerized environment, the Gateway deployment MUST include an identical volume map linking the host namespace natively (`-v /home/user/.openclaw:/home/user/.openclaw`).
- **Codex code mode**: When an OpenClaw sandbox is active, OpenClaw constrains Codex app-server turns to Codex `workspace-write` sandboxing even if the Codex plugin default is `danger-full-access`. Do not mount the host Docker socket into agent sandbox containers or custom Codex sandboxes.
If you map paths internally without absolute host parity, OpenClaw natively throws an `EACCES` permission error attempting to write its heartbeat inside the container environment because the fully qualified path string doesn't exist natively.
</Warning>

View File

@@ -314,7 +314,9 @@ See [ClawDock](/install/clawdock) for the full helper guide.
The script mounts `docker.sock` only after sandbox prerequisites pass. If
sandbox setup cannot complete, the script resets `agents.defaults.sandbox.mode`
to `off`.
to `off`. Codex code-mode turns are still constrained to Codex
`workspace-write` while the OpenClaw sandbox is active; do not mount the
host Docker socket into agent sandbox containers.
</Accordion>

View File

@@ -323,6 +323,9 @@ Local stdio app-server sessions default to the trusted local operator posture:
`approvalPolicy: "never"`, `approvalsReviewer: "user"`, and
`sandbox: "danger-full-access"`. If local Codex requirements disallow that
implicit YOLO posture, OpenClaw selects allowed guardian permissions instead.
When an OpenClaw sandbox is active for the session, OpenClaw narrows Codex
`danger-full-access` to Codex `workspace-write` so native Codex code-mode turns
stay inside the sandboxed workspace.
Use guardian mode when you want Codex native auto-review before sandbox escapes
or extra permissions:
@@ -482,7 +485,7 @@ Supported `appServer` fields:
| `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"`. |
| `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"`. When an OpenClaw sandbox is active, `danger-full-access` is narrowed to `"workspace-write"`. |
| `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"`. |

View File

@@ -4660,6 +4660,34 @@ describe("runCodexAppServerAttempt", () => {
expect(turnRequestParams?.model).toBe("gpt-5.4-codex");
});
it("clamps Codex danger-full-access when OpenClaw sandboxing is active", () => {
const appServer = resolveCodexAppServerRuntimeOptions({
pluginConfig: {
appServer: {
approvalPolicy: "never",
sandbox: "danger-full-access",
},
},
});
const sandboxed = __testing.restrictCodexAppServerSandboxForOpenClawSandbox(appServer, {
enabled: true,
} as never);
expect(sandboxed).not.toBe(appServer);
expect(sandboxed.approvalPolicy).toBe("never");
expect(sandboxed.sandbox).toBe("workspace-write");
expect(__testing.restrictCodexAppServerSandboxForOpenClawSandbox(appServer, null)).toBe(
appServer,
);
expect(
__testing.restrictCodexAppServerSandboxForOpenClawSandbox(
{ ...appServer, sandbox: "read-only" },
{ enabled: true } as never,
).sandbox,
).toBe("read-only");
});
it("passes current Codex service tier request values through app-server resume and turn requests", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");

View File

@@ -430,6 +430,19 @@ function resolveCodexPluginAppCacheCodexHome(
return appServer.start.transport === "stdio" ? resolveCodexAppServerHomeDir(agentDir) : undefined;
}
function restrictCodexAppServerSandboxForOpenClawSandbox(
appServer: CodexAppServerRuntimeOptions,
sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>,
): CodexAppServerRuntimeOptions {
if (!sandbox?.enabled || appServer.sandbox !== "danger-full-access") {
return appServer;
}
return {
...appServer,
sandbox: "workspace-write",
};
}
export async function runCodexAppServerAttempt(
params: EmbeddedRunAttemptParams,
options: {
@@ -449,12 +462,7 @@ export async function runCodexAppServerAttempt(
const attemptStartedAt = Date.now();
const attemptClientFactory = resolveCodexAppServerClientFactory();
const pluginConfig = readCodexPluginConfig(options.pluginConfig);
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig });
let pluginAppServer: CodexAppServerRuntimeOptions = appServer;
const nativeHookRelayEvents = resolveCodexNativeHookRelayEvents({
configuredEvents: options.nativeHookRelay?.events,
appServer,
});
const configuredAppServer = resolveCodexAppServerRuntimeOptions({ pluginConfig });
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
await fs.mkdir(resolvedWorkspace, { recursive: true });
const sandboxSessionKey =
@@ -470,6 +478,12 @@ export async function runCodexAppServerAttempt(
: sandbox.workspaceDir
: resolvedWorkspace;
await fs.mkdir(effectiveWorkspace, { recursive: true });
const appServer = restrictCodexAppServerSandboxForOpenClawSandbox(configuredAppServer, sandbox);
let pluginAppServer: CodexAppServerRuntimeOptions = appServer;
const nativeHookRelayEvents = resolveCodexNativeHookRelayEvents({
configuredEvents: options.nativeHookRelay?.events,
appServer,
});
const runAbortController = new AbortController();
const abortFromUpstream = () => {
@@ -2955,6 +2969,7 @@ export const __testing = {
handleDynamicToolCallWithTimeout,
resolveDynamicToolCallTimeoutMs,
resolveCodexPluginAppCacheEndpoint,
restrictCodexAppServerSandboxForOpenClawSandbox,
resolveOpenClawCodingToolsSessionKeys,
shouldForceMessageTool,
setOpenClawCodingToolsFactoryForTests(factory: OpenClawCodingToolsFactory): void {