Skip to content

Releases: chapmanjw/rutherford-mcp-server

v3.2.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 18:26

Added

  • direct_workspace_mutation on delegate, behind an operator opt-in — a write / yolo agent can
    edit working_dir itself, with live terminal access there, instead of the isolated worktree. This is for
    work whose product is the effect on the tree rather than a diff: installing dependencies, running local
    tooling, letting an agent see its own side effects. Contributed by
    @Artemonim in [#21].

    It gives up everything the sandbox provides — the clobber and concurrent-edit guards, symlink containment,
    the committed-HEAD starting point, and the diff itself — so the run leaves no record of what it changed
    and a failure may leave a partial one. The party asking for that is also the party least able to judge it,
    so a request never suffices on its own. An operator has to enable
    allow_direct_workspace_mutation in config, and the directory has to be on the trusted_workspaces
    allowlist: a per-call trust_workspace=true deliberately does NOT qualify, because a caller that can set
    it could otherwise authorise its own unsandboxed writes and the allowlist would decide nothing.
    working_dir must be named explicitly rather than inherited from the server's own directory, the call
    must not be nested inside another delegation, and propose cannot use it at all.

    Each admitted run logs before launch and again on completion, and the result carries
    direct_mutation=true so a reader can tell the absent diff means "never captured" rather than "nothing
    was written". That record is best effort: it goes through the ordinary structured logger, whose
    stderr sink is asynchronous so a host that never drains its pipe cannot freeze the event loop, which
    means the record can be dropped under log saturation or lost if the process dies before it is written.
    docs/security.md says so plainly and points an operator who needs a durable trail at collecting stderr
    or enabling persistence, both of which live outside this process. Refusing to launch unless the write was
    confirmed was built and removed; the reasoning is recorded in docs/security.md rather than the code.

    The nesting condition is documented as defence in depth rather than a boundary. Depth crosses the process
    boundary in an environment variable, and anything able to spawn a nested Rutherford controls that child's
    environment, so it stops an accident rather than a hostile agent — which, per docs/security.md, gains
    nothing here it did not already have, since a write / yolo agent was never OS-jailed.

Changed

  • FastMCP is bounded at the major, and the dependency set users actually resolve is now tested. The
    pin was fastmcp>=3.3 with no ceiling. CI installs with uv sync --locked, but the lock is not shipped
    and the published install instructions carry no constraint, so a fresh uvx rutherford-mcp-server
    resolved fastmcp 4.0.3 while the lock held 3.3.1 — every release so far ran on a dependency major its
    own CI never executed
    . It worked, with one observed regression: under 4.0.3 a tool error writes a
    rich-formatted, non-JSON line to stderr, breaking the one-JSON-object-per-line contract the structured
    logger maintains deliberately. The forward risk is larger, because the server calls
    mcp.run(transport=…, show_banner=…) with keywords a major is free to rename — a failure that lands at
    boot, on every install, with a green build.

    The bound claims only the major that has been run, and the lock now matches what a fresh resolve picks,
    so the two are no longer describing different software. A new CI job resolves unlocked and boots the
    built wheel over stdio, which is the only check here that exercises what ships.

  • The ACP SDK pin moves to 0.12.1, and Dependabot now tracks it through the uv ecosystem. The
    release is a much larger change than its patch number suggests: it deletes the pluggable dispatcher,
    queue and state-store layer outright and drops four keyword arguments from Connection.__init__.
    None of that reaches this package, which drives the SDK through spawn_agent_process rather than
    assembling a connection by hand, so no removed symbol is named here.

    What made the bump acceptable is that _deserialize.py is byte-identical to 0.12.0 — the
    salvage-instead-of-reject behaviour that is the real hazard of this dependency, and the reason the pin
    exists, is unchanged, so the guards written against it still hold. Connection.close also got safer:
    it rejects pending requests first and moves the task shutdown into a finally, while still latching on
    an already-closed flag, which is the property the non-cancellable teardown stage depends on. The
    regenerated schema is the part to watch next time — four id fields became required, the content-block
    and config-option unions gained discriminators, and several open str fields narrowed to Literal
    unions, including the config-option category that the model and effort channels read.

    The Dependabot ecosystem changes from pip to uv because the pip ecosystem updates the manifest
    without regenerating uv.lock, and CI installs with --locked. Every dependency PR therefore failed
    all nine matrix cells on a stale lockfile rather than on anything about the dependency.

  • The ACP SDK is now pinned to 0.12, and read as an untrusted source rather than a validating one. The
    0.11 pin was raised after checking what actually changed: the protocol version is unchanged, both model
    channels are intact, and one unused public name went away. What did change is deserialization, and it
    changes what the library promises rather than what it exposes. A field the SDK cannot parse is no longer
    rejected — it is salvaged into a raw dictionary and returned through an attribute still annotated as a
    model — and an unparseable item in a list is now skipped so the rest of the list can parse, where the whole
    response used to fail.

    Both behaviours are reasonable for a protocol library and neither is announced at a call site, which is the
    problem: they silently retire validation this client was leaning on. Nothing here reads an agent-controlled
    response field off its annotation any more. The pin is one exact release rather than a range or a minor
    wildcard, because the lock file is not shipped and the published install command carries no constraint of
    its own, so this is the only bound that reaches a user. A minor wildcard would not have helped: ==0.12.*
    and <0.13 admit the same unreleased 0.12.z patches, and the lesson of this very bump is that a
    compatible-looking release can retire validation without touching a signature. Only the release actually
    run through the malformed-payload suite is claimed.

Fixed

  • An agent's launch path now resolves to its real on-disk filename case. shutil.which never reads the
    directory entry: it returns the caller's own spelling joined to the directory, plus — on Windows — each
    PATHEXT entry appended verbatim. An uppercase PATHEXT therefore names kiro-cli.EXE for a file whose
    dirent is kiro-cli.exe. Windows opens either spelling, so the process starts and nothing looks wrong;
    but a launcher shim that looks its own argv[0] basename up in a case-sensitive table finds no entry,
    prints one line, and exits before reading a byte of stdin. Every seat behind such a shim failed in under
    0.15s, and connect_only failed identically, because the death is at spawn and never reaches initialize.

    The normalization runs on every platform rather than under a Windows guard. The PATHEXT mechanism is
    Windows-only but the defect is not: macOS ships a case-insensitive filesystem by default, where which
    likewise returns the caller's spelling for a differently-spelled dirent and execve passes it through
    unchanged. On a case-sensitive filesystem the exact-match branch returns the input untouched, so it is
    self-neutralizing there. It deliberately does not use Path.resolve() / realpath, which would also
    follow links and pin a version-managed node shim to one concrete install directory, and it cannot use an
    exists() probe, which is case-insensitive on exactly the platforms carrying the bug. Where two entries
    differ only in case and neither matches exactly, the input is returned rather than guessing at a different
    binary. The same normalization is applied to the node fallback inside npm-shim resolution, which was a
    second which call with the same exposure.

  • An agent's stderr is captured and a bounded excerpt included in handshake failure details. It was previously
    discarded, so a child that explained itself precisely and died surfaced only as "Connection lost" — a
    description of the socket, not of the cause — and diagnosing one meant reproducing it by hand outside
    Rutherford. The pipe is owned by Rutherford and drained continuously from spawn to EOF, which is what makes
    it safe: inheriting the host's stderr is what once let an undrained pipe wedge the MCP host, and discarding
    it was the previous fix. Retention is head-bounded, because the failure this exists to explain prints its
    one useful line first, and draining continues past the cap so the child can never block on a write. The
    text is agent-authored, so it is stripped of ANSI/OSC escape sequences and control characters — which can
    retitle a terminal, forge a hyperlink, or write the clipboard — then masked for credential shapes, then
    capped by line and byte count and fenced, so where Rutherford's own words stop is unambiguous. It is
    attached only where a process actually existed; ACP_SPAWN_FAILED means the spawn itself failed, so there
    is no child and never a tail.

    The masking exists because the subprocess inherits a credential-bearing environment, so an agent that
    prints a token on the way out would otherwise put it in a result the caller reads and a durable job keeps....

Read more

v3.1.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 05:36

Added

  • trust / untrust CLI for the global workspace allowlist — from a repo root,
    python -m rutherford trust registers (or untrust removes) the current directory in the platform
    global trusted_workspaces allowlist, so write / yolo delegations pass the trusted-workspace gate
    without a per-call trust_workspace=true. Takes an optional path argument; trust --list prints the
    global list. Creates the global config.toml when missing, preserves unrelated keys and comments, and
    refuses to run against a config that is already malformed. Rutherford reads config once at server start,
    so restart or reconnect the server after a trust for it to take effect. Contributed by
    @Artemonim in #12.
  • The allowlist writer validates before it writes. The rewritten config.toml is rendered, parsed, and
    round-trip-checked in memory and only then swapped into place with an atomic replace, so a path that
    cannot be represented in TOML is refused with the existing config untouched rather than truncated. The
    assignment scanner is string- and comment-aware, so a [ or ] inside a trusted path can no longer walk
    past the end of the array and drop the [agents.*] tables below it. Unrelated keys and comments are kept
    as written, while the block's own managed header is rewritten in place instead of accumulating a copy per
    edit. Path quoting goes through the one shared hardened quoter (io/tomltext.py), and the file mode is
    carried across the replace so an owner-only config does not widen to the umask default.

Fixed

  • setup could write a config.toml it would then refuse to load. Its TOML quoter escaped only
    backslashes and double quotes, but on Linux and macOS a control character is a legal filename byte, so
    running setup --write --trust-workspace from a directory holding one emitted an unparseable file --
    and because setup never clobbers, it could not repair the file it had just written. Quoting is now a
    single hardened implementation (io/tomltext.py) shared by every writer, escaping the full control
    range and refusing outright a path with no TOML representation at all, before anything is opened.

Changed

  • discover's registry-directed-execution guard covers program runners, not just interpreters. It
    previously refused to launch an agent resolved to a shell or language runtime, but not to pip,
    cargo, go, git, docker, make, gh, kubectl, curl or xargs -- each of which executes
    attacker-chosen work from its own arguments as directly as sh -c does, and those arguments come from
    the registry. All are now matched by the same leading-family classifier, which leaves longer names
    alone (goose, ghost and atlas are unaffected). The guard is a denylist and its docstrings now say
    so: it is defense in depth, and a name it does not match is unrecognized rather than vouched for.
  • The ACP registry cache is written only after the response parses. It was previously persisted
    before validation, so a single malformed or hostile body became the fallback replayed on every later
    network failure. A bad response now fails once and leaves a previously good cache intact.

Security

  • Dependency advisories closed in the development lockfile (cryptography, mcp,
    pydantic-settings, python-multipart, starlette). Reported severity overstates the exposure here:
    these are HTTP-server-stack advisories reached through fastmcp's transitive dependencies, and
    Rutherford serves over stdio, so none is reachable in a default deployment. uv.lock ships in neither
    the wheel nor the sdist, so this affects contributors and CI rather than installed users.
  • CI workflows pin an explicit permissions: contents: read ceiling. The repository default is
    already read-only, so nothing changes today; the block keeps a later settings change, or a job added to
    those files, from silently gaining write. The code-review workflow now also skips cleanly on pull
    requests from forks, which never receive repository secrets and so could only ever fail.

Documentation

  • The trusted-workspace allowlist is documented end to enddocs/security.md covers the
    trust / untrust commands, the platform global config path, and the fact that a project-local
    trusted_workspaces replaces rather than unions the global list at load time (previously undocumented
    anywhere). docs/troubleshooting.md points WORKSPACE_NOT_TRUSTED at the one-shot CLI, and README.md
    and docs/configuration.md follow.

Thanks to @Artemonim for the trusted-workspace CLI contribution in #12.

v3.0.7

Choose a tag to compare

@github-actions github-actions released this 14 Jul 01:12

Changed

  • Migrated to agent-client-protocol 0.11 (pinned >=0.11,<0.12). ACP 0.11 removed model channel 1
    (session.models / SessionModelState / ModelInfo / session/set_model) outright. Rutherford now reads
    the legacy channel defensively (a config-only 0.11 response no longer AttributeErrors at session open) and
    selects models through the surviving configOptions channel and, for launch-flag agents, the process argv.
    The ACP client conforms to the 0.11 Client protocol — it adds the elicitation callbacks
    (create_elicitation is declined, since Rutherford drives agents headless) and matches the reordered
    filesystem / terminal / permission signatures. The upper cap is load-bearing: it keeps a future breaking
    minor from resolving into a runtime break rather than an install-time error.
  • Provenance is stricter. DelegationResult now tracks requested_model (the pre-effort request) versus
    selected_model (the model an in-session ACP selection actually confirmed), and provenance.confirmed is
    True only after a verified in-session selection — never a config echo or a launch-argv intent.
    provenance.model remains the effective model that ran, so cross-model diversity and the correlation
    discount keep their lineage key.

Added

  • Cursor model selection via a launch --model flag (new AgentDescriptor.model_launch_flag). Cursor
    applies its model from the process argv rather than an in-session ACP call, including effort-encoded
    compound ids (…[effort=high,fast=false]), and inherits the flag on a config clone that reuses the built-in
    launch command. Launch-flag selection is validated advisorily and never blocks a turn on a missing ACP
    advertisement (the model is on the argv regardless). Contributed by
    @Artemonim in #10.
  • capabilities reports static per-agent model metadatadefault_model, fallback_model,
    model_selection (launch_argv for launch-flag agents, else in_session), and effort_capable — without
    spawning the agent; use doctor(connect_only=true) for live advertised model ids.

Fixed

  • An unconfirmable requested model fails loudly instead of silently running the wrong one. When a caller
    names a model (or effort rewrites one) that the agent advertises on no ACP channel, the turn fails
    MODEL_UNAVAILABLE rather than quietly falling back to the agent's default. A descriptor default the agent
    does not advertise — for example a Bedrock/Vertex provider id applied via an injected ANTHROPIC_MODEL,
    never on an ACP channel — remains a soft-skip, so a Bedrock/Vertex Claude Code seat is unaffected.
  • A model-selection failure can no longer leak the spawned agent process. The session tears the agent down
    before a post-handshake MODEL_UNAVAILABLE propagates, so a rejected model does not orphan a process tree.

Thanks to @Artemonim for the Cursor ACP model-routing contribution in #10.

v3.0.6

Choose a tag to compare

@github-actions github-actions released this 04 Jul 07:23

Added

  • New built-in agent: fast_agent (evalstate's fast-agent, Apache-2.0), run as its own ACP server via
    uvx fast-agent-acp==0.8.3 — bringing the built-in roster to 20. The version is pinned (not @latest) so
    the built-in launches the exact release whose ACP handshake was verified rather than a moving remote spec;
    override with [agents.fast_agent] command = [...] to track a newer release. It is bring-your-own-model /
    multi-provider (provider=None): a turn needs a provider key in the environment (ANTHROPIC_API_KEY /
    OPENAI_API_KEY / ...) or a fast-agent.secrets.yaml, which the agent advertises as its ACP authMethod.
    ACP conformance is verified: it spawns, handshakes (agentInfo fast-agent-acp v0.8.3, protocolVersion
    1), and reaches a turn that cleanly reports Authentication required with no key. Like kimi / openhands
    it is a conformance-verified seat whose full answering turn is gated on the user's own provider key.

Fixed

  • A config clone of an effort-capable built-in now keeps its reasoning-effort tier. Effort is a
    capability of the launched ACP adapter, not the agent id, but the per-call effort dispatch keyed on the
    agent id alone — so a base= clone (or a local backend= clone) of codex / claude_code / cursor /
    cline / kiro / junie got a new id, fell through the dispatch table, and had its requested tier
    silently dropped to a no-op (effort_applied null) across every channel (the model[effort] model-id, the
    --thinking / --effort launch flag, the JUNIE_EFFORT env, and the effort / reasoning_effort config
    options). The descriptor now records the built-in whose knob it inherits (AgentDescriptor.effort_base,
    stamped when a clone reuses a built-in's launch command) and effort dispatches on effort_base or id, so a
    clone resolves through the adapter it actually launches. Built-ins are unaffected (effort_base is None,
    resolving by id); a clone that supplies its own raw command stays an honest no-op by design (arbitrary
    argv — the lineage is never inferred from command[0]). Non-breaking; no config-schema change. Thanks to
    Tony Stone (@Tasktivity) for reporting and fixing this in #7.

v3.0.5

Choose a tag to compare

@github-actions github-actions released this 25 Jun 21:19
0a53dbf

Added

  • doctor remediation hint for Claude Code on AWS Bedrock / Google Vertex / enterprise wrappers. When a
    Claude Code seat's turn is rejected for its model id (400 The provided model identifier is invalid) and a
    Bedrock/Vertex indicator is present, the conformance report carries a remediation_hint describing the
    per-agent [agents.<id>.env] fix — pinning a valid provider model id (with ANTHROPIC_CUSTOM_MODEL_OPTION,
    which survives an enterprise wrapper that rewrites settings.json and an enforced model allowlist).
    doctor stays read-only; the hint is advisory text, gated to the Claude Code adapter seat. setup detects
    a Bedrock/Vertex host and scaffolds the commented [agents.claude_code.env] block into the starter config.
  • Docs: docs/bedrock.md — "Claude Code on Bedrock / enterprise wrappers": the allowlist-rewrite
    mechanism, the approaches that do not work, the working env-injection fix, and the
    ANTHROPIC_CUSTOM_MODEL_OPTION exemption. [agents.<id>.env] is now documented first-class in
    docs/configuration.md, and docs/troubleshooting.md gains a model_unavailable entry.

Fixed

  • Hardened a flaky concurrency test. test_semaphore_serializes_a_wide_panel dropped its serial > 1.5x parallel ratio assertion — a loaded CI runner's fixed spawn overhead adds to both the serial and parallel
    runs and compresses the ratio toward 1, which flaked on a busy Windows / Python 3.11 cell. It now asserts
    only the spawn-overhead-invariant absolute serialization gap (serial - parallel > 0.2s), which is the
    sound measure (the overhead cancels in the difference). No production code changed.

v3.0.4

Choose a tag to compare

@github-actions github-actions released this 25 Jun 19:06

Fixed

  • Claude Code now drives on AWS Bedrock / Google Vertex. When the host has CLAUDE_CODE_USE_BEDROCK (or
    CLAUDE_CODE_USE_VERTEX) set, the claude-agent-acp adapter would fall back to the bare cloud alias
    claude-opus-4-8, which the provider rejects (400 The provided model identifier is invalid) — so
    delegate / consensus / doctor turns failed even though the seat was reachable. Rutherford now resolves a
    valid provider model id and injects it as ANTHROPIC_MODEL (plus ANTHROPIC_SMALL_FAST_MODEL when
    available) into the adapter's environment, so the SDK uses the real inference-profile id instead of the
    rejected alias. The id is resolved, in order, from an already-set ANTHROPIC_MODEL, a [agents.claude_code] model pinned to a raw provider id, ANTHROPIC_DEFAULT_OPUS_MODEL, then the env block of the host's
    ~/.claude/settings.json and <cwd>/.claude/settings.json (ANTHROPIC_MODEL then
    ANTHROPIC_DEFAULT_OPUS_MODEL). It is gated to the Claude Code adapter seat and to a Bedrock/Vertex host, so
    a normal API-key Claude Code and every other agent are untouched, and it never overrides an
    already-configured model. Works with zero Rutherford config when the id lives in settings.json.
  • doctor recognizes the Bedrock "invalid model identifier" rejection. The model-unavailable classifier
    now matches "model identifier is invalid" / "provided model identifier", so a provider model rejection is
    reported as model_unavailable (the connection is healthy; the model/provider config is wrong) rather than
    a generic error.

v3.0.3

Choose a tag to compare

@github-actions github-actions released this 25 Jun 00:40

Fixed

  • doctor no longer reports Claude Code (or any agent) as broken just because its model id is not a plain
    cloud id.
    When an agent is configured for a non-cloud provider -- AWS Bedrock or Vertex, where the model id
    is e.g. global.anthropic.claude-opus-4-8[1m] rather than claude-opus-4-8 -- a probe turn that fails
    because the provider rejected the model is now reported with a new model_unavailable status (spawn +
    handshake succeeded; only the model/provider config is wrong) instead of a generic error. The detail points
    at the model/provider config to fix (the Bedrock/Vertex model id or ANTHROPIC_MODEL), so a recognizable
    model rejection never reads as a broken agent.
  • Model selection now reads BOTH ACP model channels. Rutherford previously honored a requested model only
    through session.models (SessionModelState). Claude Code's claude-agent-acp adapter advertises its
    selectable models on the OTHER channel -- a session.configOptions select option whose category is
    model (its session.models is empty) -- so a model could never be selected for it and doctor connect_only misleadingly reported an empty model list. session/set_model (channel 1) and
    session/set_config_option on a "model" option (channel 2) are both supported now, and available_models
    reports the union of the two (SessionModelState ids first). As before, a model is sent only when the agent
    advertised that exact value, so a Bedrock/Vertex harness is left on its provider's own configured model
    rather than handed a rejected cloud id.

v3.0.2

Choose a tag to compare

@github-actions github-actions released this 16 Jun 00:12

Added

  • Reasoning effort now works in panel configs for codex, claude_code, and kiro, and can be pinned per
    seat.
    A panel seat (and a Target) takes an effort tier, so one voice can run at xhigh while another
    runs at high; a per-seat tier overrides the call-level effort, which overrides the per-agent / global
    config default. A new max tier joins the scale (claude_code and kiro reach it; codex and cursor
    clamp it to xhigh).
  • Effort is delivered over ACP through each agent's real, self-described knob. codex and claude_code
    expose effort as an ACP config option (reasoning_effort / effort), so Rutherford reads the agent's
    advertised option at session open, clamps the requested tier to the values it actually offers, and sets it
    via session/set_config_option -- covering claude_code (previously a silent no-op) and a codex seat
    with no pinned model. kiro takes the --effort launch flag; codex with a pinned model keeps encoding
    the tier in the model id (model[effort]). An agent that advertises no effort knob is an honest no-op
    (effort_applied stays null), never a false claim.

Fixed

  • Windows: an agent whose npm launcher resolved to its extensionless bin failed to start (WinError 193
    -- e.g. codex-acp / claude-agent-acp when shutil.which returned the Unix shell-script bin that shadows
    the .cmd / .ps1 siblings on PATHEXT resolution). prepare_argv now resolves the sibling shim, so the
    agent launches with clean JSON-RPC stdio instead of reporting not_installed.

v3.0.1

Choose a tag to compare

@github-actions github-actions released this 15 Jun 22:19

Added

  • doctor and setup recognize a missing npm ACP adapter shim and offer to install it. A few agents
    launch a SEPARATE npm adapter that fronts an underlying CLI -- codex -> codex-acp, claude_code ->
    claude-agent-acp, pi -> pi-acp. When that CLI is installed but the adapter shim is not, doctor no
    longer reports a bare not_installed: it adds an install_hint with the exact npm i -g <package> command.
    setup lists every such gap under adapters.installable, and setup install_adapters=true runs the
    install for each (an explicit, opt-in machine change; off by default). The install argv is built only from a
    curated package constant, never caller input. Covers codex / claude_code / pi today; the mechanism is
    generic (an AgentDescriptor declares its underlying_cli + adapter_package).

Fixed

  • Restored the PyPI downloads badge as the first badge in the README badge row.

v3.0.0

Choose a tag to compare

@github-actions github-actions released this 15 Jun 21:31

A ground-up, ACP-native rewrite. Rutherford is now the Agent Client Protocol
client and every coding agent is an ACP server it spawns and drives through a real
initialize / session/new / session/prompt exchange, so the protocol negotiates output, system prompts,
file context, permissions, and resume, and there is no per-agent output parser to maintain. The persistent
session is the foundation a debate runs on (one session per voice across rounds, sending only the delta).

Breaking. The entire v2 subprocess-adapter architecture is gone: no ProcessRunner, no
build_invocation / parse_output, no adapters/ package, no hand-written code adapter per CLI. Adding an
agent is now a config-driven AgentDescriptor ([agents.<id>]) or a built-in descriptor, never an adapter
(see docs/adding-an-agent.md). The built-in roster is 19 ACP-native agents; a few v2 CLIs without a working
headless ACP mode are not carried over and can be added back via discover or config once their ACP support
lands. A write / yolo / propose delegation now runs inside an isolated git-worktree (or copy) sandbox
with only a reviewed diff applied back; consensus and debate are read-only deliberation and refuse a
mutating mode at the service boundary.

Added

  • grok built-in agent (19 total) + a handshake-only connection check. grok is xAI's Grok CLI
    (grok agent stdio, provider xai), ACP-native with --model / --reasoning-effort knobs.
    Connection-verified live: Rutherford spawns it, completes the ACP handshake, opens a session, and reads its
    advertised models (grok-build, grok-composer-2.5-fast) — proving it can communicate with and configure
    Grok. A completed turn additionally needs a SuperGrok subscription; without one the model call returns
    403 SuperGrok Heavy subscription required. To make "reachable but not entitled" legible, doctor gains a
    connect_only option (doctor connect_only=true) backed by a new probe_connection primitive: it does
    the spawn + handshake + new_session only (no prompt) and reports reachable / handshake_failed /
    not_installed plus each agent's advertised models — so an agent that connects but can't complete a turn
    for a reason outside ACP (auth / entitlement / quota) shows as reachable, not a turn error. (Grok's
    headless handshake auth can transiently fail "Authentication required" under rapid back-to-back spawns — an
    xAI auth-refresh race, not a Rutherford fault; the live test retries it.)
  • discover: registry-driven detection of installed ACP agents (a new tool + python -m rutherford discover CLI). Fetches the community ACP agent registry
    (cached at ~/.rutherford/acp-registry.json for offline reuse; the CDN needs a real User-Agent), detects
    which registry agents are ALREADY installed on this machine, probes the ones it finds with a real
    read-only ACP round trip, and proposes a reviewable [agents.<id>] config block for every new agent that
    drives. Detection is detect-only: it scans PATH plus curated install dirs (~/.local/bin,
    ~/.cargo/bin, and every ~/.<vendor>/bin, one subdir deep — which is how it finds a custom-path install
    like Qoder at ~/.qoder/bin/qodercli/) and never downloads or runs npx. A registry id that aliases a
    built-in (e.g. codex-acpcodex, mistral-vibevibe) is recognized as already-in-roster, so it is
    never proposed as a duplicate. write=true (CLI --write) appends the proposal to the project config the
    loader actually reads (--global for the global one), creating the file if needed and never overwriting an
    existing section; probe=false (--no-probe) returns the raw detection without spawning anything. Safety
    posture (hardened over an adversarial review): probing only ever spawns a resolved agent binary, never a
    shell/interpreter — a structural family classifier refuses powershell/python/node/R and their
    versioned, -dbg/-preview, .cmd-shim, and pythonw-variant forms, so a tampered registry cannot get
    code-bearing args executed; the written config is TOML-injection-safe (a registry id is only kept if it is a
    safe bare key, comment text and command args are fully escaped, and a write into a malformed config is
    refused rather than risked). Use it to adopt any ACP agent or bridge Rutherford does not ship as a built-in.
  • Two more built-in agents (18 total): gemini and qoder. gemini is Google's official Gemini CLI
    (gemini --acp, provider google) — live-verified driving over ACP (status=ok, ~2.2s), which supersedes
    the earlier "headless ACP known-issue" note (fixed by Gemini CLI 0.46.0); it adds a Google/Gemini voice to
    the crew. qoder is Qoder AI's qodercli (qodercli --acp; the --acp flag is real but hidden from
    --help, like Cursor's acp) — live-verified (status=ok, ~2.9s). Qoder AI's installer drops the binary at
    ~/.qoder/bin/qodercli/ rather than on PATH, so on such a machine point [agents.qoder] command at the
    full path (or add the dir to PATH).
  • delegate can resume a prior agent session via a session_id parameter (v2 parity). Pass the
    session_id from an earlier delegate result and the agent reloads that conversation over ACP
    (session/load) instead of opening a fresh one (session/new), so a follow-up turn continues where the
    last left off. It is gated on the agent advertising the ACP loadSession capability at initialize; a
    resume against an agent that does not persist its own sessions fails cleanly with RESUME_FAILED rather
    than silently starting fresh (wiring the previously-unreachable error code). The resume restores the
    conversation, not the filesystem — a write/yolo resume still runs in a fresh isolated sandbox. The
    DelegationRequest.session_id field existed but was inert; it is now threaded tool → service →
    run_acp_turnACPSession.
  • Local-model support for opencode on both Ollama and LM Studio ([agents.<id>] base="opencode" backend="ollama"|"lmstudio" model=...). opencode is configured entirely through one inline-JSON
    environment variable (OPENCODE_CONFIG_CONTENT) that declares an @ai-sdk/openai-compatible provider
    pointed at the runtime's /v1 endpoint, so there is no config file on disk — one source of truth in
    roster._opencode_openai. Vetted live (2026-06-14): a real ACP turn answered on Ollama (qwen3:8b) and
    LM Studio (openai/gpt-oss-20b). This supersedes the earlier "opencode's acp turn returns empty" finding,
    which was an unconfigured-provider artifact, not an opencode limitation.
  • Documented the full, honestly-vetted local-backend support matrix in docs/local-models.md: which
    agent × {ollama, lmstudio} pairs work, and — for the ones that do not — the concrete reason. codex
    has no local pair (its custom providers now require the OpenAI Responses API wire that local runtimes don't
    speak, and codex-acp is auth-gated); hermes can talk to Ollama but only via its own config.yaml
    provider (its acp mode ignores the inference-provider env), so it is a config-file change, not an
    env-keyed backend. claude_code on Ollama works but is slow and needs a generous timeout + a capable
    model. A new -m integration suite (tests/integration/test_local_backends.py) drives every supported
    pair live and skips a runtime that is down.
  • Durable runs (F2): a delegate / consensus / debate call can now be kept as a job on disk. Each of
    the three tools takes a persist flag (true / false, or None to follow the configured
    default_persistenceephemeral out of the box, so nothing is written unless asked), and the
    previously-inert default_persistence / jobs_dir config is now wired. A persisted run is written under
    <jobs_dir>/<run_id>/ (jobs_dir defaults to <cwd>/.rutherford/jobs):
    • state.json — a versioned, replay-complete RunRecord as JSON (an internal record only Rutherford's
      own reader consumes, so it round-trips losslessly rather than using the token-optimized TOON of the
      tool wire): the resolved launch argv, requested-vs-resolved model, provenance, safety mode, requested/applied
      effort, topology, cwd, prompt, role, files, ok / error code, changed files, cost, stop reason, and a
      rollup. The child process env is never persisted (it can carry secrets); replay recomposes it.
    • artifacts/answer.md (the answer / synthesis) and, for a write run, artifacts/diff.md (the sandbox
      diff, including created / untracked files).
    • A persisted consensus writes a parent record linking a child record per voice (child_run_ids), with
      one artifacts/voices/voice-N.md per voice and a voices/skipped.md for an auto-panel's left-out agents;
      the parent rolls up status / cost / changed-file union and carries the resolved PanelInputs (roster +
      per-seat stance + session handle, strategy, synthesize, judge). A persisted debate writes a parent
      record plus the full artifacts/transcript.md (a debate drives its turns over persistent sessions, so the
      transcript carries the run rather than per-turn child records).
    • io/ledger.py (RunLedger) is the one writer of the jobs directory; persistence is best-effort — a write
      failure logs and degrades to an unpersisted result, never failing a run that already produced an answer.
      io.ledger.read_record / iter_records are the reader side (job continuation and the analyze report).
  • The write/propose sandbox substrate, so Rutherford can safely delegate file-writing work to an agent over
    ACP. A mutating delegation (write / propose / yolo) with a working_dir no longer runs the agent in
    the user's tree — it runs in an isolated execution root and only a reviewed diff is ever applied back.
    • SandboxManager (acp/sandbox.py). ...
Read more