Skip to content

Codex app-server: launch-input layer (MCP isolation, posture, hook-trust) - #578

Merged
realtonyyoung merged 3 commits into
mainfrom
tonyyoung/ai-1761-codex-appserver
Aug 18, 2026
Merged

realtonyyoung merged 3 commits into
mainfrom
tonyyoung/ai-1761-codex-appserver

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

First PR of the daemon-side Codex app-server migration (Closes #577; part 1 of 2 — the runtime that consumes these is the follow-up). Adds the pure, fully unit-tested launch-input pieces the CodexAppServerHostedAgentRuntime will use, reusing the PTY path's proven logic so nothing drifts between the two transports.

What's here

  • CodexAppServerPosture — renders the resolved (sandbox, approval) pair into codex app-server protocol JSON: sandboxPolicy objects (readOnly / workspaceWrite+writableRoots / dangerFullAccess), an approvalPolicy string validated against the three known kebab-case tokens, and the pinned approvalsReviewer="user". String-only policies (the granular object form crashes the app-server on turn/start) and the approvals-to-client pin (Guardian can never auto-approve a sandbox escalation) are enforced here.
  • CodexHookTrust — classifies a hooks/list snapshot: absence of a required critical event (SessionStart/Stop/PermissionRequest) fails closed (seeding can't conjure a hook), an untrusted kcap-owned hook is repaired by one seeded restart via a full-table -c hooks.state={…} override (the dotted-key form silently fails on path-shaped keys), a hook with no currentHash is unseedable. Seeds every untrusted kcap hook so the transcript hooks fire too.
  • CodexLauncher.BuildAppServerLaunchArgs — the codex app-server argv: --disable apps (the codex_apps ChatGPT-connector runtime is not an mcp_servers entry and bypasses the disable table) plus the shared MCP-isolation overrides. The isolation is factored into AppendMcpIsolationArgs used by both transports; the app-server variant additionally stamps default_tools_approval_mode="approve" on each whitelisted server (the first tool call otherwise raises an approval that wedges an unattended turn even under approvalPolicy: never; trust_level does not suppress it). Sandbox/approval/model/effort are per-turn protocol params on this transport, so none of the PTY-only flags appear.
  • CodexToml and CodexHooksParser.IsCapacitorCodexHookCommand extracted so the encoder and the kcap-hook marker have one shared implementation.

Safety

The PTY BuildArgs path is byte-identical (appServer: false) — the existing CodexLauncherTests isolation/argv suite stays green. 33 new unit tests cover the three additions.

Grounded by a completed protocol spike on pinned codex 0.146.0 (the four launcher requirements above were all surfaced/verified there).

Linear: AI-1761 (epic AI-1759).

Renders the resolved (sandbox, approval) pair into codex app-server protocol
JSON: sandboxPolicy objects (readOnly / workspaceWrite+writableRoots /
dangerFullAccess), an approvalPolicy string validated against the three known
kebab-case tokens, and the pinned approvalsReviewer="user". String-only approval
policies (the granular object form crashes the app-server on turn/start) and the
approvals-to-client pin (Guardian can never auto-approve a sandbox escalation)
are enforced here, mirroring how CodexLauncher renders the same pair into PTY argv.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 18, 2026

Copy link
Copy Markdown

AI-1761

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add Codex app-server launch-input safety layer

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds app-server posture rendering with fail-closed sandbox, approval, and reviewer constraints.
• Reuses PTY MCP isolation while disabling connectors and pre-approving whitelisted server tools.
• Classifies hook trust and safely seeds repairable kcap hooks before hosted turns.
Diagram

graph TD
  CTX["Launch Context"] --> LAUNCH["Codex Launcher"] --> MCP["MCP Isolation"] --> ARGS["Launch Args"] --> RUNTIME["App-server Runtime"]
  CTX --> POSTURE["Posture Renderer"] --> RUNTIME
  HOOKS["Hook Snapshot"] --> TRUST["Hook Trust"] --> RUNTIME
Loading
High-Level Assessment

The PR’s approach is appropriate: pure launch-input components isolate protocol-specific behavior while reusing the established PTY MCP and TOML logic. Duplicating isolation per transport or deferring hook validation into turn execution would increase drift and weaken fail-closed behavior.

Files changed (8) +531 / -38

Enhancement (3) +190 / -35
CodexAppServerPosture.csRender validated app-server posture parameters +47/-0

Render validated app-server posture parameters

• Adds protocol JSON rendering for read-only, workspace-write, and full-access sandboxes. It validates approval tokens and pins approval review to the user to prevent automatic escalation approval.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerPosture.cs

CodexHookTrust.csClassify and repair app-server hook trust +95/-0

Classify and repair app-server hook trust

• Adds fail-closed classification for missing, trusted, untrusted, and unseedable kcap hooks. Seedable hooks produce a TOML-escaped full-table trust override for a verified process restart.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexHookTrust.cs

CodexLauncher.csBuild isolated Codex app-server launch arguments +48/-35

Build isolated Codex app-server launch arguments

• Adds an app-server argv builder that disables connector apps and reuses PTY MCP isolation. Whitelisted app-server MCP tools are pre-approved, while PTY-only posture and display flags remain excluded.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexLauncher.cs

Refactor (2) +58 / -3
CodexHooksParser.csShare kcap Codex hook command detection +14/-3

Share kcap Codex hook command detection

• Extracts command-marker detection into a reusable helper. Both hooks.json preflight and app-server trust classification now recognize current and legacy kcap hook commands consistently.

src/Capacitor.Cli.Core/Harness/Codex/CodexHooksParser.cs

CodexToml.csCentralize TOML string encoding +44/-0

Centralize TOML string encoding

• Moves TOML basic-string escaping into a shared encoder. Launcher MCP overrides and hook trust state now use identical escaping rules.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexToml.cs

Tests (3) +283 / -0
CodexAppServerLaunchArgsTests.csTest app-server argument isolation +89/-0

Test app-server argument isolation

• Verifies connector disabling, inherited MCP isolation, server allowlisting, app-server tool pre-approval, and omission of PTY-only flags. It also protects the existing PTY path from app-server-specific arguments.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLaunchArgsTests.cs

CodexAppServerPostureTests.csTest app-server posture rendering +68/-0

Test app-server posture rendering

• Covers each sandbox JSON shape, writable-root behavior, accepted approval policies, invalid values, and the user-reviewer containment pin.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerPostureTests.cs

CodexHookTrustTests.csTest hook-trust classification and seeding +126/-0

Test hook-trust classification and seeding

• Covers trusted, missing, foreign, untrusted, and unseedable hook snapshots. It verifies case-insensitive critical events, transcript-hook seeding, full-table overrides, and TOML escaping.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHookTrustTests.cs

Adds the launch-input layer the app-server runtime consumes, reusing the PTY
path's proven logic:

- BuildAppServerLaunchArgs assembles the `codex app-server` argv: `--disable apps`
  (the codex_apps ChatGPT-connector runtime bypasses the mcp_servers disable
  table) plus the shared MCP-isolation overrides, factored into AppendMcpIsolationArgs
  used by both transports. On app-server it stamps default_tools_approval_mode=
  "approve" on each whitelisted server (the first tool call otherwise wedges an
  unattended turn even under approvalPolicy: never; trust_level does not suppress
  it). Sandbox/approval/model/effort are per-turn protocol params, not argv.
- CodexHookTrust classifies a hooks/list snapshot: absence of a required critical
  event fails closed, an untrusted kcap-owned hook is repaired by one seeded
  restart via a full-table `-c hooks.state={…}` override (the dotted-key form
  silently fails on path-shaped keys), a hook with no currentHash is unseedable.
- CodexToml (TOML basic-string encoder) and CodexHooksParser.IsCapacitorCodexHookCommand
  are extracted so the two transports and the hooks.json/hooks-list surfaces share
  one implementation.

The PTY BuildArgs path is byte-identical (appServer:false) — the existing
CodexLauncherTests isolation suite stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung
realtonyyoung force-pushed the tonyyoung/ai-1761-codex-appserver branch from e59160a to cde4f56 Compare August 18, 2026 11:47
@qodo-code-review

qodo-code-review Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Hook ownership marker spoofing ✓ Resolved 🐞 Bug ⛨ Security
Description
CodexHookTrust treats any command containing a kcap marker substring as kcap-owned, so a project
hook such as malicious-command # kcap hook --codex is selected for trust seeding. Because project
hooks are overlaid into hosted worktrees, an untrusted hook can have its current hash approved and
execute after restart.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexHookTrust.cs[59]

+        var kcapHooks = hooks.Where(h => CodexHooksParser.IsCapacitorCodexHookCommand(h.Command)).ToArray();
Evidence
The classifier selects and seeds every command accepted by the shared helper, but that helper uses
unrestricted Contains checks. The canonical installer writes the exact kcap hook --codex
command, and the launcher can overlay project-scoped hooks into an owned worktree, making
substring-based ownership unsafe for trust decisions.

src/Capacitor.Cli.Core/Harness/Codex/CodexHooksParser.cs[45-56]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexHookTrust.cs[58-81]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexLauncher.cs[88-108]
src/Capacitor.Cli/Commands/PluginCommand.cs[592-600]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The app-server trust classifier relies on substring matching to identify kcap-owned hooks. A foreign or malicious command can embed a recognized marker and be automatically trust-seeded.

## Issue Context
The installer emits exact command forms, while project-scoped `.codex/hooks.json` files can be copied into hosted worktrees. Tighten ownership detection to accept only the actual dispatcher command and supported legacy forms, with defensible whitespace or executable-path handling, rather than arbitrary containing strings.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/Codex/CodexHooksParser.cs[45-56]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexHookTrust.cs[58-81]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHookTrustTests.cs[93-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Isolation comments restate assertions ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The comments immediately preceding the isolation assertions merely paraphrase what the assertions
already express. Similar narration throughout this test adds maintenance noise without documenting
non-obvious rationale.
Code

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLaunchArgsTests.cs[48]

+        // Same isolation table as PTY: every inherited server disabled.
Evidence
PR Compliance ID 11 prohibits comments that restate obvious code. For example, `Same isolation table
as PTY: every inherited server disabled` is followed directly by assertions that verify each
inherited server is disabled, while Flow-result force-enabled merely labels the next assertion.

CLAUDE.md: Avoid Verbose or Redundant Comments; Comments Should Capture Non-Obvious 'Why' Only
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLaunchArgsTests.cs[48-52]
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLaunchArgsTests.cs[68-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several comments in `CodexAppServerLaunchArgsTests` restate the test names or immediately following assertions rather than explaining non-obvious rationale.

## Issue Context
PR Compliance ID 11 requires comments to capture non-obvious reasons and prohibits comments that paraphrase straightforward code behavior.

## Fix Focus Areas
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLaunchArgsTests.cs[7-11]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLaunchArgsTests.cs[48-52]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLaunchArgsTests.cs[68-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. MCP enumeration timeout bypassed ✗ Dismissed 🐞 Bug ☼ Reliability
Description
BuildAppServerLaunchArgs enters MCP enumeration whose synchronous stdout and stderr ReadToEnd
calls run before the bounded wait. A Codex subprocess that hangs or fills the other redirected pipe
can therefore block the daemon indefinitely instead of failing closed after 15 seconds.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexLauncher.cs[270]

+        if (ctx.IsReviewFlow) AppendMcpIsolationArgs(args, ctx, appServer: true);
Evidence
The new app-server builder calls the shared isolation pass, which invokes ReadInheritedMcpServers.
Its default implementation calls ListInheritedServers, where both blocking stream reads precede
WaitForExit(TimeoutMs), so the documented timeout cannot run until EOF has already been received.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexLauncher.cs[254-270]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexLauncher.cs[275-280]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexMcpInventory.cs[25-34]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexMcpInventory.cs[42-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Inherited MCP enumeration reads redirected process streams synchronously before applying its timeout, allowing the new app-server launch-argument path to hang indefinitely.

## Issue Context
Read stdout and stderr concurrently while waiting for process completion under the same timeout or cancellation token. On timeout, kill the process tree, finish draining safely, and throw `CodexReviewerMcpIsolationException` as required by the fail-closed contract.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexMcpInventory.cs[37-69]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexLauncher.cs[267-272]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexMcpInventoryTests.cs[13-88]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Daemon/Harness/Codex/CodexHookTrust.cs
Comment thread src/Capacitor.Cli.Daemon/Harness/Codex/CodexLauncher.cs Outdated
Qodo review:
- Hook ownership marker (Security): IsCapacitorCodexHookCommand matched a kcap
  marker as a substring, so a project hook like `evil # kcap codex-hook` — overlaid
  from an untrusted branch into a reviewer worktree — would be treated as kcap-owned
  and trust-seeded by the app-server classifier, then run (hooks fire outside the
  turn sandbox). Now matches the command's real executable (basename, so a
  path-qualified /usr/local/bin/kcap still matches) + exact dispatcher arguments;
  anything else fails closed (not kcap-owned → never seeded). Behavior-preserving
  for every installed form (kcap hook --codex / kcap codex-hook / kapacitor
  codex-hook); the hooks.json preflight suite stays green. Added spoof/path tests.
- Trimmed test comments that merely restated assertions (house rule), keeping only
  the non-obvious rationale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung
realtonyyoung merged commit 261bd86 into main Aug 18, 2026
6 checks passed
@realtonyyoung
realtonyyoung deleted the tonyyoung/ai-1761-codex-appserver branch August 18, 2026 12:38
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