English • 中文
FerryCat (
ferrycat) is a lightweight, deterministic cross-host AI agent handoff, code review, and verification harness built on the Model Context Protocol (MCP) and Node.js built-innode:sqlite(WAL mode).
Purpose-built to safely bridge isolated desktop IDE agent hosts (such as Google Antigravity, WorkBuddy, Claude Code, and Cursor) across separate OS processes—enabling Actor-Critic evaluation loops, static analysis gates, and SWE-bench-style patch verification without context drift or blind reviews on stale files.
📖 Deep Dive: Why I Started Building FerryCat: When Two AI Coding Hosts Live on the Same Desktop
No IDE setup or config file edits required. Watch the entire deterministic loop execute inside an isolated sandbox in ~2 seconds:
npx ferrycat demo⚡ FerryCat Zero-Config Sandbox Demo ⚡
✔ [1/4] Contract Dispatched Locked: src/calc.ts (sha256: ce3ba9a5...bc5d)
✔ [2/4] Contract Claimed Lease: 7424261d-9a3f-4a86-a7ff-de278c273e96
✔ [3/4] Execution & Check Heartbeat 1 renewal | CHANGES_REQUESTED (loose equality caught)
✔ [4/4] Contract Completed TOCTOU: 0 dirty files | Status: COMPLETED (elapsed: 2.1s)
🎉 End-to-end multi-agent handoff verified successfully!
💡 Real benchmark: spins up an isolated child-process sandbox, triggers 1 real lease renewal, and runs a static analysis & verification gate that catches loose equality at
src/calc.ts:6. Options (--keep/--json/--no-color) detailed in Quickstart §0.
While the official MCP Tasks specification (2025-11 / 2026-07) addresses long-running operations within a single Client ↔ Server connection, it does not solve cross-host collaboration on a developer workstation:
stdioProcess Isolation: Desktop IDEs spawn local MCP servers as independent child processes with isolated memory spaces and distinct Node.js runtimes.- Silent Blind Reviews on Stale Code (
Stale Context): When handing off a task from Host A to Host B, unushed IDE buffers, branch switches, or mid-review edits cause the receiving agent to audit outdated code and produce plausible but invalid conclusions. - Context Window Bloat: Pasting hundreds of lines of source code across chat windows duplicates token consumption on both ends.
- Unstructured Handoff Outputs: Free-form prose responses lack deterministic schema enforcement, making downstream consumption unreliable and prone to infinite retry loops.
FerryCat resolves these failure modes through a human-scheduled, contract-driven SQLite WAL bus:
[Developer] [Host A: Antigravity] [~/.ferrycat/mesh.db] [Host B: WorkBuddy]
| | | |
|-- 1. "Dispatch review" --->| | |
| |-- dispatch_contract() -------->| Write status = SUBMITTED |
| | (Full SHA-256 + Line Bounds) | Store size / sha256 snapshot |
| | | |
|-- 2. Switch IDE: "Poll" ---|--------------------------------|------------------------------>|
| | |<-- poll_next_task() ----------|
| | | (Atomic claim changes===1 |
| | | + Verify disk SHA-256) |
| | | |-- Read local files
| | |<-- fulfill_contract() --------|-- Re-verify SHA-256
| | | (Validate Ajv JSON Schema | & submit JSON
| | | -> status = COMPLETED) |
| | | |
|-- 3. Switch IDE: "Inspect" | | |
| |-- inspect_mesh_state() ------->| Read-only query of COMPLETED |
|<-- Present structured res -| | |
Every state transition is bound to an explicit SQL transaction with optimistic concurrency control (changes === 1), eliminating phantom or dangling states:
dispatch_contract()
│
▼
┌───────────┐
│ SUBMITTED │ (Queue TTL: default 1800s)
└─────┬─────┘
│
poll_next_task()
┌────────────────┼────────────────┐
│ (Queue TTL) │ (Hash Match) │ (Missing / Size Diff / SHA-256 Mismatch)
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────────┐
│ TIMED_OUT │ │ CLAIMED │ │ POINTER_STALE │ (Terminal: Rejects blind review)
└───────────┘ └─────┬─────┘ └───────────────┘
▲ │ (Exec TTL: default 300s)
│ fulfill_contract()
│ ┌────────────────┼────────────────┬────────────────┐
└──┤ (Exec TTL) │ (Schema Valid) │ (Retries > 2) │ (Mid-Review File Edit)
│ ▼ ▼ ▼
│ ┌───────────┐ ┌───────────────────┐ ┌───────────────┐
└──────────│ COMPLETED │ │ VALIDATION_FAILED │ │ POINTER_STALE │
└───────────┘ └───────────────────┘ └───────────────┘
Note on
POINTER_STALEvs.SELF_CONTAINEDPatch Mode (FC-202):POINTER_STALEis terminal whenever on-disk pointer files mismatch the dispatched SHA-256 baseline, except when a read-only contract (code_review/architecture_audit) attaches a verified unifiedgit_patch(--patch-file) that satisfies all three gates: every declared file block carries at least one@@hunk with real+/-content (EMPTY_PATCH_HUNKS/EMPTY_PATCH_CONTENTotherwise), and the touched paths cover every pointer file (PATCH_DOES_NOT_COVER_POINTERSotherwise). In thatSELF_CONTAINEDmode,poll_next_taskreturnspatch_mode: "SELF_CONTAINED"with an explicitworkspace_warninginstructing the reviewer to inspectgit_patchrather than stale on-disk files.
Holder-side lifecycle controls on a CLAIMED contract (all require agent_id === target_agent):
| Call | Transition | Guard rails |
|---|---|---|
release_contract |
CLAIMED → SUBMITTED (tail of queue, fresh queue window) |
Resets extend_count (renewal budget is per holder) but preserves retry_count: the schema-failure circuit breaker is a contract-level quota, so releasing cannot launder retry budget. Refused past the absolute lifetime deadline. Max 3 releases per contract. |
extend_ttl |
CLAIMED → CLAIMED (claimed_at = now) |
Must be called inside the active window; max 5 renewals per holder; refused once now ≥ created_at + 4 × queue_ttl_sec. |
fail_contract |
CLAIMED → FAILED (terminal) |
Requires a reason, stored as WORKER_FAILED: <reason>. A report arriving after the window closed becomes TIMED_OUT with the reason attached. |
- Dispatch Snapshot:
dispatch_contractreads target files on disk, recording bytesize, full-filesha256(64 hex chars),first_line, andlast_line. If a file was modified within the last 3 seconds, awarningis emitted to alert the requester of potential unsaved IDE buffers. - Claim Verification:
poll_next_taskverifiesExistence -> Byte Size -> Full-File SHA-256(never rejecting onmtime_msalone to tolerate no-op IDE saves). On mismatch, it waits150msasynchronously (non-blocking, FC-203) and retries once to rule out transient IDE flushes before marking the contractPOINTER_STALE— unless the contract is a read-only review/audit carrying a verified unifiedgit_patchcovering all pointer files (SELF_CONTAINEDpatch mode, FC-202), in which case it claims with an explicitworkspace_warningand records__verified_on_disk__: "NO"and__unverified_pointers__inverified_sha256. - Pre-Flight Verify (
verify_contract): A strictly read-only check allowing workers or scripts to audit pointer health (valid: truevsvalid: falsewith dirty files list) before submitting. - TOCTOU Soft-Failure Buffering:
fulfill_contractchecks on-disk pointer fingerprints. If dirty pointers are detected:- Strike 1 returns
RETRY_NEEDED, leaves the contractCLAIMED, renews the execution window, and lists dirty files with recovery instructions (git stash/git checkout -- <file>). - Strike 2 transitions to terminal
POINTER_STALEand saves the full submitted review payload intosalvaged_result_jsonso review effort is never lost.
- Strike 1 returns
- TOCTOU Race Narrowing: While no file-based system can eliminate the microsecond race between reading disk bytes and committing a database row, FerryCat's dual-verification (at
polland atfulfill) drastically narrows the TOCTOU window from minutes of blind review down to the sub-millisecond atomic SQL transition boundary.
- Outputs are validated against the contract's JSON Schema via cached Ajv validators, with errors flattened into concise single-line paths (e.g.,
["/passed: must be boolean"]). - Up to 2 validation failures return
RETRY_NEEDEDwhile keeping the contractCLAIMEDfor immediate self-correction (each rejection also renews the execution window); a 3rd failure transitions the contract to terminalVALIDATION_FAILED.
- Separates
queue_ttl_sec(default 1800s forSUBMITTED) fromexec_ttl_sec(measured fromclaimed_at), preventing tasks claimed late in the queue window from timing out prematurely during execution. exec_ttl_secdefaults are tiered bytask_type:code_review/architecture_audit→ 1800s (deep reasoning or human-in-the-loop),implementation/test_generation→ 300s. An explicitexec_ttl_secalways wins.poll_next_taskexecutes a lock-freeSELECT COUNT(*)probe before attempting anyUPDATEon expired rows;inspect_mesh_stateis strictly read-only.
- Enforces path containment within
workspace_rootand blocks sensitive file patterns (.env*,*.pem,*.key,id_rsa*,*credentials*,*secret*,.npmrc,.git/config) on bothdispatchandworkerverification paths. - Path Resolution:
workspace_rootis strongly recommended to be passed as an absolute path. If a relative path is provided, it is deterministically resolved againstprocess.cwd()viapath.resolve.
- Assumed Trust Environment: FerryCat is intentionally designed as a Trusted Local Workstation Bus bridging isolated IDE hosts operating under the same OS user account. Callers and workers share the local file system (
workspace_root) and SQLite database (~/.ferrycat/mesh.db). - Defense Scope: Path containment (
assertSafePointerPath) and sensitive file pattern filters prevent unintentional workspace escapes, symlink traversals, or accidental leakage of credentials/secret files during handoffs. - Daemon Execution Surface: In daemon mode (
ferrycat-cli daemon), handler scripts are invoked directly viaexecFile. Write permissions toferrycat.config.jsonor handler script paths are equivalent to host execution privileges (ADR-0005 §2.2). Keep configurations local and version-controlled. - Out of Scope (Non-Goals): FerryCat does not provide adversarial sandboxing, multi-tenant isolation, or network authorization against malicious local processes or adversarial agents spoofing
AGENT_NAME.
- Disjoint File Systems: FerryCat offloads context by file reference; both hosts must share access to the same local
workspace_root. - Trivial Single-Turn Queries: Each cross-host handoff involves 3 explicit human window switches (~15–25s overhead).
- Multi-Step Dynamic DAG Pipelines: FerryCat's background daemon (
ferrycat-cli daemon, ADR-0005) manages single-step contract dispatch and supervised worker lifecycles; for complex multi-step branching DAG orchestrations, use dedicated DAG workflow engines such as PatchCat.
FerryCat provides a controlled daemon mode governed by SQLite microsecond CAS leases on daemon_locks and declarative scripts defined in ferrycat.config.json (ideal for binding static analysis gates like tsc / eslint / AST checks or automated test suites like Jest / Vitest / Pytest as deterministic verification workers):
# Run a single dispatch cycle with declared handlers
ferrycat-cli daemon --config ./ferrycat.config.json --once
# Start continuous daemon loop
ferrycat-cli daemon --config ./ferrycat.config.json
# Inspect lease ownership + write-ahead journal health (read-only)
ferrycat-cli daemon --statusdaemon --status reports journal_degraded / last_journal_error / journal_path / journal_entries alongside lease ownership, so a silently unwritable write-ahead journal (which would disarm crash recovery) is visible instead of failing mute.
Example ferrycat.config.json:
{
"version": 1,
"daemon": {
"poll_interval_ms": 1000,
"lease_ttl_sec": 30,
"heartbeat_interval_ms": 10000,
"notify": true
},
"agents": {
"workbuddy": {
"handlers": {
"code_review": {
"exec_file": "./scripts/review-handler.js",
"pass_env": ["CUSTOM_ANALYZER_PATH"]
}
}
}
}
}Before configuring any MCP server, watch the whole loop in ~3 seconds:
npx ferrycat demo(or npx -p ferrycat ferrycat-cli demo)
It spins up a fully isolated sandbox under the OS temp dir (dedicated SQLite database,
sample source file, sample handler), runs dispatch → claim → supervised execution → TOCTOU re-verification → COMPLETED, then deletes the sandbox.
Your real workspace and ~/.ferrycat/mesh.db are never read or written.
⚡ FerryCat Zero-Config Sandbox Demo ⚡
Sandbox: /tmp/ferrycat-demo-c5iK6V
✔ [1/4] Contract Dispatched
Locked src/calc.ts (sha256: ce3ba9a5...bc5d)
✔ [2/4] Contract Claimed
Lease 7424261d-9a3f-4a86-a7ff-de278c273e96
✔ [3/4] Execution & Schema Check
Heartbeat 1 lease renewal(s)
✔ [4/4] Contract Completed
TOCTOU fingerprints matched (0 dirty files)
🎉 End-to-end multi-agent handoff verified successfully!
| Option | Effect |
|---|---|
--json |
Emit a structured result for CI / upstream assertions |
--keep |
Keep the sandbox and print its path for manual sqlite3 inspection |
--no-color |
Disable ANSI colors (auto-off when not a TTY or NO_COLOR is set) |
No local clone or manual compilation required. Mount directly into your IDE's MCP configuration using npx -y ferrycat:
Edit %USERPROFILE%\.gemini\antigravity\mcp_config.json (or ~/.gemini/antigravity/mcp_config.json on macOS/Linux):
{
"mcpServers": {
"ferrycat": {
"command": "npx",
"args": ["-y", "ferrycat"],
"env": {
"AGENT_NAME": "antigravity"
}
}
}
}Configure the target host's MCP settings with its respective AGENT_NAME:
{
"mcpServers": {
"ferrycat": {
"command": "npx",
"args": ["-y", "ferrycat"],
"env": {
"AGENT_NAME": "workbuddy"
}
}
}
}- Global CLI Installation:
Then simply set
npm install -g ferrycat
"command": "ferrycat"in your MCP config. - Local Source Build (For Contributors):
Mount using your local path:
git clone https://github.com/GuoBug/FerryCat.git cd FerryCat npm install npm run build npm test
"command": "node","args": ["--experimental-sqlite", "/path/to/FerryCat/dist/index.js"].
Beyond serving as an in-IDE MCP Server, FerryCat provides a first-class CLI utility (ferrycat-cli). The single ferrycat executable dispatches on its first argument: no argv starts the MCP stdio server, a known subcommand routes to the CLI. So you can run npx ferrycat <command> with zero install (or ferrycat-cli <command> once installed globally). Sharing the same ~/.ferrycat/mesh.db SQLite WAL database and Zod validation contracts, the CLI allows developers to inspect mesh health, verify working tree safety, and hook into automated CI/Git scripts without leaving their terminal:
Inspect all cross-host handoffs and error reasons formatted as an ASCII table:
# View all contracts in an ASCII table
npx ferrycat inspect
# Filter by target agent (e.g., only tasks for antigravity)
npx ferrycat inspect --target antigravityAudit on-disk pointer file integrity against the contract's baseline SHA-256 before committing review work (ADR-0004), eliminating false-positive deliveries:
npx ferrycat verify <CONTRACT_ID>Poll the queue, claim tasks, and execute local review/test scripts with automatic failure closure:
# Safely run a script upon task arrival (no shell quotation hazards)
npx ferrycat worker --agent codex --mode=exec --exec-file ./scripts/review.shRun the self-contained multi-agent lifecycle simulation without configuring an MCP server:
npx ferrycat demoOptions (--keep to retain SQLite for manual inspection, --json for CI assertions) detailed in Quickstart §0.
Run the headless orchestration daemon coordinating background workers via atomic CAS leases and declarative script bindings:
# Launch daemon using project-local declarative configuration
npx ferrycat daemon --config ./ferrycat.config.json
# Run single poll/dispatch iteration for a targeted agent and exit
npx ferrycat daemon --agent workbuddy --once| Tool Name | Role | Transactional Behavior |
|---|---|---|
dispatch_contract |
Requester | Enforces path blacklist git_patch / --patch-file validation covering pointers, FC-202) SUBMITTED. |
poll_next_task |
Worker | Sweeps expired TTLs (read-before-write) changes === 1 (CLAIMED) SELF_CONTAINED patch mode with workspace_warning (otherwise marks POINTER_STALE). |
fulfill_contract |
Worker | Re-verifies SHA-256 (TOCTOU guard) RETRY_NEEDED, 3rd marks VALIDATION_FAILED) COMPLETED. |
release_contract |
Worker (holder) | Hands a CLAIMED contract back to the queue tail as SUBMITTED. retry_count is preserved (contract-level circuit breaker); extend_count is reset. Max 3 releases; refused once past the absolute lifetime deadline. |
extend_ttl |
Worker (holder) | Heartbeat: restarts the execution window (claimed_at = now), max 5 renewals and bounded total lifetime. |
fail_contract |
Worker (holder) | Marks a CLAIMED contract terminal FAILED with a mandatory reason. |
verify_contract |
Worker / Any (Read-Only) | Pre-flight check: validates on-disk pointer files against the baseline SHA-256 without mutating contract state (ADR-0004). |
inspect_mesh_state |
Any (Read-Only) | Pure read-only query of contract states, outputs, patch_sha256 / patch_bytes (opt-in include_patch: true), and dynamic is_logically_expired flags. |
💡 Architecture Decision Records & Engineering Verification:
- ADR-0001: Cross-Host SQLite WAL Bus & Human-Scheduled Poll Model (Why
node:sqliteWAL +poll_next_taskinstead of 60s Blocking Hold or single-connection MCP Tasks)- ADR-0002: Full-File SHA-256 Fingerprinting, TOCTOU Guard & Cross-Model Audit (Stale-context prevention and real-world cross-host verification records)
- ADR-0003: GUI Sandbox vs CLI Process Hybrid Layering Architecture & Wake-Up Boundaries (Desktop HITL approval vs in-house CLI dual-channel autonomy)
- ADR-0004: Separation of Review and Mutation (SRM), TOCTOU Soft-Failure Buffering & Immutable Baseline Protocol (How to prevent review abortion from concurrent or well-intended reviewer edits; live mesh case: contract
fc_1793098036393_aea8bb9b)- ADR-0005: Controlled Daemon, Atomic CAS Micro-Leases & Safe Execution Boundaries (Microsecond atomic CAS leases on
daemon_locksand declarative whitelist handler enforcement)- ADR-0006: Dual-Track TOCTOU Semantics & Post-Mutation SHA-256 Fulfillment (Why
implementation/test_generationtasks commit post-mutation SHA-256 intoverified_sha256while preserving pre-mutation baselines inpointers; live mesh case:fc_1793100801409_4eedf544)- FerryCat Requirements Backlog & Roadmap
About the Author
Guo Qiang (GuoBug), Product Engineer specializing in platform engineering and growth. Currently focused on AI workflow orchestration, DAG state machines, and deterministic system architectures.
Open Source & Homepage: https://github.com/GuoBug · https://guobug.github.io
Inquiries and discussions on workflow engine architecture, topological scheduling, and developer experience are warmly welcomed.