Agent Crossbar
Delegate review, advice, text, and dev work to local coding agents — Codex, Claude, OpenCode — through a single MCP server. One agent_start call, one job_result answer.
Experimental developer preview (v0.3). APIs may change. Provider guarantees are qualified by live gates.
Expose to MCP clients with the server key agents.
Ten-Minute Quickstart
1. Install
# Canonical: uvx pulls the latest PyPI release
uvx agent-crossbar
# Or via npm (thin launcher → delegates to uvx)
npx agent-crossbar
Prerequisites: uv is required for both launch paths because the npm package is
only a thin launcher around uvx; install it from the
uv documentation.
The npm path additionally requires
Node.js ≥ 20.
2. Check Readiness (doctor)
uvx agent-crossbar doctor
# Optional: check one provider and emit machine-readable output
uvx agent-crossbar doctor --profile codex --json
Verifies that supported provider CLIs are installed, authenticated, and runnable. A provider must be ready before jobs can be created.
3. Configure Your MCP Client
Codex
For a user-wide installation shared by the Codex app, CLI, and IDE extension:
codex mcp add agents -- uvx agent-crossbar
codex mcp list
This writes the native Codex MCP configuration to ~/.codex/config.toml.
The equivalent explicit TOML is:
[mcp_servers.agents]
command = "uvx"
args = ["agent-crossbar"]
For a trusted-project-only installation, put the same TOML table in
.codex/config.toml inside that repository. Codex does not use
Claude Code's .mcp.json format.
Claude Code
Claude Code uses the native claude_bg backend (claude profile). Interactive follow-ups attach a harness-owned terminal to the same background session; print mode remains disabled because claude -p uses separate Agent SDK credit/metered billing — read Claude Billing below.
For a user-wide installation:
claude mcp add --scope user agents -- uvx agent-crossbar
claude mcp get agents
Use --scope project instead to create a shareable project-root .mcp.json,
or omit --scope for Claude Code's private local-project scope.
Claude prerequisite: authenticate with claude auth login. The doctor will report needs_auth until you do.
OpenCode
Add this to the global ~/.config/opencode/opencode.json or to a project-root
opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"agents": {
"type": "local",
"command": ["uvx", "agent-crossbar"],
"enabled": true
}
}
}
Then verify it with opencode mcp list.
4. First Review Flow
With the MCP server running, from any MCP client:
1. profiles_list → see available profiles and their tiers
2. profile_health → verify readiness before creating jobs
3. agent_start(
profile="codex",
model="gpt-5.6-sol",
prompt="Review my uncommitted changes for security issues.",
task="review"
) → creates a review job
4. job_tail(job_id="<id>") → stream real-time output
5. job_result(job_id="<id>") → get final structured result
Tools (8)
Exact 8-tool MCP surface. No hidden tools, no deprecated aliases.
Session Isolation
By default, all job-access tools (job_tail, job_result, job_stop,
job_send, job_list) are scoped to the requesting client_session_id.
A client session can only see and operate on its own jobs; foreign jobs
return "error": "job_not_found" with a cross_session_note hint:
{
"ok": false,
"error": "job_not_found",
"cross_session_note": "pass client_session_id=\"*\" for explicit local cross-session access"
}
Explicit Local Cross-Session Access
Pass client_session_id="*" to job_tail, job_result, job_stop,
job_send, or job_list for explicit local cross-session access. This
permits any local client to see and operate on all jobs regardless of the
owning session. No environment variable or token setup is required —
"*" is the literal opt-in string.
Support Matrix
Supported Profiles
model is mandatory for every agent_start request. Agent Crossbar never
chooses or falls back to a default model. Use profiles_list to inspect the
currently available model IDs before starting a job.
Experimental (Installed, Not Guaranteed)
Provider Prerequisites
Claude Subscription vs Print SDK Billing
Agent Crossbar uses Claude's native claude --bg subscription path. This uses your ordinary Claude plan — no separate API billing.
claude -p(print/SDK mode) is disabled — it uses separate Agent SDK metered billing- Claude is interactive-only:
agent_start(profile="claude")rejects an explicit or defaultedinteractive=falsewithinteractive_requiredbefore readiness, admission, lease, or job creation - With
interactive: true, Agent Crossbar opens a harness-ownedclaude attach <session-id>tmux session;job_sendwrites the next turn into that same native session - The background monitor stays alive across
awaiting_inputunder one monotonicmax_runtime_secdeadline, so ajob_sendreply is observed without restarting the monitor or resetting the deadline - When the native session pauses mid-turn, the
awaiting_inputdetail carries the full question text recovered from the tmux transcript (redacted, not cut to the 2 KiB diagnostics prefix) alongside the nativewaiting_forlabel - Profile
claudemaps toclaude_bgfor one-shot calls andclaude_bg_ptyinternally for attach-backed interactive calls - Readiness is validated via
claude auth status --jsonbefore job creation
Owner-Mediated Permissions and Continuation
Two provider pause points are surfaced to the owner through the existing
job_send tool — no new tool and no new agent_start field.
Claude questions. When a Claude interactive turn pauses mid-turn, the job
becomes awaiting_input and job_tail reports the native waiting_for label
plus a question field carrying the full, redacted question text. Reply with
plain text via job_send; the same monitor thread observes the resume on its
next poll.
ACP permissions (OpenCode/DeepSeek). A session/request_permission call
that is not resolved by the existing bounded local-edit auto-allow policy is
held pending instead of being auto-rejected. The job becomes awaiting_input
with a durable pending_request in job_tail:
{
"pending_request": {
"request_id": "perm-1a2b3c4d5e6f",
"kind": "other",
"decisions": ["allow", "reject"],
"command": "rm -rf /tmp/x"
}
}
request_id, kind, bounded tool/command/path(s), and generic allow/reject
choices are exposed; raw ACP option ids are never surfaced, and no decision
can escalate to allow_always. Resolve it with a JSON object as job_send's
text:
{"request_id": "perm-1a2b3c4d5e6f", "decision": "allow"}
Any text that is not a JSON object carrying string request_id and
decision keeps its exact plain-text meaning. A decision for a stale,
unknown, or already-resolved request is rejected with request_not_pending
(never reinterpreted as plain text). Resolution enforces the same
owner/client_session_id policy as any other job_send, including the "*"
wildcard. Pending requests settle (expire/cancel) in the same operation that
terminalizes a job by job_stop or deadline, so a late decision can never
resurrect a terminal job; a durable pending request with no live provider
callback fails closed and cleans up.
OpenCode does not support free-text interactive continuation (interactive is
false), but its ACP permission flow advertises owner_permission_decisions
separately in profiles_list; structured decisions still use job_send while
the ACP connection remains alive.
ACP completion honesty. ok=true, status="completed" now requires native
completion evidence: a prompt response whose stop_reason is not refusal,
max_turn_requests, cancelled, or max_tokens. Non-empty progress output
alone no longer counts as success.
Timeouts
The doctor CLI reports readiness and preflight failures only. It does not report active job deadlines or running-job state.
Local State and Retention
- State directory:
~/.local/state/agent-crossbar(override withAGENT_CROSSBAR_STATE_DIR) - Job storage: one directory per job under
jobs/ - Retention: no automatic cleanup in v0.3 — jobs persist until manually deleted — jobs persist until manually deleted
- Local audit logs: full MCP request and response payloads, including
prompts and results, are written under
telemetry/with owner-only permissions. They follow the same no-cleanup policy in v0.3. - No remote telemetry: these audit logs are not sent remotely; Agent Crossbar does not phone home.
Controller terminal waiter
When a controller starts an asynchronous job, wait for the durable terminal result with the CLI waiter. If the Agents MCP server uses a non-default state root, pass that exact root explicitly; a job ID alone is not enough to locate a job across MCP processes:
uv run --directory <agent-crossbar-repo> \
python -m agent_crossbar.cli wait-job \
--job-id "<job-id>" \
--state-dir "<the-state-root-used-by-Agents-MCP>" \
--timeout-sec 1815
The waiter treats result_not_ready as an expected intermediate response and
never calls job_stop. Exit codes are stable: 0 successful completion, 2
observed deadline, 3 terminal failure/cancellation, and 4 missing or
inaccessible job. Do not infer a stall from a quiet job_tail, no workspace
diff, or absent output.
The waiter intentionally never cancels a job. If an unhandleable blocking
prompt is visible in job_tail, or the declared runtime deadline has elapsed,
use the explicit stop-then-collect wrapper (never for silence alone):
python3 ${CODEX_HOME}/skills/quota-aware-delegation/scripts/terminalize_job.py \
"<job-id>" blocking_prompt
# or, after the runtime deadline:
python3 ${CODEX_HOME}/skills/quota-aware-delegation/scripts/terminalize_job.py \
"<job-id>" runtime_deadline
It calls the existing provider-neutral job_stop lifecycle and then waits for
terminal job_result. result_not_ready, job_tail, and durable metadata do
not authorize replacement; if terminalization itself times out, retain the
original job and report the unresolved state.
runtime_deadline is accepted only when the job metadata records a positive
max_runtime_sec and started_at, and the current time is past that deadline
plus the documented 15-second result grace. blocking_prompt is accepted only
for the durable awaiting_input state.
Development jobs are serialized by a durable per-canonical-cwd writer lease.
agent_start(task="dev") acquires it before provider launch and releases it
only after a terminal result (or stop). Controller-local fallback must use the
same configured state through the quota-aware wrapper, which avoids guessing
the Agents MCP state root:
python3 ${CODEX_HOME}/skills/quota-aware-delegation/scripts/writer_lease.py \
acquire --cwd "<cwd>" --owner-id "<controller-id>" --owner-kind local
# hold the returned token for the full edit/test window, then:
python3 ${CODEX_HOME}/skills/quota-aware-delegation/scripts/writer_lease.py \
release --token "<token>"
The MCP surface remains exactly eight tools; writer-lease commands are an internal CLI/controller path, not MCP tools.
An external job lease is never reclaimed by age while its job is nonterminal, missing, or corrupt. If a job record is truly missing or corrupt, an operator may use the explicit recovery path after preserving the state directory:
python3 ${CODEX_HOME}/skills/quota-aware-delegation/scripts/writer_lease.py \
recover --cwd "<cwd>" --acknowledgement recover-missing-or-corrupt-job
Recovery refuses nonterminal jobs and corrupt lease files; ordinary
controllers must wait for job_result or call the existing provider-neutral
job_stop lifecycle.
The credential-free scripts/acp_quiet_live_harness.py is a lifecycle
regression harness, not a provider E2E. Maintainers can run the real provider
surface gate separately:
uv run --directory <agent-crossbar-repo> \
python scripts/provider_surface_gate.py \
--profile opencode \
--model opencode-go/deepseek-v4-flash \
--task dev \
--max-runtime-sec 1800 \
--artifact-dir /tmp/agent-crossbar-live-opencode-<timestamp>
For the full real chain, Codex root runs gpt-5.6-sol at low, starts the
native Luna coder with gpt-5.6-luna at xhigh, and that coder captures route
output from
uv run --directory <codexbar-mcp-repo> delegation-route --lane implementation, then start the exact routed profile/model/effort
through Agents MCP and run the source-path waiter above with the same Agents
MCP state root. This covers Codex root -> native coder -> delegation_router
-> Agents MCP -> OpenCode. It is a maintainer-only live gate and is not run in
provider-credential-free CI.
Reasonix is retained for credential-free compatibility tests but is excluded
from live gates because its paid quota is exhausted. Use the OpenCode Go
opencode-go/* namespace for DeepSeek live verification.
Maintainer live gate prerequisites
The provider gate is a maintainer-local check. Run it from a checkout with a
fresh uv environment and authenticated provider CLIs already installed:
- Codex:
codex loginand a discovered model fromprofiles_list. - Claude: Claude Code authenticated through its supported subscription login.
- OpenCode Go: an authenticated OpenCode Go account and a live
opencode-go/*model fromprofiles_list.
The repository does not install provider CLIs, handle credentials, or claim a
GitHub-hosted runner can execute these gates. The removed workflow was
unusable on stock ubuntu-latest runners. For a semantic dev gate, use a
fixture that creates a file and runs its tests, then retain the job directory
and job_result evidence for review.
Optional local admission policy
Admission is disabled by default. A configured local Agents process can enable
the private callback protocol through its mcp_servers.agents.env settings:
[mcp_servers.agents.env]
AGENT_CROSSBAR_ADMISSION_MODE = "strict"
AGENT_CROSSBAR_ADMISSION_COMMAND = '["uv", "run", "--directory", "/path/to/codexbar-mcp", "python", "-m", "codexbar_mcp.admission"]'
AGENT_CROSSBAR_ADMISSION_TIMEOUT_SEC = "5"
Strict mode fails closed when the command, timeout, response schema, or exact profile/model/task candidate is missing or malformed. The callback receives canonical request data on stdin and returns a versioned allow/deny decision. It runs in a fixed inherited working directory; the requested workspace is data, not callback process authority. This policy applies to every client in the configured Agents process and does not trust client or session metadata.
Troubleshooting by Error Code
job_stop is idempotent. ACP jobs persist a terminal result even when the
provider process has already exited; running ACP child processes receive
SIGTERM and then SIGKILL after a bounded grace period when necessary.
GUI (browser) jobs are marked terminal first, then their registered run handle
is cancelled: the worker stops polling, clicks ChatGPT's visible Stop action
when one is exposed, retires the browser session, and records whether the
provider stop was actually confirmed. A late provider completion can never
overwrite a stopped result.
Stable error codes are guaranteed across patch versions. The next_action field in job results provides exact remediation.
Environment Variables
Migration note: The old AGENT_HARNESS_* env var names still work but emit a FutureWarning. Rename them to AGENT_CROSSBAR_*. The compat shim will be removed in v0.4.0.
Architecture
MCP Client (Codex / Claude / OpenCode)
│
▼
FastMCP("agents") ← 8-tool MCP surface
│
┌────┼────┐
▼ ▼ ▼
Codex Claude OpenCode ← provider adapters
│ │ │
▼ ▼ ▼
ACP / claude_bg / tmux / GUI ← provider backends
One Python package (agent-crossbar on PyPI). Bounded provider adapters under agent_crossbar.adapters. No separate plugin packages in v0.3.
Contributing
See CONTRIBUTING.md. Quick rules for contributors in AGENTS.md.
License
MIT — see LICENSE.