rycen7822/subagent-pi
Persistent Pi coding subagents for Codex: bounded inspection, steering, interruption and recovery through MCP and CLI.
Changelog
Unreleased
- 受管子代理默认加载 Pi 自身的配置(extensions/packages/skills/prompts/themes/settings),
不再默认传
--no-extensions/--no-skills;ambient_extensions/ambient_skills改为显式退出开关(默认true)。 - builtin 工具面改由新扩展
extensions/managed-surface.ts在 session_start 应用, 并按 Pi 报告的来源(sourceInfo.path = "<builtin:NAME>")区分 builtin 与扩展工具: 只激活/停用真正的 builtin,扩展注册的同名工具(例如用自己的bash覆盖 builtin) 保持 Pi 给它的状态。--tools与--exclude-tools都不再使用——两者都按工具名过滤 同一份注册表,会连带剔除扩展的同名工具;merge_bridge_tool随之删除。限制 builtin 的 profile 若缺少该扩展则拒绝启动,且启动前必须收到子进程回读实时注册表后的报告 (tool_surface事件;缺失或与期望不符时tool_surface_unavailable/tool_surface_unapplied,不会谎称限制已生效)。 - 精简:桥接测试共用一个 harness fixture(原先 5 处重复的 skip/临时目录样例), internal server key 与 policy key 各自收敛为单一常量元组,worker 侧工具面校验/技能 回读直接取 worker 自己的身份,runtime 的三处 delegated text 信封合并为一个函数; 删除 bridge 与测试中的死分支/空转换。
PI_CODING_AGENT_DIR作为非秘密配置定位变量随 scope 绑定:客户端环境 → scope 快照 → daemon → guard → 子 Pi,daemon 重启后仍按记录恢复;未设置保持 Pi 默认,profile 的[profiles.x.env]取值仍然优先。- skill 同名冲突由 Pi 在加载边界裁决(Pi 已加载的 skill 保留、Codex 版本不注册);
每次 boot 通过
get_commands回读真实注册表并记录inheritance_skills(loaded/skipped/kept),不做 BM25/语义/别名去重,不复制或改名 skill 文件。 access=read不再拒绝加载 extensions,也不再声称"只具备只读工具":只读含义收窄为 builtin 限制、继承 MCP 的 readOnly 暴露与写者互斥,均非 OS 沙箱。- MCP 不按 server 名去重:Pi 0.85.1 没有 MCP 体系、没有 server 注册表(
get_commands只列命令,getAllTools()只给工具名与所属扩展文件),无法在不臆造配置格式的前提下 判断"Pi 已有同名 server"。仅保证(server, tool)寻址:不同 server 的同名工具不冲突。
0.2.9 — 2026-09-15
Module-boundary refactor of the daemon runtime. No IPC/MCP behavior changes; the one behavioral change is the ownership verdict (below), which is now shared.
runtime.pyno longer owns the process, binding and projection mechanics. It was 1081 lines with six unrelated responsibilities; it is now a 450-line state machine, and the extracted modules each own one seam:worker.py—Worker(the JSONL RPC channel),boot_worker,read_receipt/write_bootstrap,terminateandreap_orphan.binding.py—child_env,bind_scope_source,inheritance_plan,merge_bridge_tool,doctor.views.py—brief_agent/brief_run/outstanding/inspect/result/wait. Dependencies run runtime → {worker, binding, views}; none of the three imports runtime, so they stay testable without a live Runtime. A test pins that direction so the split cannot silently erode.
- The owner-record verdict is now one function (
worker.ownership) returninggone/live/unknown, used by crash reconciliation, orphan reaping and terminate. Previously the restart path andcloseeach re-derived it, so the same ledger row could beunknownto one and reapable by the other. close/reconcile no longer stall on a row that never reached the fork point: an agent recorded ascleanup='verified'with no pid has no missing owner record to explain, so it resolves todormantinstead of staying unresolvable.
0.2.8 — 2026-09-15
Architecture review follow-up: four reproducible defects plus the structural cleanups they pointed at. No behavior is weakened — every safety check keeps or tightens its previous guarantee.
- Restart no longer boots a worker without a base environment. The scope's
non-secret base keys (
PATH,HOME, ...) are persisted per scope (scopes.base_env, schema 3) and reloaded at every worker boot, so arespawnafter a daemon restart can still find its interpreter. Secrets are still never persisted: only base keys are stored, and the regression test asserts a bound secret never reaches the ledger. Previously the in-memory snapshot died with the daemon and the respawned worker started with an empty environment — whiledocs/inheritance.mdclaimed the base binding happens on every bind. - Crash reconciliation no longer reports an unverifiable process as verified.
When
owner.jsonis missing (the guard was killed before writing it) the old code read "no record" as "nothing running" and marked a live orphancleanup='verified'; it now requires a provably dead leader with no surviving process group, matching whatclosealready demanded of the same row. - Boot IPC calls got a client budget derived from the daemon's own worst case
(
startup_timeout_seconds, ~90s by default) instead of a flat 45s, so a slow-but-healthyspawn/respawn/closeis no longer reported as a failed mutation after it already committed.spawn'stimeout_secondsis the run deadline and deliberately does not inflate this wait. - The ZIP and the installed plugin now share one file-selection rule
(
scripts/ship_manifest.py). Previouslypackage.pyshipped dot-caches, editor dirs and drafts into the ZIP andFILES.sha256, andinstall.pyhad a different list for the same intent. - Fix:
_merge_bridge_toolmerged only the FIRST--toolsflag, so a config with two occurrences lostcodex_mcpagain under Pi's last-wins parsing. All occurrences now merge into one flag. - Fix: the TypeScript
confirm_allpolicy field was declared but never read; a write child would not confirm an allowlist-less server's calls. It is now enforced inneedsConfirmation(read children still confirm everything). - Ledger migrations are a version registry (
store.py:MIGRATIONS) instead of a hand-writtenif v == '1'branch, with tests for 1->current, 2->current and refusal of a future version. - Cleanups: one canonical base-env list (
common.BASE_ENV_KEYS) instead of three divergent copies, a shared resident-state tuple for restart/writer checks,inspect's byte budget measured incrementally instead of re-encoding the whole page per event, and removal of the unusedACTIVEset andWorker.lock_fd. validate_package.pynow fails whenpyproject.tomlor the bridgeclientInfoversion drifts from__version__.
Unreleased
- GitHub Actions CI:python-core(3.11/3.14)与 integration(真实 Pi 0.85.1 + Node 22.19.0,bridge tests 真实执行 + ZIP/FILES.sha256 校验);0 模型调用、0 secrets、
contents: read。
0.2.7 — 2026-09-15
Three MCP P1 fixes against review baseline 2c057b4 (0.2.6).
- P1-A (stdio dual-era Auto lifecycle): a modern-enabled stdio process now
starts with
server/discovercarrying full modern_meta— never a baretools/list. A valid DiscoverResult (object,resultType,supportedVersionscontaining a common modern version) or a recognized modern error keeps modern with no initialize; any other legacy-style error or a discovery timeout falls back to the full legacy handshake (initialize -> notifications/initialized -> tools/list). Fallback happens only on the side-effect-free discovery and never replays a tools/call. The CODEX_MCP_PROTOCOL_VERSION marker is still consumed client-side and never reaches the server process. stdio JSON-RPC errors now preserve code/message/data (shared RpcError). - P1-B (same-origin manual redirects): HTTP requests are sent with
redirect: "manual"and every hop is verified against the ORIGINAL MCP origin before anything is transmitted — a cross-origin redirect is refused with zero requests reaching the target. 301/302/303 become body-less GETs, 307/308 preserve method/headers/body; max 3 hops, visited-set cycle protection, and one shared deadline across all hops. The legacy session DELETE also carries Mcp-Session-Id and never redirects. - P1-C (full modern error classification): one shared classifier recognizes
-32020 (HeaderMismatch), -32021 (MissingRequiredClientCapability) and
-32022 (UnsupportedProtocolVersion) — modern, no initialize fallback;
-32022 additionally requires a common
data.supportedversion. HTTP 200 in-band JSON-RPC errors are classified by structured code too. A successful discover must be a real DiscoverResult; a malformed one is a protocol error, not a fallback. The fake modern servers now return the real 2026 DiscoverResult shape.
Minor: the Base64 sentinel check now re-encodes ANY string starting with
=?base64? and ending with ?=; x-mcp-header properties paths may not pass
through $ref/items/oneOf/... ancestors.
All new regressions run against the real TypeScript bridge with localhost fake servers (zero model calls); red-verified 12 failures on 2c057b4.
0.2.6 — 2026-09-15
Three MCP P1 fixes against review baseline a030a1d (0.2.5).
- P1-A (modern stdio opt-in):
CODEX_MCP_PROTOCOL_VERSIONin a stdio server's env is now consumed as the Codex client-side protocol marker —2026-07-28selects the modern stateless stdio lifecycle (first RPC istools/listcarrying modern_meta; no legacy initialize) — and is stripped from the env forwarded to the server process. Unknown marker values fail the server closed without starting any process; a globallegacy_2025_06_18override keeps the legacy handshake while still stripping the marker. - P1-B (HTTP era detection): non-2xx responses are parsed into a structured
error (status + JSON-RPC code/data, body capped at 64 KiB, never logged).
autoclassifies a side-effect-freeserver/discoverprobe by the error body: HTTP 400 with the recognized modernUnsupportedProtocolVersionError(-32022) keeps modern (verifyingdata.supportedcontains2026-07-28, else a clear incompatibility is reported); unrecognized/legacy-style 400, 404 and 405 prove legacy; 401/403/429/5xx never downgrade. No message substring matching remains. - P1-C (header encoding + nested paths):
encodeMcpHeaderValueimplements the 2026-07-28 rule (plain visible ASCII as-is; non-ASCII, control characters, edge whitespace and sentinel-shaped values as=?base64?<Base64 UTF-8>?=), shared byMcp-Param-*andMcp-Name.x-mcp-headerannotations are found through nested properties chains (bounded depth/node walker); annotations under dynamic positions invalidate only that tool. Runtime values are type-checked against the declared schema before sending. - Startup/tool timeouts accept floating-point seconds (current Codex accepts 0.5/1.5-style values); sec-over-ms precedence unchanged.
All new regressions run against the real TypeScript bridge with localhost fake servers (zero model calls); red-verified 13 failures on a030a1d.
0.2.5 — 2026-09-15
Three P1 fixes, each red-verified against 04a46db.
- P1-A (x-mcp-header): the annotation is a SCHEMA declaration on plain-typed properties, not an argument. Discovery parses inputSchema.properties into an in-memory extraction plan, validating per the 2026-07-28 rules: non-empty HTTP token, case-insensitively unique, no control characters, plain string/integer/boolean types only, no array/items/oneOf/anyOf/allOf/not/ conditional/$ref dynamic paths. Modern HTTP tools/call mirrors declared arguments as Mcp-Param-* headers with the body unchanged; an absent argument produces no header; unsafe integers and control-character values are refused before sending. Tools with invalid annotations are excluded from the catalog and rejected by describe/call without failing the server; stdio and legacy connections ignore the annotation. The previous arguments-based check was removed.
- P1-B (proxy serialization): codex_mcp is registered with executionMode "sequential" AND serializes in memory via a promise chain — concurrent sibling calls never overlap server-side; a cancelled first call never poisons the chain. supports_parallel_tool_calls diagnostics now state the enforced behavior.
- P1-C (stdio generations): a StdioConnection maps to exactly one handshake generation. When its process dies (exit or EPIPE), the connection is dead: the in-flight/pending operation fails, tools/call is never replayed, and the next explicit operation builds a NEW process whose first RPC is initialize. No respawn ever happens inside a dead connection.
- startup_timeout: sec wins over ms when both are present (current Codex semantics), correcting 0.2.4's "ms wins".
New regressions (all localhost fakes, zero model calls): schema-annotation header mirroring against a strict modern server, concurrent proxy calls with server-side start/end ordering, confirm-pending process death (exit and EPIPE) with per-process first-RPC checks. Existing lifecycle tests updated for the serialized proxy.
0.2.4 — 2026-09-15
Two MCP/Codex compatibility P1 fixes, each verified red against 6741777.
- P1-A (protocol eras): the HTTP transport now has an explicit protocol
compatibility layer —
legacy_2025_06_18,modern_2026_07_28,auto(configured once via[inheritance] mcp_protocol_mode, never filled in by the model; stdio stays legacy because managed children never forward the upstream env opt-in). Legacy now NEGOTIATES the initialize version and sends the negotiatedMCP-Protocol-Versionon every later request; a 404 on a session-scoped request marks the connection stale, returns "outcome is unknown" for a sent call, never replays it, and the next explicit operation re-initializes a fresh session.autoprobes withserver/discoverand falls back to the legacy handshake ONLY on proof of legacy-only (HTTP 404/405 or JSON-RPC -32601 on the side-effect-free discover). Modern mode is stateless: no handshake, every request self-describes via_meta(protocolVersion/clientInfo/clientCapabilities) plusMCP-Protocol-Version,Mcp-Methodand (for tools/call)Mcp-Nameheaders.x-mcp-headerargument mirroring is refused (model-controlled values must not become HTTP headers). Legacy sessions are closed with a best-effort DELETE (documented boundary). - P1-B (config compatibility): the two hardcoded stdio/http key whitelists are
replaced by one declarative classification table for the current Codex
RawMcpServerConfigsurface (mapped / accepted_no_effect / explicitly_unsupported / unknown_fail_closed), reported by doctor asbaseline.startup_timeout_msmaps with Codex precedence overstartup_timeout_sec(no truncation);supports_parallel_tool_callsand the legacynamelabel are accepted without effect;environment_idis local- only;omit_tools_from,scopes,oauth,oauth_resourcefail/exclude with precise diagnostics instead of disabling servers for unknown reasons; tool-leveloutput_token_limitis enforced at the proxy serialization boundary as a tighten-only 4 bytes/token budget. Genuinely unknown fields still fail closed.
New conformance fixtures (localhost fakes, zero model calls): a strict modern server that rejects requests missing the required headers/_meta, a legacy server with session expiry, and a legacy-only discovery endpoint; plus the Codex config field matrix and required/optional unsupported-field matrix.
0.2.3 — 2026-09-14
- P1 (receipt fd ownership): the receipt read end now has exactly one closer.
_read_receipttakes ownership of the fd on entry and closes it exactly once on every path (success, malformed receipt, required-server failure, timeout, cancellation, transport-creation failure);boot_workertransfers ownership BEFORE awaiting, so its failure cleanup can no longer close an fd number that another connection reclaimed duringterminate. Previously a boot failure could double-close the receipt fd and kill an unrelated agent's RPC pipe or CLI/MCP IPC socket in the shared daemon (reproduced with a real socketpair reclaiming the freed number: peer EPIPE / writer EBADF). Regressions: boot-level test re-occupies the freed receipt fd with a live socketpair inside the cleanup window and asserts it stays functional, plus fd-closed-exactly-once tests for the timeout, cancellation and transport-creation-failure paths. Red verified against a0cf6cb.
0.2.2 — 2026-09-14
Five P1 fixes found by the d7ff1e9 review; each has a regression that was verified to FAIL against the unfixed code (red/green) and PASS after the fix.
- P1-A (HTTP exchange lifecycle): the deadline, the caller's cancellation and
connection close now cover the WHOLE exchange — send, headers, body and
parsing — through one AbortController armed until settlement.
close()aborts every in-flight exchange and rejects new ones until an explicit reconnect re-handshakes. Timeout and user cancellation stay distinguishable and keep an explicit "server outcome is unknown" for calls that were sent. - P1-B (read-child policy): the parent's
enabled_toolsis no longer treated as child authorization. A read child sees only tools explicitly declaredreadOnly(deny rules still win; an explicit allowlist can only shrink the surface), always confirms before calling, and a confirmation can never upgrade the worker. Write children keep the per-tool/server policy. - P1-C (stdio async EPIPE): all writes go through one controlled
sendFrame; the stdin socket gets a real error listener installed before the first write, which settles this connection's pending requests with a deterministic transport error and marks the connection for reconnection. Best-effort cancellation notices cannot crash the worker or turn a cancellation into a success; stale bytes from a replaced process are dropped by generation. - P1-D (tool discovery):
action=listwith a server connects that one server and returns its VISIBLE tool names + short descriptions (parent deny/allow and child access rules applied), with bounded pagination and an honesttruncatedflag (size caps drop whole entries instead of cutting JSON). The catalog invalidates ontools/list_changed; an in-flight crawl no longer repopulates a cache that was invalidated mid-fetch. - P1-E (base environment vs inheritance): binding the worker base environment
(PATH/HOME/authorized child_env names) now happens on EVERY scope bind,
independent of the inheritance master switch; when the switch is off, no
Codex source is read at all (verified with a FIFO config that would block).
A scope whose source binding was lost in a daemon restart fails with
inheritance_source_unboundinstead of silently falling back to the daemon user's ~/.codex.
New test layers (no model calls): bridge-host red/green matrix against real fake stdio/HTTP MCP servers (25 regressions), and a full CLI -> daemon -> guard -> fake-Pi subprocess chain for the environment binding.
0.2.1 — 2026-09-14
Repair release based on the 0.2.0 review. Every finding is a behavior fix with regression coverage; see docs/inheritance.md for the resulting semantics.
- Launch argv: the
codex_mcpbridge tool is merged into the single--toolsallowlist over the full launch argv. Appending a second--toolsflag let Pi's last-flag-wins parser wipe the reader/writer builtin tools (F01). - Codex launcher:
-C/--cdis scanned read-only for scope binding and forwarded to Codex VERBATIM; stripping it left Codex in the launcher's directory while the scope pointed elsewhere (F02). Covers-C DIR,--cd DIR,--cd=DIR,-CDIR, last-wins duplicates, and--separators. - Bridge stdio: timeouts reject through the pending entry (the old timer
callback referenced an out-of-scope
rejectand crashed instead of ending the RPC), spawn failures and mid-call exits reject waiters instead of crashing Pi, per-connection generation isolation, shared initialize promise, reconnects re-handshake, and a failed call is never replayed (F03). - Bridge HTTP: the deadline covers the whole exchange (headers, body and
result parsing), Pi's cancellation signal propagates into the transport,
cancellation is forwarded as
notifications/cancelledwith an explicit "outcome unknown" for in-flight write calls, and bodies/SSE buffers are bounded (F04). - Tool metadata: full
inputSchemais kept in memory (no disk cache) and adescribeaction returns it; MCPisErrorand transport failures surface as real tool errors via thrown errors, per Pi's extension contract (F05). - Approval policy: a structured effective model replaces the auto-tools set —
per-tool override > server default; child
confirm_all(read child without allowlist) wins over any parent-sideauto;writes/unknown modes degrade to confirm; unimplementable tool config (e.g.output_token_limit) denies that tool instead of being ignored. readOnlyHint affects visibility only and never bypasses confirmation (F06). - Environment isolation: guard/Pi children get the scope's own base env plus
explicitly configured
inheritance.child_envnames — never a copy of the daemon's environ; profile env values are re-read from the operator config at boot and only env NAMES are persisted; the bridge gives each MCP server only the base keys plus the server's declared env (F07). - Packaging: the compatibility manifest
.codex-plugin/plugin.jsonis now committed (it was gitignored but unconditionally required by the validator), clean-checkout validation usesgit archiveonly, and installer/packager share stricter exclusion rules (F08). - Tests: the default suite never launches a real Pi (no model calls); the
real-Pi check is gated behind
SUBAGENT_PI_LIVE_PI=1, boots without sending a prompt, and verifies the bridge receipt. A jiti-based host fixture loads the actual TypeScript bridge against local fake stdio/HTTP MCP servers for lifecycle/policy/deadline/cancel evidence, andtsc --stricttype checking is wired via pinned devDependencies (F09). - Diagnostics: Path objects are coerced to strings at the diagnostic boundary, so a missing skills directory can no longer break event serialization (F10).
- Required servers: every declared server keeps a disposition (ok/failed/disabled) with named reasons; any required failure at parse, env resolution or child initialization aborts spawn/respawn before any task is sent; readiness is a structured, non-secret JSON receipt read from a private pipe with agent id and generation, not a substring scan of the capped stderr.log (F11).
- Recursion guard: extended to the installer-generated wrapper
(
python <install>/bin/subagent-pi mcp),python -m subagent_piforms and entry-script args, enforced both in the daemon parser and inside the bridge; renaming the server evades nothing (F12). - Source resolution: a configured
codex_homeor scope-boundCODEX_HOMEthat does not exist is an error instead of a silent fallback; re-binding a scope no longer resets its per-scope inheritance switch; project.agentsskills resolve against the worker's actual cwd. - Bootstrap writer: the pipe fd is owned and closed by the writer thread (a timeout abandons the wait, never closes the fd mid-write).
0.2.0 — 2026-09-14
Codex inheritance for managed children (normal pi untouched):
- Source resolution: explicit trusted
codex_home→ scope-boundCODEX_HOME→~/.codex; per-scope binding persisted as non-secret fields; conflicting rebinds require an explicit management action;subagent-pi codexparses Codex's-C/--cd. - Skills: original-path
--skillreferences for project.agents/skills, profile skills, and<codex_home>/skills; realpath dedup,[[skills.config]]disabling, name-conflict refusal, management-skill recursion guard. - MCP: read-only
tomllibconversion of[mcp_servers.*](stdio + streamable HTTP, env/header/bearer references, allow/deny lists, timeouts, approval modes, required semantics); unsupported auth/helper/remote forms disable the server with named diagnostics; in-memory only. - Private bootstrap: anonymous pipe daemon → worker guard → Pi (
pass_fdson every hop, async parent write for payloads beyond the pipe buffer), consumed once byextensions/codex-mcp-bridge.ts(zero npm dependencies, explicit--extensiononly, singlecodex_mcplist/call tool), verified by a non-secret stderr ready receipt before any task is sent. - Read children: MCP exposure is the intersection of server policy and child
limits; confirmations flow through the existing
pi_answer_agentchannel. - Persistence: scopes schema v1→v2 transactional migration (codex_home, source mode, inheritance flag). Secrets never touch the ledger, launch.json, logs, events or diagnostics.
subagent-pi doctor --inheritance,docs/inheritance.md, packaging exclusions for scratch/runtime files, version references derived from the package instead of hardcoded.
0.1.0 — 2026-09-14
Initial source distribution: Codex portable and compatibility plugin manifests, 12 MCP tools, shared CLI/daemon, isolated Pi RPC workers, persistent scopes/runs, idempotent mutations, steering receipts, queued follow-ups, bounded inspection, explicit result acknowledgement, interruption, process-group cleanup and recovery.
Includes local-marketplace installer, offline subprocess/MCP tests, and an opt-in real Pi smoke script. No hooks, automatic model wakeups or native Codex /agents UI.