# ServerFS MCP v0.3.0 Design Baseline — Agent Bridge > Status: **v0.3.0 COMPLETE/FROZEN** after Phase E acceptance. > > Acceptance evidence: [`docs/phase-e-acceptance-2026-09-20.md`](docs/phase-e-acceptance-2026-09-20.md). > This document freezes the intended v0.3 architecture before implementation. ## 1. Goal ServerFS v0.3 adds an optional **Agent Bridge** capability so a remote MCP client such as ChatGPT can delegate a long-running task to an AI coding agent already available on the Linux server, initially: - OpenAI Codex CLI / Codex App Server - Anthropic Claude Code / Claude Agent SDK The user experience should be: ```text ChatGPT -> submit an agent task against a configured ServerFS workdir -> receive a task_id immediately -> poll progress later -> receive approval requests or questions in the ChatGPT conversation -> answer/approve/deny through dedicated MCP tools -> retrieve the final agent response ``` Tasks may take minutes or hours. A single MCP tool call MUST NOT remain open for the whole agent run. ## 2. Non-goals v0.3 does **not** turn ServerFS into a generic remote shell. Do not add: - `execute_command` - `run_shell` - arbitrary executable paths - arbitrary argv from MCP callers - arbitrary environment injection - arbitrary working directories outside configured workdirs - arbitrary host filesystem access - generic process management - permanent permission-rule editing - automatic `bypassPermissions` / `danger-full-access` - provider-specific Codex or Claude protocol objects in the public MCP schema The new capability is specifically: > delegate a task to an administrator-enabled AI-agent runtime under an explicit > workdir and permission profile. ## 3. Architectural decision ### 3.1 Keep the existing ServerFS container security domain The current `serverfs-mcp` container remains: - no published port - no Internet egress - read-only container root filesystem - non-root - `cap_drop: ALL` - `no-new-privileges` - no Docker socket - no generic shell tool The MCP container MUST NOT directly spawn `codex` or `claude`. ### 3.2 Add a thin host-side Agent Bridge Add a separate host-side component: ```text ChatGPT | OpenAI Secure MCP Tunnel | serverfs-mcp container | | versioned JSON RPC over local Unix-domain socket v serverfs-agent-bridge | | | | CodexAdapter ClaudeAdapter | | official Codex official Claude App Server daemon Agent SDK / session layer ``` The bridge is a **protocol gateway and permission broker**, not a replacement agent daemon. It owns: - ServerFS task IDs and provider-neutral state - mapping task IDs to native provider sessions/turns - normalized events - pending approval/question records - workdir/profile authorization - cross-process write leases - persistence needed to reconnect/reconcile It does NOT own: - Codex's thread history - Claude's session transcript - provider model execution - provider authentication - provider auto-update lifecycle - a second implementation of either provider's agent runtime ## 4. Provider strategy ### 4.1 Codex: use the official App Server daemon Codex integration MUST target the official machine-readable App Server protocol and, when configured, its official managed daemon. Preferred topology: ```text serverfs-agent-bridge -> Codex App Server JSON-RPC -> codex app-server daemon ``` The adapter must be isolated behind a `CodexAdapter` interface because the official `codex-app-server-daemon` is currently documented as experimental and its lifecycle contract may change. The Bridge MUST NOT expose raw Codex JSON-RPC to MCP callers. The Bridge SHOULD require the administrator to prepare Codex authentication and daemon availability. It may optionally use the official `codex app-server daemon start` lifecycle command when an explicit autostart setting is enabled, but MUST NOT silently bootstrap/download/install Codex because an MCP task was submitted. ### 4.2 Claude: use the official Claude Agent SDK Do not reverse-engineer the private implementation behind `claude agents`. Use the official Python `claude-agent-sdk`, preferably `ClaudeSDKClient` for interactive/bidirectional runs because it supports: - persistent conversation context while connected - streaming messages - interrupts - dynamic permission mode - `can_use_tool` - session resume through documented session IDs/options The bridge should use the existing system Claude Code CLI/authentication when explicitly configured, or the SDK-bundled CLI only when that deployment choice is intentional. The public ServerFS model must not depend on undocumented Claude daemon socket formats. ## 5. Why ServerFS still needs a bridge process Reusing the provider daemons does not remove the need for a ServerFS-specific bridge. Something still has to: - authenticate/authorize MCP-originated requests against ServerFS workdir policy - translate one provider-neutral task API into two different provider APIs - normalize approval requests and user questions - keep provider-native IDs private - preserve pending interaction state across individual MCP calls - enforce a write lease shared with ServerFS mutation tools - store task metadata/results/events for polling The bridge must remain thin enough that provider lifecycle is still delegated to the official runtime. ## 6. Transport and trust boundary Use a Unix-domain socket only. Recommended host path is a persistent directory owned by the current user: ```text ~/.local/share/serverfs-agent-bridge/runtime/socket/bridge.sock ``` The user-owned socket directory is bind-mounted read-only into `serverfs-mcp` at the stable container path: ```text host ~/.local/share/serverfs-agent-bridge/runtime/socket -> container /run/serverfs-agent-bridge ``` Phase E deployment is **user-scoped by design**. The project must not require sudo/root, system users/groups or writes to `/etc`, `/opt` or `/var/lib`. Default identity contract: ```text host Bridge UID:GID current login user's id -u / id -g serverfs-mcp requested UID:GID same current user UID/GID Bridge allowed_peer_uid/gid same current user UID/GID ~/.local/share/serverfs-agent-bridge/runtime/socket owner=current user, mode 0750 bridge.sock owner=current user, mode 0660 ~/.local/share/serverfs-agent-bridge/runtime/locks owner=current user, mode 0750 01.lock .. 16.lock owner=current user, mode 0640 ``` The group on these files is simply the user's existing primary group; no dedicated system group is created. Phase E still measures the real host-side `SO_PEERCRED` values. The expected equality with `id -u` / `id -g` is a validation result, not an input assumption: deployment must not pre-populate `SERVERFS_AGENT_PEER_UID/GID` from the login user's IDs before the real container peer has been measured. If Docker rootless mode, userns-remap or another mapping causes the peer UID/GID to differ from the current login user, the default user-scoped deployment is considered incompatible and MUST fail closed. Do not recommend privileged ownership changes as a workaround. No TCP listener and no host-published port are introduced. The bridge MUST: - set restrictive socket permissions - verify the connecting peer UID/GID with Linux peer credentials where available - accept only the expected ServerFS identity/group - use a versioned request protocol - reject unknown methods/fields by default - impose request-size limits The UDS is an additional local security boundary; MCP authorization remains mandatory. ## 7. Workdir agent authorization Agent execution is a separate capability from file mutation. Existing: ```env WORKDIR_01_READ_ONLY=true ``` does NOT imply anything about agent execution. Add per-workdir configuration: ```env WORKDIR_01_AGENT_MODE=disabled WORKDIR_01_AGENT_RUNTIMES= ``` Allowed `AGENT_MODE` values: ```text disabled review workspace-write ``` Default MUST be: ```text disabled ``` so upgrading from v0.2 never grants agent execution. Example: ```env WORKDIR_02_ALIAS=ServerFS WORKDIR_02_READ_ONLY=false WORKDIR_02_AGENT_MODE=workspace-write WORKDIR_02_AGENT_RUNTIMES=codex,claude ``` Rules: - `disabled`: no agent task may be submitted. - `review` remains a provider-neutral Phase A profile for adapters that can honestly enforce read-only execution. - `workspace-write` means the delegated agent may mutate files, so the Bridge holds the cross-process workdir lease for the active turn. - `workspace-write` requires `WORKDIR_XX_READ_ONLY=false`; configuration mismatch is a startup error. - runtime names are strict allowlist values; unknown names fail configuration. - MCP callers cannot request a runtime not allowlisted on that workdir. - **Codex Phase B native mode accepts only `workspace-write`**. This is a scheduling and lease declaration, not a Codex sandbox override. ## 8. Provider-native execution policy For Codex Phase B, ServerFS deliberately does **not** redefine the user's Codex execution environment. The selected ServerFS workdir determines: - which administrator-approved workdir may be used to start a Codex task; - the initial `cwd` passed to Codex; - which workdir lease ServerFS holds while that Codex turn is active. It is **not** a Codex filesystem/network sandbox boundary. ServerFS does not override, disable or synthesize Codex: - sandbox mode; - approval policy; - MCP servers; - skills/plugins/apps; - web/network features; - shell environment policy; - user/project Codex configuration. Codex therefore behaves as closely as practical to the same server user's direct Codex usage. Its existing configuration, project trust, authentication and provider-level permissions remain authoritative. If Codex itself emits an approval or user-input request, the Bridge relays that native interaction to ChatGPT and returns the user's decision. ServerFS does not silently deny the request merely because it refers to network access or a path outside the selected workdir. This is an intentional product boundary: > ServerFS controls **whether and where delegation starts**. Codex controls **what its > configured runtime is allowed to do after delegation starts**. The workdir lease remains necessary because native Codex may modify files even when ServerFS itself is not performing a mutation. ## 9. Codex native-mode behavior Phase B should preserve the familiar server-side Codex environment: - connect to the user's existing official managed App Server daemon; - pass the selected `cwd` and user prompt; - omit per-thread/per-turn sandbox overrides; - omit per-thread/per-turn approval-policy overrides; - omit config overrides that disable MCPs, skills, plugins, web search or shell features; - do not rewrite Codex config files; - do not copy provider credentials into ServerFS; - preserve Codex-native approval/question behavior through the Bridge. `codex.autostart` defaults to false. This is preferred for native-mode predictability because the already-running official daemon uses the environment inherited when that daemon was started. If an administrator explicitly enables autostart, the Bridge may call only the official idempotent daemon start lifecycle command and must document that the daemon then inherits the Bridge service environment. Claude Phase C may make a separate provider-specific choice; do not force Codex's native mode semantics onto Claude before its official SDK behavior is evaluated. ## 10. Public MCP task model A ServerFS `task_id` represents **one delegated objective / provider turn**, not an entire conversation forever. A task has one terminal result. If the user wants a follow-up that keeps the same native provider conversation, submit a new task with `continue_from_task_id`. This prevents a task from changing from `succeeded` back to `running` and keeps task state compatible with future MCP Tasks semantics. Native provider IDs are internal implementation details. Suggested internal mapping: ```text ServerFS task_id -> runtime -> native_session_id / thread_id -> native_turn_id (if provider has one) ``` ## 11. Task states Freeze these provider-neutral states: ```text queued starting running waiting_for_approval waiting_for_question succeeded failed cancelled interrupted ``` Terminal: ```text succeeded failed cancelled interrupted ``` Transitions: ```text queued -> starting -> running running -> waiting_for_approval -> running running -> waiting_for_question -> running running -> succeeded | failed | cancelled | interrupted waiting_* -> cancelled waiting_* -> interrupted ``` Never silently retry a side-effecting task after an ambiguous interruption. ## 12. Provider-neutral task record Persist at least: ```text task_id runtime workdir_alias workdir_slot relative_cwd profile status created_at started_at updated_at completed_at continue_from_task_id (nullable) native_session_id (private) native_turn_id (private) final_response (nullable) error_code (nullable) error_message (nullable) pending_request_id (nullable) ``` Do not return native host paths or provider-native IDs to MCP callers unless a later diagnostic design explicitly needs them. ## 13. Public MCP tools Freeze the initial v0.3 surface to eight new tools. ### 13.1 list_agent_runtimes Read-only. Returns provider availability and normalized capabilities, for example: ```json { "runtimes": [ { "name": "codex", "available": true, "version": "…", "capabilities": { "persistent_session": true, "live_steer": true, "interactive_approval": true, "interactive_question": true } } ] } ``` Must not start/install a provider merely to list it. ### 13.2 submit_agent_task Inputs: ```text runtime workdir path # relative cwd, default "" profile # must not exceed configured workdir mode prompt continue_from_task_id? ``` Immediately returns: ```json { "task_id": "agt_…", "status": "queued" } ``` It MUST NOT wait for completion. If `continue_from_task_id` is supplied: - prior task must exist - runtime must match - workdir must match - profile cannot become less restrictive without explicit policy - Bridge reuses/resumes the native provider session but creates a NEW ServerFS task ID ### 13.3 get_agent_task Returns normalized task state. If waiting: ```json { "task_id": "agt_…", "status": "waiting_for_approval", "pending_request": { ... } } ``` If complete: ```json { "task_id": "agt_…", "status": "succeeded", "final_response": "…" } ``` ### 13.4 read_agent_task_events Cursor-paginated normalized events. Do not return unbounded raw stdout/stderr. Suggested event types: ```text task.started turn.started agent.message command.started command.completed file_change.started file_change.completed approval.requested approval.resolved question.requested question.answered task.completed task.failed task.interrupted ``` Do not persist token-by-token text deltas as individual durable events by default. ### 13.5 respond_agent_approval Inputs: ```text task_id request_id decision granted_permission_ids? # only for permission-set requests ``` Decisions: ```text approve_once approve_session deny cancel_task ``` Rules: - only a currently pending approval may be answered - stale/already-resolved request -> `REQUEST_ALREADY_RESOLVED` - `approve_session` is session-scoped only - never modify global Codex/Claude config files - no permanent approval in v0.3 - no Codex exec-policy amendment exposure in v0.3 - provider adapters may apply provider-specific limits only when that provider's phase explicitly defines them; Codex Phase B native mode does not impose an additional ServerFS permission ceiling ### 13.6 answer_agent_question Inputs: ```text task_id request_id answers[] ``` Normalized answer: ```text question_id selected_option_ids[] text? ``` Support: - single choice - multiple choice - free text - mixed provider forms where representable ### 13.7 send_agent_message Only valid while the current task is active. Purpose: steer an in-flight task, not create a post-completion follow-up. Examples: - "Do not modify the database; change only the API layer." - "Stop investigating the frontend and focus on the failing test." If the task is terminal, return an error instructing the caller to use `submit_agent_task(... continue_from_task_id=...)`. ### 13.8 cancel_agent_task Interrupt/cancel an active task. Repeated cancellation must have no additional environmental effect. ## 14. Tool annotations Recommended conservative annotations: ```text list_agent_runtimes: readOnly=true openWorld=false get_agent_task: readOnly=true openWorld=false read_agent_task_events: readOnly=true openWorld=false submit_agent_task: readOnly=false destructive=true idempotent=false openWorld=true respond_agent_approval: readOnly=false destructive=true idempotent=true only when request_id resolution is CAS-protected openWorld=true answer_agent_question: readOnly=false destructive=false idempotent=true only when duplicate answer is detected/rejected openWorld=true send_agent_message: readOnly=false destructive=true idempotent=false openWorld=true cancel_agent_task: readOnly=false destructive=true idempotent=true openWorld=true ``` Annotations are Host hints, never authorization. ## 15. Pending interaction model ChatGPT is the remote human-in-the-loop UI. The Agent Bridge converts native provider requests into one of two public request kinds: ```text approval question ``` The MCP call that originally submitted the task is already finished. ChatGPT later learns about the interaction by calling `get_agent_task` or `read_agent_task_events`. ### Approval request Normalized shape should include only the fields needed for an informed decision: ```text request_id kind category title reason? relative_cwd? command_display? file_changes[] network_targets[] requested_permissions[] available_decisions[] created_at ``` Categories: ```text command file_change filesystem_permission network_permission mcp_tool other ``` ### Question request ```text request_id kind="question" questions[] created_at expires_at? ``` Each question: ```text question_id prompt options[] multi_select allow_free_text ``` ## 16. Host-path confidentiality during approvals The existing ServerFS contract that host workdir paths are not exposed remains in force. Provider-native absolute paths must be normalized: - paths under the selected workdir -> workdir-relative paths - configured workdir root itself -> "." - paths outside the selected workdir -> display `` rather than the raw host path This redaction is an MCP information-disclosure rule only. It does not prevent native Codex from requesting or using such a path if Codex's own configuration and the user's approval allow it. Codex `cwd`, `grantRoot`, and permission path fields must not leak the host mapping. Commands may be shown for approval, but the adapter must redact configured workdir root prefixes from display strings. ## 17. Codex mapping ### Start a new task ```text thread/start cwd = resolved host workdir + relative cwd serviceName = "serverfs-agent-bridge" # no sandbox / approval / config override turn/start threadId cwd user input = task prompt # no sandbox / approval / config override ``` Persist: ```text thread.id -> native_session_id turn.id -> native_turn_id ``` ### Continue a prior task ```text thread/resume(native_session_id) turn/start(new prompt) ``` A new ServerFS task ID is returned. ### Progress Normalize: - `item/started` - `item/completed` - completed agent messages - command execution - file change events - errors - `turn/completed` ### Steer ```text send_agent_message -> turn/steer ``` ### Cancel ```text cancel_agent_task -> turn/interrupt ``` ### Command approval Native: ```text item/commandExecution/requestApproval ``` Normalize to `approval(category="command")`. Mapping: ```text approve_once -> accept approve_session -> acceptForSession (only if advertised by availableDecisions) deny -> decline cancel_task -> cancel + task cancellation semantics ``` Do not expose `acceptWithExecpolicyAmendment` in v0.3. ### File-change approval Native: ```text item/fileChange/requestApproval ``` Same normalized decision model. ### Permission approval Native: ```text item/permissions/requestApproval ``` The adapter: 1. preserves the native Codex permission request as provider data; 2. exposes its top-level permission categories as selectable permission IDs; 3. sends the request to ChatGPT without imposing an additional ServerFS permission ceiling; 4. maps `approve_once` to turn scope; 5. maps `approve_session` to session scope; 6. returns either the user's selected permission categories or the full native request when the user approves without narrowing the set. ### User input Native: ```text item/tool/requestUserInput ``` Normalize to `question`. Track Codex `serverRequest/resolved`. If Codex clears the request before ChatGPT answers, mark the ServerFS request stale and reject a late response. ### Codex daemon lifecycle The adapter connects to the official control socket using WebSocket-over-UDS, matching Codex's current remote app-server client implementation. On connection it performs the normal JSON-RPC `initialize` request followed by the `initialized` notification before issuing thread/turn requests. v0.3 should support explicit settings such as: ```env SERVERFS_AGENT_CODEX_ENABLED=true SERVERFS_AGENT_CODEX_AUTOSTART=false ``` Default autostart SHOULD be false. When autostart is enabled, use only official lifecycle commands such as `codex app-server daemon start`; do not implement process supervision or silent bootstrap/install logic. ## 18. Claude mapping Use the official Python `ClaudeSDKClient`, not a private `claude agents` daemon protocol. Phase C follows the same product principle already frozen for Codex: **preserve the server user's native Claude Code environment instead of synthesizing a second ServerFS permission/sandbox policy**. The Claude Agent SDK intentionally isolates SDK applications from filesystem settings and the Claude Code system prompt by default. That default is useful for generic SDK applications, but it is the wrong behavior for ServerFS native mode. The adapter MUST explicitly restore normal Claude Code context: - `cli_path` points to the administrator-selected, already-installed system `claude` executable; - `setting_sources=["user", "project", "local"]`; - `system_prompt={"type": "preset", "preset": "claude_code"}`; - no ServerFS-supplied `permission_mode`; - no ServerFS `allowed_tools` / `disallowed_tools`; - no ServerFS replacement MCP list; - no ServerFS sandbox/environment override. Existing Claude authentication, settings, CLAUDE.md files, skills, MCP servers, hooks and permission rules remain authoritative. As with Codex native mode, Phase C only exposes Claude on a `workspace-write` Agent workdir. This is a **writer-lease accounting rule**, not a request to force Claude into any particular permission mode. Native Claude may modify files, so ServerFS must hold the selected workdir's exclusive lease for the active task. ### Start a new task Create `ClaudeSDKClient(ClaudeAgentOptions(...))` with: - the selected host workdir as `cwd`; - the existing system Claude CLI via `cli_path`; - native setting sources and Claude Code system-prompt preset as described above; - a `can_use_tool` callback for native permission decisions that reach Claude's `ask` path. Do not force a tool into the callback merely to make ServerFS display an approval. Tools already allowed/denied by the user's native Claude settings should retain that behavior. Persist the native Claude session ID from the result. ### Continue a prior task Use `ClaudeAgentOptions.resume` with the recorded native session ID and create a new ServerFS task ID. Do not use "continue the most recent conversation" as the persistence primitive; explicit session IDs are required. ### Progress Normalize useful provider events such as: - assistant text; - Bash/tool starts; - file-edit tool starts; - result/error information. Do not mirror the entire raw provider transcript into the ServerFS SQLite database. ### Steer `ClaudeSDKClient` is bidirectional, but Phase C MUST NOT advertise live steer merely because `client.query(...)` exists. First prove against the installed SDK/CLI that a query submitted during an in-flight response has the intended steer semantics rather than queuing a second turn. Until that proof exists: ```text live_steer = false send_agent_message -> explicit provider error ``` ### Cancel ```text cancel_agent_task -> ClaudeSDKClient.interrupt() ``` User cancellation remains authoritative in the Bridge even if the provider interrupt itself fails. ### Permission approval Use `can_use_tool` only for native Claude permission decisions that reach the `ask` path. For a request that reaches the callback: 1. normalize it into a durable ServerFS approval; 2. transition the task to `waiting_for_approval`; 3. await ChatGPT/user resolution; 4. `approve_once` returns `PermissionResultAllow(updated_input=...)`; 5. `deny` returns `PermissionResultDeny`; 6. `cancel_task` denies with `interrupt=True`; 7. expose `approve_session` only if Claude itself supplies a `PermissionUpdate(destination="session")` suggestion, and echo only those session-scoped suggestions back via `updated_permissions`. Never turn a Claude suggestion targeting `userSettings`, `projectSettings` or `localSettings` into a persistent approval. ServerFS must not silently modify the user's Claude permission configuration. Do not set `allowed_tools` merely to avoid callbacks: provider rules that already allow a tool are intentionally outside the remote approval path. ### AskUserQuestion When `tool_name == "AskUserQuestion"`: 1. normalize the Claude questions into the provider-neutral question model; 2. persist `waiting_for_question`; 3. await `answer_agent_question`; 4. return `PermissionResultAllow(updated_input=...)` containing the original input plus Claude's expected `answers` mapping. Treat this as a user question, not an approval card. A historical Agent SDK / Claude Code bug has allowed `AskUserQuestion` to resolve with empty answers before an asynchronous `can_use_tool` callback completes. Therefore **a real installed-CLI AskUserQuestion round-trip is a mandatory Phase C release gate**. Mock coverage alone is insufficient. ### Human-wait idle timeout Waiting for ChatGPT/user input is not provider idleness. If an optional provider event idle timeout is configured, the adapter must keep the same pending SDK receive operation alive while a permission/question callback is waiting for user input. ### Recovery Initial Claude recovery capability is `session-resume`. Do not claim transparent in-flight recovery of a live SDK subprocess/callback after Bridge restart. ## 19. Waiting for input and restart semantics The public pending request is durable; the provider callback/request may not be. This distinction is critical. ### Normal operation The Bridge is a long-lived process. A Codex server request or Claude `can_use_tool` callback may remain pending while ChatGPT is idle. The original MCP request is not held. ### Bridge restart while waiting Do NOT claim transparent recovery unless proven by provider-specific integration tests. Conservative v0.3 behavior: - persist the ServerFS task and pending request before exposing it - after Bridge restart, reconcile the native provider session - if the exact native pending request can still be answered safely, restore waiting state - otherwise mark the task `interrupted` - never fabricate an approval response or silently re-run a side-effecting provider turn A user can continue an interrupted provider conversation by submitting a NEW task with `continue_from_task_id`. ## 20. Runtime recovery capability `list_agent_runtimes` should publish tested recovery capability, not marketing claims. Suggested values: ```text in_flight_recovery: native session-resume none ``` Codex may reach `native` when daemon reconnect/reconciliation is proven. Claude should initially be treated as `session-resume`: the session can be continued, but an in-flight SDK subprocess/callback is not assumed to survive Bridge restart. ## 21. Cross-process write lease v0.2's in-process mutation lock is insufficient once a host agent becomes an official writer. v0.3 adds a cross-process per-workdir write lease. Recommended implementation: ```text /run/serverfs-agent-locks/.lock ``` using Linux `flock`. The Bridge pre-creates all 16 lock files before serving requests. In private development mode the directory/files remain `0700/0600`. The Phase D implementation also supports an explicit peer GID, in which case the runtime assets become group-readable `0750/0640`. That is a protocol/lease capability, not a requirement to create a system group. Phase E's default user-scoped deployment sets the explicit peer GID to the **current user's existing primary GID** and keeps the host lock directory under that user's `~/.local/share/serverfs-agent-bridge/runtime/locks`. No project-managed group is created. Phase E bind-mounts this directory **read-only** into `serverfs-mcp`. The MCP container opens the already-existing lock file `O_RDONLY|O_NOFOLLOW` and takes `flock(LOCK_EX)`; it never creates or modifies host lock files. Linux `flock` is inode-based, so the read-only bind still coordinates with the Bridge's read-write descriptor on the same file. Rules: - ServerFS `create/edit/delete/mkdir/rmdir` takes the workdir exclusive lease for the short mutation transaction. - a `workspace-write` Agent task holds the exclusive lease for the entire active turn. - a second workspace-write task for the same workdir is rejected/queued according to a fixed policy; v0.3 should prefer `WORKDIR_BUSY` over hidden queuing. - read-only ServerFS tools never take the lease. - review Agent tasks do not hold a long exclusive lease. - do not put lock files inside user workdirs. This intentionally serializes ServerFS mutations with a coding agent that may edit many files over a long turn. For Codex native mode, the lease covers only the selected ServerFS workdir slot. If the user's native Codex configuration permits the agent to access or modify paths outside that workdir, those external paths are not serialized by ServerFS. Administrators who need a stronger filesystem/network boundary should configure that boundary in Codex itself rather than expecting Agent Bridge to synthesize a second sandbox. ## 22. Bridge persistence Use SQLite in a private host state directory. Suggested: ```text ~/.local/state/serverfs-agent-bridge/ state.sqlite3 logs/ ``` No Redis/PostgreSQL. SQLite stores: - task metadata - provider mapping IDs - normalized events - pending requests - final response/error - retention timestamps Provider transcript remains provider-owned. State directory permissions: 0700. Database/file creation must use restrictive permissions. ## 23. Retention and limits Add explicit limits from day one. Suggested defaults: ```text SERVERFS_AGENT_MAX_PROMPT_BYTES=65536 SERVERFS_AGENT_MAX_FINAL_RESPONSE_BYTES=262144 SERVERFS_AGENT_MAX_EVENT_BYTES=65536 SERVERFS_AGENT_MAX_EVENTS_PER_TASK=10000 SERVERFS_AGENT_MAX_ACTIVE_TASKS=4 SERVERFS_AGENT_TASK_RETENTION_HOURS=168 ``` Values are implementation defaults to validate during development, not promises that may be silently ignored. If final output exceeds the MCP result limit, return a bounded result plus a retrieval mechanism rather than a giant tool response. ## 24. Audit logging ServerFS MCP audit should record: ```text tool task_id runtime workdir relative cwd profile status transition request kind decision duration success/error_code ``` Do not log: - full prompt by default - full final agent response - raw provider transcript - secrets - provider auth material - host workdir paths - raw environment - sensitive command output Bridge local diagnostic logging may be more detailed but still must not dump credentials. ## 25. Unified error codes Add a provider-neutral layer, for example: ```text AGENT_DISABLED AGENT_RUNTIME_NOT_ALLOWED AGENT_RUNTIME_UNAVAILABLE AGENT_RUNTIME_NOT_READY AGENT_PROFILE_NOT_ALLOWED AGENT_TASK_NOT_FOUND AGENT_TASK_NOT_ACTIVE AGENT_TASK_TERMINAL AGENT_TASK_LIMIT_REACHED AGENT_TASK_TIMED_OUT AGENT_PROVIDER_ERROR AGENT_PROVIDER_DISCONNECTED AGENT_SESSION_NOT_RESUMABLE WORKDIR_BUSY REQUEST_NOT_FOUND REQUEST_ALREADY_RESOLVED REQUEST_STALE INVALID_APPROVAL_DECISION INVALID_QUESTION_ANSWER PERMISSION_OUTSIDE_POLICY ``` Provider-native errors can be stored internally and summarized, but MCP clients should not need Codex/Claude-specific error parsing. ## 26. MCP Tasks extension compatibility The 2026-07-28 MCP Tasks extension is conceptually aligned with this design: - durable task handle - polling - cancellation - in-progress input requests However the official Python MCP SDK currently documents the Tasks extension as not yet implemented. Therefore v0.3 MUST NOT block on MCP Tasks. Build the task backend/provider model independently and expose the eight ServerFS tools above. When the Python SDK and target OpenAI host support `io.modelcontextprotocol/tasks` reliably, add an adapter that maps the same task backend to standard MCP Tasks. Do not rewrite the Agent Bridge backend. ## 27. Bridge protocol Use a small versioned RPC contract over UDS. Suggested methods: ```text runtime.list task.submit task.get task.events task.approval.respond task.question.answer task.message.send task.cancel ``` Each request carries: ```text protocol_version request_id method params ``` Each response carries: ```text request_id ok result | error ``` No unsolicited notifications are required for v0.3 because MCP uses polling. The Bridge internally consumes provider event streams and persists normalized events. Do not make the UDS protocol a public Internet API. ## 28. Implementation packaging Keep the existing `serverfs-mcp` Python dependency set minimal. Do not install Codex/Claude SDK/runtime dependencies into the MCP container just because the repository also contains the Bridge. Preferred repository layout: ```text src/serverfs_mcp/ # existing MCP package agent_bridge/ pyproject.toml # separate host-side Python application src/serverfs_agent_bridge/ main.py protocol.py store.py policy.py leases.py adapters/ base.py codex.py claude.py ``` Use `uv` for the Bridge environment. Claude adapter depends on the official `claude-agent-sdk`. Codex adapter should connect to the official managed App Server control socket using the transport Codex itself currently uses for remote Unix-socket clients: **WebSocket frames over the Unix-domain socket**, with the WebSocket handshake URI `ws://localhost/rpc` and JSON-RPC messages inside text frames. Do not treat the daemon control socket as raw JSONL. The official Python Codex SDK currently launches its own stdio app-server and does not expose a supported attach-to-existing-daemon transport, so Phase B uses a small WebSocket-over-UDS client behind `CodexAdapter`. Keep that transport isolated because the managed daemon remains experimental and may change upstream. Do not restructure the released v0.2 package into a monorepo workspace until the implementation proves that the extra complexity is useful. ## 29. Native adapter interface Freeze a narrow internal interface before provider implementation. Conceptually: ```text probe() -> RuntimeInfo start_task(...) continue_task(...) get_state(...) send_message(...) cancel(...) respond_approval(...) answer_question(...) reconcile(...) close() ``` Adapters emit provider-neutral events and pending requests into the Bridge core. No MCP tool should directly import or call `CodexAdapter` / `ClaudeAdapter`; MCP talks only to the Bridge RPC client. ## 30. Testing strategy ### Unit - state machine transition matrix - workdir/profile authorization - prompt/result/event size limits - path redaction - approval normalization - question normalization - request CAS/resolution - SQLite persistence/recovery - write lease behavior - provider error normalization ### Codex integration Against a real official App Server daemon: - probe without starting - new thread + turn - event stream - final response - thread resume - continue task - turn steer - turn interrupt - command approval - file-change approval - permission request - user-input question - stale resolved request - daemon reconnect behavior ### Claude integration Against a real Claude Agent SDK / authenticated Claude Code environment: - new session - final response - explicit session ID capture - resume by ID - interrupt + buffer drain - `can_use_tool` approval - AskUserQuestion - deny - session-scoped approval behavior if supported safely - Bridge restart -> conservative interrupted/session-resume behavior ### MCP E2E Real path: ```text ChatGPT -> OpenAI Secure MCP Tunnel -> ServerFS MCP -> UDS Bridge -> provider -> real workdir ``` Must test: - submit returns immediately - polling while task runs - ChatGPT receives an approval request - approve_once and deny - ChatGPT receives a question with options/free text - answer resumes the task - final response - cancel - continue_from_task_id - concurrent workdir mutation -> WORKDIR_BUSY - provider unavailable - workdir agent mode disabled - Codex native mode rejects the provider-neutral `review` profile rather than pretending to enforce read-only behavior ## 31. Rollout order Do not implement everything in one patch. Phase A — Bridge core: - RPC - SQLite - task state machine - pending request model - workdir mapping - leases - fake adapter tests Phase B — Codex: - App Server daemon adapter - approvals/questions - resume/steer/cancel - real integration tests Phase C — Claude: - Claude Agent SDK adapter - permissions/questions - session resume/cancel - real integration tests Phase D — MCP (frozen): - eight public Agent tools - stdlib UDS Bridge client - fail-closed global + per-workdir Agent settings - conservative tool annotations and audit - integration with existing ServerFS workdir/path policy - shared cross-process mutation lease consumption - **no production Compose/systemd wiring yet** Phase E — deployment (**COMPLETE/FROZEN**; see [`docs/phase-e-acceptance-2026-09-20.md`](docs/phase-e-acceptance-2026-09-20.md)): - opt-in `compose.agent.yml` overlay; keep base `compose.yml` backward-compatible - systemd user-service lifecycle and graceful Bridge SIGTERM/SIGINT shutdown - real SO_PEERCRED UID/GID measurement; never infer under rootless/userns-remap - current-user UID/GID parity and SO_PEERCRED validation; no system group creation - socket/lock runtime permissions and read-only container bind mounts - deterministic Bridge config rendering from the single repository-root `.env`; no second Agent env/override layer - explicit provider environment parity checks without sourcing shell startup files - container-to-Bridge RPC and real provider discovery - ChatGPT -> Tunnel -> MCP -> Bridge -> Codex/Claude E2E - rollback to the base 11-tool deployment (live drill waived by maintainer for this release) - release documentation and v0.3 release gate ## 32. Release gate for v0.3 **CLOSED for v0.3.0.** All release-gate items below passed acceptance. The authoritative acceptance record is [`docs/phase-e-acceptance-2026-09-20.md`](docs/phase-e-acceptance-2026-09-20.md). The live base 11-tool rollback/re-cutover drill was separately waived by the maintainer for v0.3.0 and is documented there; the implementation and recovery tests remain green. ```text [x] v0.2 filesystem behavior remains fully green [x] agent execution defaults disabled on every workdir [x] no generic shell MCP tool exists [x] serverfs-mcp container still has no Internet egress [x] MCP container does not contain provider credentials [x] Codex uses official App Server protocol [x] Claude uses official Agent SDK API [x] provider-native IDs stay behind the Bridge [x] long task submit returns immediately [x] task polling survives separate MCP calls [x] approval round-trip works through ChatGPT [x] question round-trip works through ChatGPT [x] ServerFS injects no Codex sandbox/approval/config override in native mode [x] Codex-native approval/question requests are faithfully bridged when App Server emits them [x] ServerFS exposes no separate bypass/unrestricted Agent profile of its own [x] cancel is tested [x] continuation by prior task is tested [x] Agent tools are absent when SERVERFS_AGENT_BRIDGE_ENABLED=false [x] Agent tools are present only under explicit global + per-workdir enablement [x] ServerFS mutations return WORKDIR_BUSY while a workspace-write Agent holds the slot lease [x] workdir write lease is tested [x] Bridge restart semantics are tested and documented per provider [x] real ChatGPT -> Tunnel -> Agent E2E passes ``` ## 33. Current known provider limitations ### Codex The official managed App Server daemon is documented as experimental. Keep all daemon details behind the adapter and version/protocol probes. Do not assume a pending server request survives a Bridge connection loss until tested. Phase B intentionally supports the core Codex human-interaction requests needed for the initial delegation surface: command approval, file-change approval, permission approval and `item/tool/requestUserInput`. The user's existing Codex MCP servers remain enabled in native mode, but MCP-originated `mcpServer/elicitation/request` is not yet translated into ServerFS's provider-neutral interaction model. The Bridge must reject unsupported server requests promptly rather than leave the Codex turn hanging. Add MCP elicitation later as a separate interaction-surface extension instead of prematurely generalizing the Phase B question model to arbitrary MCP form/URL schemas. ### Claude The official Python Agent SDK is the supported programmable surface. The SDK can resume sessions by explicit session ID and provides interactive permission callbacks, but the Bridge must not assume an in-flight subprocess/callback survives Bridge restart. Use explicit recorded session IDs for continuation rather than relying only on "continue the most recent conversation" behavior. ## 34. Source references verified for this design Verified on 2026-09-19: - Codex App Server: https://developers.openai.com/zh-Hans/docs/app-server - Codex managed App Server daemon: https://github.com/openai/codex/blob/main/codex-rs/app-server-daemon/README.md - Claude Agent SDK Python reference: https://code.claude.com/docs/en/agent-sdk/python - Claude Agent SDK permission model: https://code.claude.com/docs/en/agent-sdk/permissions - Official Claude Agent SDK Python repository: https://github.com/anthropics/claude-agent-sdk-python - MCP Tasks extension / SEP-2663: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md - MCP Python SDK roadmap (re-verified 2026-09-20: Tasks extension still not implemented): https://github.com/modelcontextprotocol/python-sdk/blob/main/ROADMAP.md ## 35. Final v0.3 product boundary ServerFS v0.3 should be accurately described as: > A secure remote workdir MCP with optional, administrator-enabled delegation to > supported local AI-agent runtimes. ServerFS exposes no generic shell; it brokers > structured long-running tasks, human approvals, questions, cancellation, progress, > and final responses through a separate local Agent Bridge. The invariant is: ```text MCP caller -> explicit workdir -> explicit agent enablement -> provider-specific delegation mode -> ServerFS Agent Bridge -> official provider runtime -> human-in-the-loop when native runtime asks ``` ServerFS always controls whether delegation is enabled and which configured workdir is selected. Execution restrictions after delegation are provider-specific: Codex Phase B intentionally preserves the server user's native Codex policy, while later providers may define stricter adapter-level ceilings when their official integration model requires it.