# Direct architecture and boundary inventory

Code Mode remains outside this package. See [ADR 0001: Keep Code Mode in a standalone MCP process](docs/adr/0001-standalone-code-mode-mcp.md).

## Design intent

This package is a thin transport adapter to the official signed Computer Use tools. It has no independent policy or security role. Codex and macOS remain authoritative for tool behavior, access decisions, and platform controls.

Preserve the official capability surface. Add adapter code only when the transport, zero-model-turn architecture, component compatibility, or process lifecycle requires it.

Canonical app resolution, same-app locking, focus completion telemetry, and metadata-only audit came from the earlier background-computer-use wrapper. The current release retains them for compatibility. They do not provide extra authorization or sandbox boundaries. This inventory does not justify extending them. A maintainer must explicitly approve their removal or material simplification.

## Official API evidence

The architecture uses public source and the installed signed binaries; it does not speak a private Computer Use socket protocol.

At OpenAI Codex source commit [`5c19155cbd93bfa099016e7487259f61669823ff`](https://github.com/openai/codex/tree/5c19155cbd93bfa099016e7487259f61669823ff):

- the app-server documentation describes initialization, ephemeral threads, and the distinct `turn/start` operation that begins model work ([README lines 74–81](https://github.com/openai/codex/blob/5c19155cbd93bfa099016e7487259f61669823ff/codex-rs/app-server/README.md#L74-L81));
- it documents `mcpServer/tool/call` as a direct call to a configured MCP tool ([README line 238](https://github.com/openai/codex/blob/5c19155cbd93bfa099016e7487259f61669823ff/codex-rs/app-server/README.md#L238));
- the typed request contains `thread_id`, `server`, `tool`, and `arguments`—not a prompt or model request ([protocol lines 97–110](https://github.com/openai/codex/blob/5c19155cbd93bfa099016e7487259f61669823ff/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs#L97-L110));
- the implementation loads the thread runtime and invokes `thread.call_mcp_tool(...)` directly ([processor lines 455–474](https://github.com/openai/codex/blob/5c19155cbd93bfa099016e7487259f61669823ff/codex-rs/app-server/src/request_processors/mcp_processor.rs#L455-L474));
- downstream structured elicitation is an explicit client capability rather than implicit approval ([README lines 89–94](https://github.com/openai/codex/blob/5c19155cbd93bfa099016e7487259f61669823ff/codex-rs/app-server/README.md#L89-L94)).

The Full access behavior is also public in the pinned Codex `rust-v0.144.2` source at commit [`a6645b6b8a656360fa16fb7e1c6721d0697d3d6a`](https://github.com/openai/codex/tree/a6645b6b8a656360fa16fb7e1c6721d0697d3d6a):

- legacy `danger-full-access` maps to `PermissionProfile::Disabled` ([models lines 289–297](https://github.com/openai/codex/blob/a6645b6b8a656360fa16fb7e1c6721d0697d3d6a/codex-rs/protocol/src/models.rs#L289-L297));
- MCP permission prompts are auto-approved when the approval policy is `Never` and the permission profile is disabled ([MCP policy lines 76–99](https://github.com/openai/codex/blob/a6645b6b8a656360fa16fb7e1c6721d0697d3d6a/codex-rs/codex-mcp/src/mcp/mod.rs#L76-L99));
- the elicitation manager applies that policy before client notification ([elicitation lines 196–221](https://github.com/openai/codex/blob/a6645b6b8a656360fa16fb7e1c6721d0697d3d6a/codex-rs/codex-mcp/src/elicitation.rs#L196-L221)), and limits the automatic response to empty-schema approval/confirmation forms ([lines 319–331](https://github.com/openai/codex/blob/a6645b6b8a656360fa16fb7e1c6721d0697d3d6a/codex-rs/codex-mcp/src/elicitation.rs#L319-L331)).

The installed CLI labels app-server experimental. This project therefore verifies the current method and upstream schemas on every call and fails closed on drift.

## Live architecture probe

A raw stdio client can initialize the signed `SkyComputerUseClient` and list all ten tool schemas, but a real `list_apps` call from an ordinary Node parent returns:

```text
Computer Use server error -10000: Sender process is not authenticated
```

The same read-only call through signed app-server `mcpServer/tool/call` succeeded while app-server ran with an empty isolated `CODEX_HOME`:

- all ten exact tools discovered;
- one `mcpServer/tool/call`;
- 39 apps returned;
- ephemeral context explicitly attested `ephemeral: true`, `turns: []`, and `path: null`;
- no `turn/start`, `turn/*`, or `item/*` model activity;
- no account auth file or model API credential available to app-server;
- a dummy provider with `supports_websockets = false` and unreachable loopback base URL;
- plugin and remote-control features disabled;
- trace/network sampling showed no app-server Responses API connection or model request after those controls (the separate official Computer Use service may use its own network transport).

This is the narrow official boundary: direct calls require a loaded thread runtime ID in the current API, but do not require a conversation turn or model generation. Literal removal of the thread-shaped runtime object would require an upstream app-server API change.

## Sender-authentication differential

A supported fake stdio MCP server was substituted only to observe the app-server's public downstream envelope shape. App-server used:

- MCP protocol `2025-06-18`;
- `clientInfo` name/title/version identifying `codex-mcp-client` / Codex / the installed CLI;
- standard `elicitation.form` and `elicitation.url` capability keys;
- standard progress metadata on `tools/list`;
- standard progress metadata plus public app-server-added `threadId` metadata on `tools/call`.

An ordinary MCP SDK client then repeated a read-only raw-helper call with the same client identity, capability shape, protocol behavior, and public `progressToken`/random `threadId` metadata shape. The helper still returned `-10000 Sender process is not authenticated`. Changing documented handshake fields therefore does not repair raw dispatch.

The relevant construction difference is the macOS responsible-process chain:

- unsupported: ad-hoc/no-Team-ID Node parent → signed helper;
- supported: Node/Pi → strict-valid OpenAI-signed app-bundled Codex (`2DC432GLL2`) → strict-valid OpenAI-signed helper (`2DC432GLL2`).

Process sampling confirmed the helper children were direct descendants of signed app-server; helper processes may create their own process groups. App-server and helper therefore share one private broker-session cwd. One-shot MCP calls close that session after the call; Pi retains it only across the required `get_app_state` and following action. Cleanup preserves partial ancestry results, recovers any reparented process by that unique cwd, freezes the owned set to stability, then kills and verifies every process rather than assuming one PGID. Enumeration, freeze, or exit uncertainty fails cleanup closed. The app-server has the expected sandbox/application-group entitlement keys; the helper has OpenAI application/team identifiers, application groups, and keychain-access-group entitlements. Ordinary Node has no Team ID or entitlement set. The service's peer audit token is not exposed through the supported MCP layer. No private socket inspection, identity spoofing, copied signing material, re-signing, injection, or TCC change was attempted. The remaining enforcement boundary is therefore OS peer/responsible-process identity, not a missing public JSON field.

Command, environment, cwd, and config differences were also isolated: both paths invoke the same signed helper with `mcp`; the accepted path gives it a signed parent plus a private fixed cwd/config environment. Matching public MCP fields did not alter the raw error, while the signed-parent path succeeds without model credentials.

## Process path

```text
Pi model
  └─ chooses computer_use_<method> and typed arguments
      └─ local direct service
          └─ signed app-bundled `codex app-server --stdio`
              ├─ empty ephemeral runtime context (zero turns; no transcript)
              └─ `mcpServer/tool/call`
                  └─ signed SkyComputerUseClient `mcp`
                      └─ official Computer Use service
                          └─ target macOS app
```

Forbidden primary-path methods are enforced by code and tests:

```text
turn/start
turn/steer
turn/resume model work
codex exec
codex mcp-server model tools
nested prompts or result summaries
```

## Current adapter inventory

| Mechanism or former restriction | Role in the direct design | Current treatment |
|---|---|---|
| Reviewed ChatGPT/Codex/client paths | official transport compatibility | current per-user installed-component contract plus exact legacy plugin layout; canonicalized and verified |
| Strict signatures and OpenAI Team ID | official component verification | retained to verify the selected signed transport components |
| Signed-parent/responsible-process chain | official transport requirement | retained through signed app-server |
| Private Sky socket/native-pipe access | unsupported/private route | prohibited |
| Nested Codex model and prompt | former compatibility layer | removed |
| Model selection and reasoning effort | former compatibility layer | removed; dummy unreachable provider prevents model transport |
| Responses websocket prewarm on empty thread | accidental app-server behavior | disabled with non-websocket dummy provider |
| Plugin/remote-control startup networking | accidental app-server behavior | corresponding features disabled |
| Model-written multi-step task plan | former compatibility layer | removed |
| Model result schema/summary | former compatibility layer | removed |
| Streamed model-event target/method validation | former compatibility layer | replaced by pre-dispatch typed call validation |
| Per-operation tool allowlists and call budgets | former wrapper policy | removed; one typed method per direct request |
| Task text, cleanup instructions, required-capabilities fields | former compatibility layer | removed |
| Dictionary-only special policy | former wrapper policy | removed |
| Wrapper app/intent allowlists in full mode | former wrapper policy | absent |
| Safe/full wrapper modes | former wrapper policy | removed; one durable no-permissions interface exposes all ten methods |
| Wrapper approval prompts/configuration | former wrapper policy | removed; no command, config, environment, or per-call selector remains |
| Official first-party app access | official Codex control | official Codex Full access auto-accepts normal empty-schema app approval elicitations; any elicitation app-server emits is forwarded unchanged |
| macOS TCC | official platform control | retained; never modified |
| Exact ten-tool inventory/schema | protocol compatibility check | retained and checked before each call |
| Canonical bundle identity | legacy wrapper behavior | retained for the current release contract; not an independent authorization boundary |
| Same-app kernel lock | legacy wrapper coordination | retained in one fixed per-user namespace shared across Pi/MCP state roots |
| Automatic background app launch | accidental wrapper behavior | removed; the official tool owns app behavior |
| Global focus watcher and final sample | legacy completion telemetry | retained for the current release contract; not a preventive sandbox |
| Timeout/cancellation process-tree termination | operational lifecycle | retained with strict enumeration, freeze, kill, stdio-close, and exit verification |
| Temporary broker-session cleanup | operational lifecycle | retained with isolated `CODEX_HOME`; Pi closes after the inspection-action pair, replacement inspection, or session shutdown |
| Codex token usage accounting | former compatibility layer | removed; no model turn exists |
| Content-safe private audit | legacy observability behavior | retained with direct-call fields; not an authorization mechanism |
| Full-result spill files | output hygiene | prohibited; truncation is in-memory only |
| Browser-host integration | out of scope | unchanged |

These roles explain the current code. They do not authorize new safeguards. Only propose hardening for a concrete, realistically reachable failure in the supported path.

## No-permissions and elicitation boundary

No-permissions is the sole interface and means unrestricted wrapper dispatch with no wrapper approval prompt. All ten methods are registered. No config file, environment value, slash command, CLI mode switch, or tool argument selects another route.

App-server is created with `approvalPolicy: "never"` and `sandbox: "danger-full-access"`. This is the official Codex Full access combination. The legacy sandbox field maps to `PermissionProfile::Disabled`; Codex then auto-accepts empty-schema MCP approval elicitations before client notification. Normal first-party Computer Use app approvals therefore do not prompt, and the bridge does not need to mutate the per-bundle approval store.

The broker still handles any `mcpServer/elicitation/request` that app-server emits. Pi advertises OpenAI-form support and renders emitted form, OpenAI-form, and URL requests. Generic stdio MCP forwards supported standard modes as `elicitation/create`. The response path preserves `accept`, `decline`, `cancel`, structured content, and response metadata; no callback or compatible UI yields `cancel`, never an invented decision.

## Pi tool and session boundary

The Pi extension registers and activates all ten official definitions on `session_start`, additively preserving active tools owned by Pi and other extensions. Keeping inspection and interaction methods on the same direct surface avoids provider-bound tool discovery creating a second, incompatible client binding.

For `get_app_state`, Pi starts one verified zero-turn app-server runtime and signed Computer Use client. The following direct interaction is serialized through that same runtime and thread, preserving the official active-app lease. The retained process tree is closed and verified after the action, before a replacement inspection, or during `session_shutdown`. One-shot MCP calls still start and close a broker session within the call. Exact inventory and schemas are revalidated before every dispatch in either path.

This lifecycle changes neither the schemas nor the no-permissions policy. Interaction definitions omit extra `promptSnippet` and `promptGuidelines` metadata and rely on their official descriptions.

## Output boundary

Only official `text` and `image` MCP blocks are accepted. They are returned to the invoking client because app state and screenshots are the requested capability. They are never copied to audit, logs, temp files, or structured metadata. Text is truncated in memory at Pi's standard 50KB/2000-line bound; the full text is not persisted.

## Package name

Version 0.2.0 keeps the repository and npm package name `codex-computer-use-mcp` for continuity with version 0.1.0. Pi-owned typed tools are now the primary product path. OpenAI's signed Codex app-server remains an implementation boundary, not the planner.

Any future package or repository rename would be a separate distribution change and would require maintainer approval.
