Skip to content

Harden Codex CUA facade isolation, evidence, and tool logging - #2907

Open
miguelg719 wants to merge 1 commit into
evals/consolidation-17-gemini-cuafrom
evals/consolidation-18-codex-cua
Open

Harden Codex CUA facade isolation, evidence, and tool logging#2907
miguelg719 wants to merge 1 commit into
evals/consolidation-17-gemini-cuafrom
evals/consolidation-18-codex-cua

Conversation

@miguelg719

@miguelg719 miguelg719 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Stack

Top-of-stack child of #2906 (evals/consolidation-17-gemini-cua).
Review against that immediate parent, not main.

Summary

Port the remaining focused Codex/Stagehand facade hardening from the experimental
Codex worktree without replacing the consolidation stack's newer shared runtime.

  • Add opt-in facade JSONL tool logging: request/session IDs, actual arguments/code,
    paired starts/ends, timing, errors, bounded result previews and browser readiness.
    Never write logs to MCP stdout; redact known credentials and omit image payloads.
  • Escape Unicode line separators in text tool results, addressing the stream
    parsing failure observed in the Hostelworld benchmark traces.
  • Share isolated HOME/CODEX_HOME creation between the SDK example and evals;
    do not inherit operator plugins/config/thread context. Preserve file-based auth.
  • Abort unexpected MCP servers, await observation capture, and match evidence by
    tool-call ID so unrelated calls/missing final captures do not shift screenshots.
  • Add optional bounded SDK failure artifacts and keep binary payloads out of
    telemetry without mutating original events.
  • Document the difference between Codex event logs and facade server logs, privacy
    limitations, authentication, and runnable logging commands.

This remains the existing Codex SDK harness with the Stagehand facade, not a new
native OpenAI computer-use adapter. Preserve the parent's terminal session-loss
semantics, shared facade API, HardBench dataset, verifier and usage contracts.
The older experimental state/nodeRepl/reset surface and reconnect implementation
are not copied wholesale into the newer shared runtime. The original dirty
worktree remains untouched.

Validation

  • 220 focused tests pass across 23 files (core facade, Codex SDK, eval adapters).
  • 2 standalone Codex example tests pass.
  • Built stdio regression verifies the JSONL file is created and contains paired
    request IDs and error results while the MCP client remains responsive.
  • Integration packages and eval ESM/CLI builds; core/Codex/example/evals typechecks.
  • Targeted lint: no errors; warnings remain in existing patterns and diagnostic
    stringification. git diff --check passes.
  • No paid model or HardBenchmark rerun; no benchmark score improvement claimed.

Operational notes

Logging remains opt-in. Logs/error artifacts may contain sensitive model/page
content despite best-effort redaction and must be reviewed before sharing.
Unexpected-server detection aborts observed calls; it is not a security sandbox.
Keychain-only login is not copied into isolated profiles.


Summary by cubic

Hardens Codex facade runs with isolated HOME/CODEX_HOME profiles, opt-in tool logging, and evidence keyed to tool-call IDs so unmatched or unrelated calls no longer misplace screenshots.

New Features

  • Adds opt-in JSONL facade tool logging with request/session IDs, arguments, timing, errors, and bounded result previews; logs go only to file or stderr, never MCP stdout, with known credentials redacted and image payloads omitted.
  • Saves bounded SDK failure artifacts to STAGEHAND_CODEX_DIAGNOSTICS_DIR or EVAL_CODEX_DIAGNOSTICS_DIR; a failed diagnostic write never masks the original SDK failure.
  • Shares isolated HOME/CODEX_HOME creation between the SDK example and evals; only file-based auth.json is copied, never plugins, config, or inherited thread context.

Bug Fixes

  • Matches probe evidence by tool-call ID so missing final captures no longer shift screenshots onto the wrong step.
  • Aborts on unexpected MCP servers (detection only, not a sandbox).
  • Escapes Unicode line separators in text tool results, fixing the stream parsing failure seen in Hostelworld traces.

Written for commit 024b0ed. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 024b0ed

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 23 files

Confidence score: 3/5

  • In packages/integrations/core/src/facade/logging.ts, invalid facade logging settings raise a generic Error during stdio startup, weakening the expected configuration-error handling; use the existing facade configuration error type.
  • In packages/integrations/core/src/facade/logging.ts, redactToolLog leaves image payloads of 256 characters or fewer unchanged, allowing image bytes into debug and error records; omit every data payload before applying length-based redaction.
  • In packages/integrations/codex-sdk/src/isolation.ts, duplicated environment filtering lets buildIsolatedCodexEnv and eval callers diverge, creating inconsistent isolated environments; centralize the filtering or have evals consume the returned environment.
  • In packages/integrations/codex/README.md, the raw CLI pipeline omits the task prompt, so the documented command cannot reproduce a run whose events are captured; include the instruction argument.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/integrations/core/src/facade/logging.ts">

<violation number="1" location="packages/integrations/core/src/facade/logging.ts:11">
P2: When an image payload is 256 characters or shorter, `redactToolLog` returns it unchanged, so debug and error records can write the image bytes despite the facade logging contract. Omit every `data` payload before applying the preview limit.</violation>

<violation number="2" location="packages/integrations/core/src/facade/logging.ts:34">
P2: Custom agent: **Exception and error message sanitization**

When either facade logging setting is invalid, `createFacadeLogger` raises a generic `Error` during stdio startup. Use the existing `StagehandFacadeConfigError` for both validation failures so callers can identify facade configuration errors consistently.</violation>
</file>

<file name="packages/integrations/codex-sdk/src/isolation.ts">

<violation number="1" location="packages/integrations/codex-sdk/src/isolation.ts:24">
P2: The new environment-filtering logic duplicates `buildIsolatedCodexEnv`, while eval callers discard this function's returned environment and reconstruct it separately. Centralize the filtering or have evals use the returned environment so future isolation changes cannot diverge between the SDK example and evals.</violation>
</file>

<file name="packages/integrations/codex/README.md">

<violation number="1" location="packages/integrations/codex/README.md:104">
P2: This raw CLI pipeline supplies no task prompt, unlike the complete `codex exec` example above, so it cannot reproduce a run whose events are being captured. Include the instruction argument in the command.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant C as Codex CLI/Agent
    participant SDK as Codex SDK Session
    participant FS as File System
    participant M as MCP Server (Stagehand Facade)
    participant TL as Tool Logger
    participant B as Stagehand Browser
    participant O as Observation Recorder
    participant E as Evals Harness

    Note over C,E: Isolated Codex Environment
    C->>SDK: runCodexSession(env, allowedMcpServers, diagnosticDir)
    SDK->>FS: create isolated HOME/CODEX_HOME
    SDK->>FS: copy auth.json only (no plugins/config/thread)
    SDK->>M: Connect MCP server (allowlist: stagehand)
    SDK->>SDK: Initialize thread (read-only sandbox, on-failure approval)

    Note over SDK,M: Tool Call Flow with Logging
    C->>SDK: Run streamed events
    SDK->>M: CallToolRequest (run/snapshot/screenshot)
    M->>TL: tool.start (requestId, name, args)
    TL->>TL: Redact credentials, omit images, bound size
    M->>B: Execute browser tool
    B-->>M: Result
    M->>TL: tool.end (durationMs, status, preview)
    TL->>TL: Write JSONL to file or stderr (never MCP stdout)
    M-->>SDK: Text result (escaped Unicode separators)

    Note over SDK,O: Event Handling & Policy
    SDK->>SDK: Check mcp_tool_call item
    alt Unexpected MCP server
        SDK->>SDK: Abort run, throw policy error
        SDK->>FS: Save diagnostic artifact (bounded)
    else Allowed MCP server
        SDK->>O: recordObservation(toolCallId) - await completion
        O->>O: Capture probe evidence (screenshot/url/ariaTree)
        O-->>SDK: Observation with toolCallId
    end

    Note over SDK,E: Evidence Matching
    SDK-->>E: Session result (events, observations)
    E->>E: Key evidence by toolCallId
    alt Tool call ID matches observation
        E->>E: Attach evidence to specific step
    else Missing/unrelated ID
        E->>E: Leave step without evidence (no misattribution)
    end

    Note over SDK,FS: Failure Handling
    SDK->>FS: Save diagnostics (best-effort, never masks original error)
    SDK-->>C: result (status, diagnosticPath)
    C->>FS: Optional diagnostics directory cleanup
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const file = env.STAGEHAND_FACADE_LOG_FILE;
const level = env.STAGEHAND_FACADE_LOG_LEVEL ?? (file ? "calls" : "off");
if (!["off", "calls", "debug"].includes(level))
throw new Error("STAGEHAND_FACADE_LOG_LEVEL must be off, calls, or debug.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Exception and error message sanitization

When either facade logging setting is invalid, createFacadeLogger raises a generic Error during stdio startup. Use the existing StagehandFacadeConfigError for both validation failures so callers can identify facade configuration errors consistently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integrations/core/src/facade/logging.ts, line 34:

<comment>When either facade logging setting is invalid, `createFacadeLogger` raises a generic `Error` during stdio startup. Use the existing `StagehandFacadeConfigError` for both validation failures so callers can identify facade configuration errors consistently.</comment>

<file context>
@@ -0,0 +1,115 @@
+  const file = env.STAGEHAND_FACADE_LOG_FILE;
+  const level = env.STAGEHAND_FACADE_LOG_LEVEL ?? (file ? "calls" : "off");
+  if (!["off", "calls", "debug"].includes(level))
+    throw new Error("STAGEHAND_FACADE_LOG_LEVEL must be off, calls, or debug.");
+  const parsedLimit = Number(env.STAGEHAND_FACADE_LOG_MAX_CHARS ?? 16_000);
+  if (!Number.isSafeInteger(parsedLimit) || parsedLimit < 256 || parsedLimit > 1_000_000)
</file context>

const env = Object.fromEntries(
Object.entries(source).filter(
([key, value]) =>
value !== undefined && (!key.startsWith("CODEX_") || key === "CODEX_API_KEY"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new environment-filtering logic duplicates buildIsolatedCodexEnv, while eval callers discard this function's returned environment and reconstruct it separately. Centralize the filtering or have evals use the returned environment so future isolation changes cannot diverge between the SDK example and evals.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integrations/codex-sdk/src/isolation.ts, line 24:

<comment>The new environment-filtering logic duplicates `buildIsolatedCodexEnv`, while eval callers discard this function's returned environment and reconstruct it separately. Centralize the filtering or have evals use the returned environment so future isolation changes cannot diverge between the SDK example and evals.</comment>

<file context>
@@ -0,0 +1,28 @@
+  const env = Object.fromEntries(
+    Object.entries(source).filter(
+      ([key, value]) =>
+        value !== undefined && (!key.startsWith("CODEX_") || key === "CODEX_API_KEY"),
+    ),
+  ) as Record<string, string>;
</file context>

JSON.stringify(value, (key, item: unknown) => {
if (/^(?:authorization|cookie|password|secret|token|api[_-]?key|signingKey)$/i.test(key))
return "[redacted]";
if (key === "data" && typeof item === "string" && item.length > 256)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an image payload is 256 characters or shorter, redactToolLog returns it unchanged, so debug and error records can write the image bytes despite the facade logging contract. Omit every data payload before applying the preview limit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integrations/core/src/facade/logging.ts, line 11:

<comment>When an image payload is 256 characters or shorter, `redactToolLog` returns it unchanged, so debug and error records can write the image bytes despite the facade logging contract. Omit every `data` payload before applying the preview limit.</comment>

<file context>
@@ -0,0 +1,115 @@
+    JSON.stringify(value, (key, item: unknown) => {
+      if (/^(?:authorization|cookie|password|secret|token|api[_-]?key|signingKey)$/i.test(key))
+        return "[redacted]";
+      if (key === "data" && typeof item === "string" && item.length > 256)
+        return `[binary omitted: ${item.length} characters]`;
+      if (typeof item === "string")
</file context>

but code, page text and typed values may remain sensitive. Review before sharing
and rotate files yourself; preview limits do not bound total file size.

Raw `codex exec --json | tee /tmp/codex-events.jsonl` only saves Codex events.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This raw CLI pipeline supplies no task prompt, unlike the complete codex exec example above, so it cannot reproduce a run whose events are being captured. Include the instruction argument in the command.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integrations/codex/README.md, line 104:

<comment>This raw CLI pipeline supplies no task prompt, unlike the complete `codex exec` example above, so it cannot reproduce a run whose events are being captured. Include the instruction argument in the command.</comment>

<file context>
@@ -65,3 +65,48 @@ service worker — browser-side, never on your machine. Browserbase is the recom
+but code, page text and typed values may remain sensitive. Review before sharing
+and rotate files yourself; preview limits do not bound total file size.
+
+Raw `codex exec --json | tee /tmp/codex-events.jsonl` only saves Codex events.
+It does not enable facade logging or the SDK example's isolated profile. Pass
+the `STAGEHAND_FACADE_LOG_*` variables in `mcp_servers.stagehand.env` when using
</file context>
Suggested change
Raw `codex exec --json | tee /tmp/codex-events.jsonl` only saves Codex events.
Raw `codex exec --json "your instruction" | tee /tmp/codex-events.jsonl` only saves Codex events.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant