Skip to content

feat: builtin thin agent plugins (pi/omp/claude/codex/opencode) + bili plugin install CLI - #173

Merged
ranxianglei merged 12 commits into
masterfrom
2026-08-18_builtin-agent-plugins
Aug 21, 2026
Merged

feat: builtin thin agent plugins (pi/omp/claude/codex/opencode) + bili plugin install CLI#173
ranxianglei merged 12 commits into
masterfrom
2026-08-18_builtin-agent-plugins

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

What

Ships the per-agent builtin plugin layer discussed in the 内外呼应 track: a thin (~100 line) extension per host family that registers the proxy's ACP tools natively and forwards executes to the proxy. Zero acp-kernel in the plugin — the proxy stays the single engine, so no kernel-version skew is possible.

bili plugin install <pi|omp|claude|codex|opencode>
bili plugin list
bili plugin remove <agent>

Files

  • src/agent/shared.ts — proxy discovery (/bili/ URL prefix, BILLION_CONTEXT_PROXY env, BILLION_CONTEXT_PLUGIN=0 kill switch) + manifest/tool-forward/status client
  • src/agent/pi.ts / src/agent/omp.ts — pi & omp ExtensionAPI factory (same API shape); stamps x-bili-plugin/-conversation/-context-window headers on provider requests; registers manifest tools at session_start with retry throttle; ToolResult content-block contract per pi-stable types
  • src/plugin-install.ts — per-host installers (pi settings.json packages, omp config.yml extensions, codex config.toml [mcp_servers.bili] with env-stamped proxy origin, claude mcp add -e, opencode mcp.bili environment); all idempotent with .bili-bak backup
  • src/mcp.ts — lazy manifest fetch on first tools/list (no more fetch+exit at startup), 5s timeout
  • src/cli.ts, tsup.config.ts (dist/agent/pi.js + omp.js entries), package.json pi manifest field
  • tests/plugin-agent.test.ts — 11 tests: discovery variants, hostile host shapes, throttle dedupe, install/remove roundtrips for all 5 hosts under fake HOMEs (broken-config refusal, flow-style YAML, TOML table surgery, stale-origin refresh)

Live verification (this machine, isolated HOMEs)

host result
pi tmux session, real compress via plugin path, fold 74→33 msgs, agent stayed alive
omp ext loads (CJS/ESM interop factory), registers tools, manifest hit, replies via proxy
claude claude mcp add user scope, ✔ Connected
codex mcp__bili__* tools listed, acp_status tools/call round-trip through proxy (rollout jsonl evidence)
opencode full initialize → tools/list → tools/call ok:true round-trip
dsh not installed on this machine — noted; MCP path would cover it

Notable bugs found during live testing (all fixed here)

  1. codex sanitizes MCP child env → BILI_MCP_PROXY never reached dist/mcp.js → fell back to dead default origin → eager manifest fetch exited(1) before stdin attached → "connection closed: initialize response" and no bili tools all session. Fix: lazy manifest + env-stamped origin in the TOML at install time (reinstall refreshes a stale origin).
  2. pi ToolResult contract is {content:[{type:"text",text}],isError} — a plain {output} shape crashed the agent mid-session.
  3. jiti CJS/ESM interop: factory must also be reachable as module.exports.default or pi/omp silently skip the extension.
  4. CODEX_HOME replaces the whole .codex dir (config at $CODEX_HOME/config.toml); omp honors PI_CODING_AGENT_DIR, default ~/.omp/agent (no OMP_CONFIG).

Full test suite 464/464, typecheck clean, build ok.

Sisyphus and others added 10 commits August 19, 2026 00:28
- src/agent/shared.ts: proxy detection (/bili/ prefix, BILLION_CONTEXT_PROXY, kill switch), manifest fetch, tool forwarding, status fetch
- src/agent/pi.ts: pi/omp extension factory, zero runtime deps, registers manifest tools at session_start, stamps x-bili-plugin* headers
- src/plugin-install.ts: bili plugin install/remove/list for 5 hosts with backup+idempotency; PI_CODING_AGENT_DIR respected
- tests: 7 new tests (460 total green)
pi replaces the boot session while the awaited manifest fetch is in
flight; registerTool on the captured ctx then throws. Register per
session id instead, catch staleness and defer, and retry from
before_provider_headers (fires per request with a live ctx) as the
safety net.
- pi/omp ToolResult contract: {content:[{type:"text",text}],isError?}
  (crashed pi live when forwarding tool output)
- strict /bili/ detection (first path segment + /https?:// next), protocol check
- registration: per-session-id single-flight, 10s retry throttle,
  before_provider_headers fallback, hostile host shape defense
- AbortSignal passthrough, explicit timeout errors, args default
- createBiliPlugin(agent) factory; omp reports omp, custom agents supported
- plugin install: OPENCODE_CONFIG/OMP_CONFIG are file paths, CODEX_HOME/
  CLAUDE_CONFIG_DIR are dirs; refuse to overwrite broken/non-object JSON;
  installers throw -> CLI exits 1; omp YAML flow-style/multi-key/column-0
  guards + quoted-entry removal; pi stale node_modules entries cleaned;
  codex line-regex table match; claude exec 15s timeout
- cli: --origin/--agent flags, non-empty value validation, plugin-register
  agent param + 5s timeout, HELP covers mcp/plugin-register
…tamped proxy origin

- mcp.ts: fetch manifest lazily on first tools/list instead of
  fetch+exit(1) at startup; codex spawns MCP children with a sanitized
  env, so a missing BILI_MCP_PROXY used to resolve the dead default
  origin and kill the handshake ("connection closed: initialize
  response") before stdin was ever attached. initialize now always
  responds; tools/list reports a JSON-RPC error when the proxy is
  unreachable instead of dropping the server for the whole session.
- plugin-install codex/claude/opencode: stamp the resolved proxy origin
  (BILI_MCP_PROXY env > proxy-origin file > default) into the MCP
  config at install time (TOML env block / claude -e flag / opencode
  environment field); reinstalling codex refreshes a stale origin.
- verified live: codex exec spawns bili MCP, lists mcp__bili__* tools,
  and completes an acp_status tools/call round-trip through the proxy.
- drop stray .bili-coop-build-stamp; .gitignore entry now matches the
  actual harness filename (was .bili-thin-build-stamp)
- plugin list: per-row try/catch so one broken host config (e.g.
  ~/.claude.json) no longer crashes bili plugin list
- mcp: forwardTool gets a 60s timeout (wrapped as a readable error),
  register fetch a 5s timeout; proxy origin resolved per request so a
  restarted proxy on a new port self-heals
- requireDistFile warning: process.stderr.write instead of console.error
  (AGENTS.md: console.error only in cli.ts/index.ts)
- tests: plugin list survives broken config; mcp forwardTool timeout
The roundtrip test asserted the installed extension path with a
forward-slash-only regex (dist\/agent\/omp\.js), which cannot match on
windows-latest where the path uses backslashes (D:\a\...\dist\agent\omp.js).
Use dist[\/]agent[\/]omp\.js so the assertion passes on both platforms.
@ranxianglei

Copy link
Copy Markdown
Owner Author

The design here is the strongest of the open PRs — one shared protocol client (src/agent/shared.ts, zero host imports) serving the pi/omp plugin and the MCP bridge, lazy manifest fetch that fixes the codex env-sanitization crash, per-request origin resolution (env > origin-file > default) that self-heals after a proxy restart, and idempotent installers with one-time .bili-bak backups that refuse to clobber unparseable configs. 409 lines of tests, 465/466 of the full suite pass locally. Two things to fix before I'd merge:

1. The claude test is environment-dependent and fails on any machine with the claude CLI installed.
tests/plugin-agent.test.ts:337 expects pluginInstall("claude") to throw /claude: install failed/, sandboxed via env: { ..., CLAUDE: "definitely-not-a-real-cli" }. But claudeInstall() (src/plugin-install.ts:209) hardcodes execFileSync("claude", ...) — the CLAUDE value is only forwarded into the child's env and is never used as the binary path, so the sandbox is a no-op. Result:

  • CI (no claude on PATH): passes by accident — ENOENT produces exactly the expected error.
  • Developer machines (claude installed, like mine): claude mcp add succeeds → assert.throws fails with "Missing expected exception". I reproduced this: the test suite goes red on 13/14 files.

Suggested fix: honor the env var as the command — execFileSync(env.CLAUDE ?? "claude", ...) in both claudeInstall and claudeRemove (the test's CLAUDE fixture already assumes exactly this contract), or make the test skip when claude is present. Note the test title says "pi/omp/codex/opencode" but the claude assertions are inside it — worth mentioning in the title too.

2. --origin is silently ignored by bili plugin install.
parseArgs puts it in overrides.BILI_MCP_PROXY, but the plugin branch of main() (src/cli.ts:315-340) returns before the overrides→process.env merge at cli.ts:369, and resolveProxyOrigin() (src/mcp.ts:37) only reads process.env / the origin file. I verified: with a stale ~/.bili/proxy-origin file present, bili plugin install codex --origin http://127.0.0.1:9999 stamped BILI_MCP_PROXY = "http://127.0.0.1:46657" (the stale file value) instead of 9999. An explicit flag being overridden by a stale file is a trap. Two-line fix in the plugin branch before dispatch:

if (overrides.BILI_MCP_PROXY) process.env.BILI_MCP_PROXY = overrides.BILI_MCP_PROXY;

(same for --agent if you want it meaningful there too).

Minor nits (non-blocking):

  • retryTimer is declared in RegisterState (src/agent/pi.ts:68) but never assigned or read — dead field; remove it, or wire up timer-driven retry if that was the intent (today retry only happens on the next host event).
  • Convention inconsistency: console.error in pi.ts (5 sites) vs process.stderr.write in plugin-install.ts:71. Your own commit note says console.error is reserved for cli.ts/index.ts — pick one for agent-bundled code (I'd take stderr.write everywhere).
  • claudeRemove throws "claude: remove failed" when the claude CLI is absent even if bili was never installed — check claudeStatus() first and report "not installed" in that case.
  • fetchJson (src/agent/shared.ts:56): if externalSignal is already aborted, the listener never fires and the request runs until its internal timeout. Bounded and harmless, but if (externalSignal?.aborted) ctrl.abort() up front makes it exact.

Blockers:
- claude install/remove now honor CLAUDE as the binary path; tests inject
  a missing stub so the failure-path assertion no longer depends on the
  claude CLI being absent from the machine running the suite.
- `bili plugin install <agent> --origin` was silently dropped: the plugin
  branch returns before the generic overrides->process.env merge, so a
  stale ~/.local/state/billion-context/proxy-origin discovery file won.
  Apply the flag to the env before dispatching the subcommand.

Nits:
- drop dead retryTimer field on RegisterState; cache empty session ids so
  hosts without a sessionManager stop refetching the manifest per request.
- fetchJson: an already-aborted external signal never fires "abort";
  forward the state immediately instead of waiting out the timeout.
- claude remove is a no-op when nothing is installed (matches other agents).
- proxy-origin discovery file: respect --host (collapse wildcard/localhost
  binds to 127.0.0.1, bracket bare IPv6 literals) instead of hardcoding.

Tests: 467/467 pass, typecheck clean; --origin end-to-end verified against
a stale state file (9999 wins over 46657).
@ranxianglei

Copy link
Copy Markdown
Owner Author

Addressed both blockers and the nits in 30fbefb:

Blockers

  1. claudeInstall/claudeRemove now honor CLAUDE as the binary path (absolute path for sandboxed setups). Tests inject CLAUDE=/nonexistent/bili-claude-stub so the install failed assertion is deterministic on machines that do have the real CLI (verified locally: suite failed pre-fix, 467/467 post-fix).
  2. --origin is now applied to the env before the plugin subcommand dispatch — previously the branch returned ahead of the generic overrides→env merge, so a stale ~/.local/state/billion-context/proxy-origin discovery file silently won. End-to-end verified: plugin install codex --origin http://127.0.0.1:9999 now stamps 9999 instead of the stale 46657.

Nits: dead retryTimer field dropped; empty session ids cached (no per-request manifest refetch); fetchJson forwards an already-aborted signal instead of waiting out the timeout; claude remove is a no-op when not installed; proxy-origin discovery file respects --host (wildcard/localhost→127.0.0.1, bare IPv6 bracketed).

Local: 467/467 tests + typecheck clean.

@ranxianglei
ranxianglei merged commit 52eb3cc into master Aug 21, 2026
5 checks passed
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