ltms 26f380a00b
CI / shell-tests (push) Successful in 8s
CI / contract (push) Successful in 1m17s
CI / build (push) Successful in 2m24s
Merge #554: portable hash256, a third state for an unhashable jar, and the shell suite in CI (#550)
Closes fleetd #550, all three items.

Verified by me on the branch at b8182c9, in a scratch worktree, not from the implementer's report.

The decisive pair, both in ubuntu:latest where shasum is absent and sha256sum is present:

  main   (93a9ed3):  SUITE exit=127   anchored ^FAIL: count 0
                     scripts/test-redeploy-fleetd.sh: line 298: shasum: command not found
  branch (b8182c9):  SUITE exit=0     anchored ^FAIL: count 0

Identical FAIL counts, opposite exit codes. That is why the new shell-tests CI job gates on the
step's own exit status and deliberately does not grep for a FAIL count: a suite that dies before
running a single test prints exactly what a clean pass prints.

Also measured by me: macOS exit 0 / anchored count 0; 70 tests defined and 70 invoked with an
empty comm -3; bash -n exit 0 under both /bin/bash 3.2.57 and bash 5.3.9; ci.yml parses with jobs
build, shell-tests, contract, and shell-tests is ubuntu-latest + actions/checkout@v4 +
bash scripts/test-redeploy-fleetd.sh.

Three mutations, all killed, each restored byte-identical against
515d929bb53c9ec2c95042e47d3e4d611d60171345227b07feb0de0d20643d3a:

  drop the sha256sum branch from hash256   anchor 1->0   Linux exit 1,
      test_no_unguarded_macos_only_hasher_calls with its own message
  echo "unhashable" -> echo "absent"       anchor 1->0   FAIL: jar_id reported absent for a file
      that exists, only because no hasher was on PATH
  both hash256 arms -> shasum -a 1         anchors 1->0 and 1->0   FAIL: hash256 of the literal
      3-byte input 'abc' must be the known SHA-256 prefix, not some other algorithm's: expected
      ba7816bf8f01, got a9993e364706

The third mutation SURVIVED on the first head (3da44ee) and was sent back. Fixing item 2 had
rewired the reference hashes in test_jar_id_defaults_to_live_and_reports_explicit_path onto
hash256 itself, making the test's reference and its subject one instrument — they agree whatever
it computes, and the existing fixture guard catches only a constant return, not a wrong algorithm.
b8182c9 adds test_hash256_computes_a_real_sha256, pinning the FIPS 180 vector for "abc" as a
literal constant written into the test rather than taken from any hasher. That closes it: the
mutation now goes red, and a9993e364706 is SHA-1("abc"), which proves the mutated code really ran.

Root cause, for the record: shasum was the trigger, not the cause. Under set -euo pipefail a
missing hasher makes the pipeline status 127, the `|| echo "absent"` fires, and jar_id returns a
confident false "absent" for a jar that is right there. Without pipefail the same function returns
an empty string and is visibly broken. A default at the read site that maps every failure onto one
value which already means something specific is the defect; the fix separates "not there" from
"could not hash it".
2026-09-12 10:20:29 +02:00

claude-bridge

A subscription-safe bridge that lets a primary Claude Code (Opus 4.8, on Pro/Max) session drive a secondary Claude agent running a different model via its own ANTHROPIC_BASE_URL — without ever putting a proxy on the primary session.

Sibling of crush-bridge (which drives a headless Crush worker on GX10 DeepSeek). claude-bridge keeps the worker a real Claude Code process, so it inherits CLAUDE.md, hooks, skills, and MCP — just pointed at a cheaper/local model.

Leading approach — herdr-centric message server (fleetd)

A small always-on message server, fleetd, controls herdr (an agent multiplexer) over its Unix-socket API and exposes a clean 2-way messaging API as an MCP server that both the primary and the workers mount — one unified Claude setup and the sole communication gateway (REST/SSE stays for non-Claude clients; any broker is fleetd-internal, below the gateway). herdr owns the PTYs, multiplexing, persistence, and agent-status events; fleetd owns policy (subscription boundary, session lifecycle, status-gated delivery) and the client contract. A Claude member launches with ANTHROPIC_BASE_URL pointed at the gateway, https://llm.ltms.dev/anthropic, plus a bearer token; the lead stays env-clean and calls fleetd's MCP tools. See the wiki's 13 User Guide to run it.

flowchart LR
    OPUS["Opus — primary<br/>(Claude Code, env CLEAN)<br/>MCP client"]
    subgraph BD["fleetd — standalone daemon (not a claude process)"]
        SRV["SERVER face<br/>MCP · REST/SSE · policy"]
        CLI["CLIENT face<br/>status-gated injector · herdr socket"]
        SRV --> CLI
    end
    HERDR["herdr<br/>panes · agent-status"]
    W["worker claude pane<br/>ANTHROPIC_BASE_URL set<br/>MCP client"]
    M["llm.ltms.dev<br/>(the one gateway)"]

    OPUS -->|"MCP fleet_send (blocks)"| SRV
    W -.->|"MCP fleet_reply"| SRV
    CLI -->|"Unix socket<br/>send_text · events.subscribe"| HERDR
    HERDR -->|"drives PTY"| W
    W -->|"inference"| M

    classDef ext fill:#2b6cb0,stroke:#1a365d,color:#ffffff;
    classDef core fill:#2f855a,stroke:#22543d,color:#ffffff;
    class OPUS ext
    class SRV,CLI,HERDR core
  • Subscription boundary: the primary never sets ANTHROPIC_BASE_URL (stays on Pro/Max). Only the secondary process is off-subscription — and fleetd itself is a plain daemon (no Anthropic quota), so it may poll/subscribe freely.
  • One gateway (unified MCP setup): fleetd is the sole communication path for every Claude session. Primary and workers each mount it as an MCP server (one claude mcp add line, same on both) and talk over MCP tools — fleet_send / fleet_reply / fleet_status (with fleet_ask planned for the blocked-worker path). No Claude session ever addresses a broker, a peer, or the network directly; any queue is fleetd-internal. MCP tool I/O never sets ANTHROPIC_BASE_URL, so mounting the bridge is subscription-safe by construction. Tool naming: the tools are fleet_* (renamed from bridge_* in CB-622). The old bridge_* names were removed in CB-634 — only fleet_* answers now.
  • How the primary consumes a reply: a single blocking MCP call (fleet_send); fleetd holds it open until the worker calls fleet_reply or its turn hits agent_status=done, then returns the reply as the tool result. No cross-turn busy-poll, so no quota burn. SSE is an optional side-channel for humans/dashboards watching status.
  • Worker → primary rides fleetd's MCP rendezvous — the reply resolves the primary's blocking call (or, for detached work, fleetd injects the primary's idle pane when it's ready), so no keystroke-into-primary and no broker are involved, even single-host. The one exception: a split-host primary that isn't a herdr pane wakes via its own Stop-hook, which polls fleetd (never a broker). See the wiki for the two topologies.
  • Different model per process sidesteps Claude Code's lack of per-subagent provider routing — the worker isn't a subagent, it's its own configured process.
  • AgentAPI (coder/agentapi) is retained only as a swappable fallback injector behind the same interface. See the wiki for the full design, comparison, and rationale.

Docs

Full design, setup, and operations live in the wiki, vendored here as a submodule under wiki/:

git clone --recurse-submodules ssh://git@git.ltms.dev:2224/fleet/fleetd.git
# or, after a plain clone:
git submodule update --init

Edit docs in wiki/, then cd wiki && git commit && git push to publish them to the Gitea wiki.

Status

🟢 Implemented & dogfooded — the herdr-centric fleetd message server is built and in real use: an Opus primary delegates tasks to off-subscription workers that reply through the bridge (code reviews delegated this way have produced committed bug fixes). Selected as the primary approach 2026-07-11, superseding the AgentAPI plan (2026-07-08); AgentAPI retained as a fallback injector.

Shipped (Java 25 · Maven · 266 unit/acceptance tests green; the live-herdr and broker contract tests run separately via mvn test -Pcontract):

  • Core gateway — herdr socket client (contract-tested vs live 0.7.0); guard-checked worker spawn with ANTHROPIC_BASE_URL injected only into the worker's env; status-gated injector; blocking fleet_send with reply rendezvous; MCP server as a thin adapter over the REST core.
  • MCP tools — fleet_send / fleet_reply / fleet_status (messaging) and fleet_spawn / fleet_list / fleet_stop / fleet_profiles / fleet_poll (fleet). Caller identity is connection-based (loopback peer PID → herdr pane), so the same mount serves primary and workers.
  • Delivery reliability — completion fallback (a confirmed working→idle turn resolves a send); async fire-and-poll (beats the caller's MCP call timeout for long tasks); and failure detection for wedged (unknown), vanished, and never-ready workers so a send never hangs.
  • Fleet — multiple worker profiles, each with an independent base_url guard check; workers inherit the primary's working directory (never $HOME); a readiness gate holds delivery until a worker's Claude has connected the bridge MCP (no paste lost into its boot window).
  • Blocked-worker path — fleet_ask reverse rendezvous: a worker pauses its delegated turn to ask the primary and resumes the same turn with the answer (CB-205).
  • Session lifecycle — session manager with spawn/reuse/recycle, idle_ttl reaper, context_cap, and graceful drain on shutdown (CB-301/CB-303); per-worker git worktrees on their own branch with a config-parity overlay, so parallel implementers never stomp each other (CB-301-ext).
  • Reliable worker→primary delivery — a durable ReplyInbox (in-memory by default, AMQP/LavinMQ for cross-restart durability) holds a reply that arrives with no open send, and an active status-gated push loop nudges the primary to drain it (CB-307).
  • Pluggable peers — a PeerLauncher SPI with two in-tree adapters, claude-code and opencode, routed by a kind: discriminator (CB-401/CB-402).

Next (see the roadmap) — Stage 5 hardening (auth/TLS, /metrics, CI, service supervision, per-session authz + audit), then cross-host: CB-308 multi-host federation and CB-500 multi-tier coordination.

S
Description
No description provided
Readme 16 MiB
2026-08-10 15:58:06 +02:00
Languages
Java 94%
Shell 5.1%
Python 0.9%