Open Rust code for the Edison Watch MCP quarantine system agent. It discovers the MCP (Model Context Protocol) servers configured across a machine's host apps (Claude Code, VSCode, …) and — when an admin requires it — quarantines them: moves them off the local config and onto the Edison Watch backend.
The target design (a privileged, non-stoppable, multi-user enforcement daemon) is
specified in docs/architecture.md — read that for the
why behind the decisions (root LaunchDaemon, getpeereid scoping, fail-closed
policy, quarantine-first, level-triggered reconciliation, the frozen fingerprint
contract). This README describes what is implemented today; the repo is
mid-migration from a read-only detector toward that design.
| Crate | Path | Role | Status |
|---|---|---|---|
edison-detectord |
crates/edison-detectord/ | Read-only engine: agent abstraction, discovery, fingerprint, filesystem watcher. Cross-platform, publishable. | Reshaped |
mcp_quarantine |
crates/mcp_quarantine/ | Mutation + state + the reconcile planner. No privilege/IPC/network. | Planner done; seen-store + writers WIP |
mcp_detector_daemon |
crates/mcp_detector_daemon/ | Long-lived macOS daemon wrapping the engine behind a Unix-socket IPC. | Being reworked to the root enforcement model |
Build everything: cargo build --workspace --release. Test: cargo test --workspace.
MCP servers are configured independently by each host app — often in several places per app: a global user-level file, per-project files, and sometimes application state in a SQLite database. Quarantine is imposed by admins, so it must run as a system agent the user cannot stop. That posture — a privileged enforcement agent that assumes the local user is adversarial — drives the whole design.
The read-only engine. Cross-platform, no root, no network.
- Agent abstraction. Each supported host app implements the
Agenttrait:name,is_installed,watch_targets, anddiscover.discovernormalises each on-disk entry into aDiscoveredServercarrying its rawconfig([ServerConfig::Stdio|Http|Unsupported]) and alocation(where + how to mutate it). - Server fingerprint.
fingerprintcomputes the stable identity used to ask "is this server already known to the backend?". It is a frozen cross-implementation contract — byte-for-byte identical to the Python backend and the TS client (sha256(identifier)[:16], secrets templatised first viasecret_detection). Pinned by golden-vector tests. See docs/architecture.md §6. - Event-driven watching.
Watcherusesnotify-debouncer-fullagainst parent directories (editors write configs via atomic rename, which breaks single-file watches) and emitsChangeEvents. (This edge-triggered watcher will be superseded by the daemon's level-triggered reconcile driver — see the design doc.)
Config sources differ in format (JSON / JSONC / SQLite state DB), scope (a
single file can mix global and project entries, e.g. Claude Code's ~/.claude.json
projects map), and location (project configs live inside each project dir).
The Agent trait hides all of this; servers that expose no extractable
command/url (e.g. VSCode extension-provider contributions) are emitted as
ServerConfig::Unsupported — surfaced for reporting, skipped by enforcement.
| Agent | Status | Cargo feature | Notes |
|---|---|---|---|
| VSCode | Implemented | vscode |
Global, per-workspace, and extension/marketplace (state.vscdb) servers |
| Claude Code | Implemented | claude_code |
Global + per-project servers (~/.claude.json, .mcp.json) |
| Cursor, Claude Desktop, Zed, Codex | Planned | — |
[dependencies]
edison-detectord = "0.1"use std::sync::Arc;
use edison_detectord::{Agent, Result, Watcher, clients::{ClaudeCode, VsCode}};
fn main() -> Result<()> {
let agents: Vec<Arc<dyn Agent>> = vec![
Arc::new(VsCode::discover()?),
Arc::new(ClaudeCode::discover()?),
];
let (events, _handle) = Watcher::new(agents).spawn()?;
for ev in events {
println!("{ev}");
}
Ok(())
}discover() uses platform-specific paths; for tests/CI use from_paths(...). Each
agent lives behind its own cargo feature (vscode pulls in rusqlite; both on by
default).
Mutation, persistent state, and the decision logic — no privilege, no IPC, no network (the daemon injects those). Everything here is unit-testable in a tempdir.
- Reconcile planner (reconcile.rs) —
implemented. The pure, level-triggered heart:
plan(observed, oracle, policy) -> Vec<Action>. Quarantine-first — an unknown server is neutralised immediately, then surfaced for disposition; a known one is quarantined silently; the policy-off pass is inert; our ownedison-watchentry and report-only servers are skipped. Being level-triggered, it is inherently tamper-resistant (a restored server simply reappears next pass). - Seen-store (the
KnownOracle) and config writers (quarantine/restore, dispatched onSourceKind) — in progress.
Long-lived macOS process wrapping the engine over a Unix-domain socket. Today
it is the original read-only design: it watches configs gated on Full Disk Access
and reports ChangeEvents; it performs no mutation.
It is being reworked into the privileged enforcement daemon from the design doc:
root LaunchDaemon, one socket with getpeereid per-user scoping, per-user
reconcile workers, enrollment + fail-closed policy, an operator CLI
(install/uninstall/unenroll/status), and a state.json status file. See
docs/architecture.md §4–§10.
.
├── Cargo.toml # virtual workspace
├── docs/architecture.md # design source of truth
└── crates/
├── edison-detectord/ # read-only engine
├── mcp_quarantine/ # mutation + state + reconcile
└── mcp_detector_daemon/ # macOS daemon binary