Skip to content

Seller-tool onboarding: Proxy swap route, Holder route in the daemon, holder kit - #1004

Merged
pmilic021 merged 59 commits into
mainfrom
feat/seller-tool-onboarding
Sep 16, 2026
Merged

pmilic021 merged 59 commits into
mainfrom
feat/seller-tool-onboarding

Conversation

@pmilic021

Copy link
Copy Markdown
Contributor

What this delivers

A seller can offer third-party tools to its jobs without a credential entering a job container. Two automated routes ship, and a skill routes a seller's tool to the right one.

Route How it works State
Proxy swap ([[sandbox.mcp_tools]]) The job holds a per-job placeholder. The existing credential proxy (#647) swaps the real credential into the request header at egress, for the vendor's host only, for the life of the job. The job reaches a vendor-hosted MCP server through mcp-http-bridge, baked into the sandbox image. Accepted against GitHub's remote MCP server on 2026-09-14, through the real proxy, the real launch, and a real claude-agent-acp turn.
Holder ([[sandbox.held_tools]]) The daemon runs one persistent holder container per declared tool (tool-holderd from crates/maxplayer-tool-kit plus the vendor CLI). The holder enrols once and resumes its login across restarts. Each job gets its own Unix socket per tool, mounted as a volume subpath; the agent reaches it through tool-mcp-bridge --socket …. Job end detaches the socket; the tool stays enrolled. Wired into the seller daemon and proved live through the daemon code with two tools on 2026-09-14, including a real claude-agent-acp turn that called both. The vendors in that proof are the kit's fakes.
Public, Direct token Manual setup. The skill gives the steps. Documented.
Dedicated machine, Browser login Not supported at this moment. The skill says so and stops. Decided.

The routing decision tree is docs/specs/seller-tool-onboarding/10-routing-and-options.md. The skill is .claude/skills/seller-tool-onboarding/SKILL.md.

What changed in the product

  • crates/maxplayer-core/src/held_tool.rs (new): the holder container guard, per-job attach and detach, pure argv builders.
  • seller_exec.rs: [[sandbox.mcp_tools]] registration on the proxy; ExtraMount and JobAttachments; run_agent_job_in_env; the docker alias pinhole opens when an MCP server entry names it; the capture redactor knows every real credential value a launch holds.
  • seller_node/run.rs: holders start at boot and stop at shutdown; both job paths attach and detach; the container-delivery path carries the mounts and the session entries to the orchestrator.
  • driver/acp.rs: McpServer is now the ACP wire shape the adapters read (a stdio entry with no type key, or an http entry).
  • docker/maxplayer-sandbox/Dockerfile: mcp-http-bridge and tool-mcp-bridge installed.
  • crates/maxplayer-tool-kit: the holder kit (race-safe no-follow file consumption, staging, per-job sockets), the two bridges, the fake vendor and proxy test doubles, templates.
  • Config: [[sandbox.mcp_tools]] and [[sandbox.held_tools]] on [sandbox]. A seat without them is unchanged.

How it is proven

Every claim has a bundle under evidence/, each with a README that states what it proves and its limits. The token or synthetic secret of each run appears in no file.

  • evidence/20260914T085619Z-github-proxy-swap/ — Proxy swap against GitHub: the bridge in the sandbox image, uncontained and under egress containment, and a real agent turn. The token owner's login came back from GitHub; the token was absent from everything the container received; the placeholder got 400 at GitHub and 502 at the proxy; job end revoked it.
  • evidence/20260914T103608Z-holder-route-two-tools/ — Holder with two tools through the daemon code: two holders enrolled once each, one job driving both through their own sockets, a contained job, a restart that resumed both logins, and a real agent turn that called both tools.
  • evidence/20260910T125544Z/ — the kit's digest-pinned container demo, 39 checks, which cleared the five review findings (F1 to F5) recorded in docs/handoff/README.md.

The live tests are #[ignore]d and configured by environment: seller_exec::mcp_tool_tests::live_* and held_tool::live_tests::live_*. Their READMEs say how to rerun them.

Gates on the merged tree (this branch merged with main at 1569010)

Row Result
cargo test -p maxplayer-core (default, acp, wallet, wallet,acp) 449 / 498 / 1555 / 1619 pass; one flake in the last row, see below
cargo test -p maxplayer (default, acp,wallet) 162 pass; 199 of 200 pass, see below
cargo test -p maxplayer-tool-kit 52 pass
clippy clean in the changed code

Two tests failed once each in the full run and are not related to this change:

  • seller_node::run::tests::the_receipt_sub_backfills_the_terminal_cache_on_boot failed with relay run: AddrInUse — a local relay fixture that predates this branch (gate open-pool claims on a relay-derived settlement receipt (#541) #548) and exists on main. It passed three times in a row when run alone.
  • doctor::tests::sandbox_image_check_is_wired_into_the_boot_gate needs docker manifest inspect on an unresolvable host to fail fast. It passed in the default row of the same run and failed in the featured row when Docker Desktop's registry client hung, which a direct probe confirmed. This branch's change to that file is one struct-literal field.

Requirements and limits

  • The Holder route needs Docker Engine 26 or newer for the volume subpath mount. Boot probes for it.
  • The proxy constrains the destination host, not the operations. A broad credential behind the Proxy swap still needs a trusted operation filter, which is not built. GitHub was accepted with a read-only fine-grained token on the read-only endpoint.
  • Each holder holds one credential for one vendor.
  • A real vendor CLI inside a seller-built holder image is its own acceptance run, per vendor.

Merge with main

The branch is merged with main at 1569010 (the resolver-file and relay-push work). The merge met
four conflict files, all of one kind: both sides added a field or a literal line at the same place,
this branch for tools and mounts (mcp_servers, mcp_tools, held_tools), main for the resolver
file (resolv_conf, dns_servers). Both were kept everywhere. main's DNS live test builds typed
mounts now. After the merge, both routes ran live again under egress containment and passed.

Where to start reading

  1. docs/handoff/CONTINUATION-2026-09-10.md, sections 10 and 11.
  2. docs/specs/seller-tool-onboarding/10-routing-and-options.md, then 09-production-integration.md.
  3. crates/maxplayer-core/src/held_tool.rs and the mcp_tool_* regions of seller_exec.rs.

🤖 Generated with Claude Code

w-seller-tool-onboarding-r2 and others added 30 commits September 9, 2026 10:00
…d-policy mapping

Paper artifacts only, no runtime changes. Anchored in plan v3
(1c35ba32...) and advisor verdict (44377fb0...).
Survey of upstream/main d7b94db: seller config, job lifecycle, MCP and
credential/sandbox surfaces, with named gaps G-1..G-7. Paper only.
Walk A routes to rung 4, supported conditional on separable auth-file
writes. Walk B routes to rung 3 and concludes deferred, with five named
reasons the mapping contract does not transfer to HTTP bodies.
… register

Completes the stage-0 paper contract: planned check map for plan v3
5.1-5.12, runtime negative-control mutants, evidence bundle layout with
pre-recorded oracles, and gaps G-1..G-7 plus contract gaps C-1..C-7.
Records the ten explicitly unsupported/deferred cases and the eight
supported-profile requirements. No runtime changes.
Corrects two material errors found by the stage-0 verdict, both
re-verified first-hand against the base source:

- credential custody is NOT host-held injection: seller_exec.rs:2575
  starts a per-job host credential proxy that forwards a placeholder and
  fails closed rather than putting the real credential in the container.
  G-6 narrowed to the genuinely absent pieces.
- timeout DOES reach Failed via fail_job_with_feedback -> fail_job; the
  real hazard is that the write is best-effort and restart re-drives
  non-terminal rows treating a missing deadline as live.

Adds the award-authority analysis (Awarded::{New,Duplicate,NoClaim},
suppression and ACCEPT reach the same store method) showing a
record_award hook is unsafe, the MCP attachment gap (mcp_servers is
Vec::new() at seller_exec.rs:2408), and the three named core adapters.
F1: authorization adapter moved off record_award to the owned-award /
eligible-execution boundary in the Awarded::New arm after
AwardMatch::Execute. Five positive preconditions, explicit no-grant
list, arm-by-arm treatment, two-phase idempotent open, and a durable
holder admission record supplying the service/grant binding the jobs
table lacks.

F2: durable monotonic admission state with close reason; fixed close
ordering (commit closing -> deny -> reservations -> revoke/stop/remove
-> commit closed) since termination is not transactional with SQLite;
holder-enforced expiry independent of the marketplace process; boot
reconciliation that fails closed; reservations forfeited without proof
of non-occurrence; no write auto-replay.

F6: party_scope defined as same-party same-vendor concurrent jobs only;
no cross-party authority inferable from shared-holder.
…ests, gaps

F4: replace the vendor-request-counter oracle with three independent
observers (harness-owned child-start counter, vendor request counter,
forbidden-effect markers). Reject-before-invocation now requires
validation error AND zero child starts AND zero vendor effects. Adds a
mutant that launches a child then rejects without contacting the vendor,
and removes the unjustified cleanup dependent-skip.

F5: holder-issued handles and slots immutably bound to holder/party/job
with an admission membership check, plus the valid-cross-job-handle
negative case. Effect maxima derived from bindings and enforceable, with
input/output/network counted separately; page/item ceilings made
consistent. Walk A classification conditional on A1-A5, not A5 alone.

F6: removes the universal single-file-output requirement (plan requires
private checked output slots); single-file stays walk A and the demo
only. party_scope defined as same-party same-vendor. Splits router
results into unsupported / deferred / manual-setup-required. Corrects
C-1/C-4/C-7 to name missing enforcement rather than imply the plan
permits weaker rules. Real-tool needs no longer read as blocking the
authorized synthetic demo.

Adds integration checks 13 (owned-award admission) and 14 (durable close
and reconciliation) for F1/F2.
Per Petar 2026-09-09: the offering is per seller, not per job, and the tool
is active together with the seller daemon. So tool-holderd enrols once at
startup, reuses an existing session if one is already persisted, and stays
available until the process stops. No award, payment, job start or job
completion opens, closes, renews or revokes anything; detach_job explicitly
reports that the tool stayed enrolled.

Jobs are addressed, not entitled: attach_job creates a per-job Unix socket
and records that job directory, so a connection carries its job identity
from the listener it reached rather than from the request body. Reserved
argument names (job_id, job_root, cwd, home) are refused rather than
ignored.

Five binaries: vendor-service (fake authenticated vendor, owns the request
counters that serve as the independent oracle), vendor-cli (the tool being
onboarded; persists its login in its own home and never takes a credential
on argv), tool-holderd (the holder), holderctl (seller operator CLI) and
tool-mcp-bridge (real MCP JSON-RPC over stdio inside a job container,
proxying to the per-job socket).

Credential containment is structural: vendor-cli runs as a child of the
holder with a cleared environment pointing at holder-private state, so a
job container is given only its own socket and its own directory.

Dependency surface is serde plus std, nothing else, because the audit
surface of a credential holder should be readable in one sitting.
fixture_suite (6): one enrolment serving two sequential jobs with the
vendor login_count staying 1; the operation list identical on both job
endpoints and on the control endpoint; a real MCP stdio session driving
the tool through the bridge; daemon restart reusing the persisted session
with zero further logins; availability following the daemon and nothing
else; and the credential staying out of job directories (session file
0600 under holder state, job tree greppably free of secret and token).

negative_controls (26): the parameter grammar and job-directory
confinement, each asserting the reason for the refusal rather than that
something failed, plus a positive control so a validator that rejected
everything could not pass; live-endpoint refusals for reserved arguments,
tool calls on the control endpoint, cross-job access through MCP by
absolute path, traversal and output redirection; the output ceiling; and
a vendor-side revocation becoming visibly unhealthy, failing closed, then
recovering through re-enrolment.

Claims about calls are checked against the vendor service counters, never
the holder self-report.

Two real defects the run surfaced, both fixed here rather than worked
around: the holder runtime directory must be short because a Unix socket
path is capped at SUN_LEN (104 bytes on macOS), which macOS temp paths
silently exceed for per-job sockets while the control socket still fits;
and a flag-like text probe longer than max_len trips the length rule
first, so the test was asserting a rule it never reached.
Explicit dependency versions instead of workspace inheritance so the crate
compiles from its own directory with no parent manifest. That lets the
image build from a context containing only this crate, which keeps the
build honest about the dependency surface: if the holder needed anything
from maxplayer-core, this build would fail rather than quietly pick it up.

One image carries all five binaries and serves every role in the demo.
The roles differ by entrypoint and, far more importantly, by what is
mounted into them.

Verified: cargo test -p maxplayer-tool-kit still 32/32 after the manifest
change, and docker build succeeds on colima/aarch64 (image
maxplayer-tool-kit:demo, id 148697048a3a).
…kit binaries

Cargo.lock records maxplayer-tool-kit as a workspace member. The Dockerfile builds
all five binaries from the crate directory alone; NOT yet built or run here.
Each attached job now gets runtime/jobs/<job_id>/job.sock in its own 0700
directory, instead of a flat runtime/jobs/<job_id>.sock.

The reason is the container demo: a job container must receive exactly its
own endpoint. With a flat layout the only mountable unit is the directory
holding every job socket, so per-job isolation could not be expressed as a
mount boundary at all. One directory per job makes the isolation a
property of the topology rather than a promise in a document.

cargo test -p maxplayer-tool-kit: 32/32 after the change.
…p screen

No binary in the kit handles --help: both CLIs print a usage line to stderr and exit
non-zero on bad arguments, so CMD ["holderctl","--help"] made the image fail by default.
The default command now names what the image carries, prints both usage lines and exits 0.
Verified by rebuild: default run exits 0.
Runs the holder across two sequential job containers on Linux and writes
an evidence directory: per-step verdicts, both MCP transcripts, holder and
vendor logs, vendor counter snapshots, and a manifest.

Two properties the in-process tests could not express:

Job containers run with --network none and receive exactly two mounts,
their own working directory and their own single-socket directory. A job
container therefore could not reach the vendor even if it held the
credential, and the per-job socket layout is what makes that mountable.

The vendor port is published on host loopback, so every counter used as
evidence is read from outside the system under test. The holder status
output is recorded but is never the sole basis for a call-count claim.

The manifest states mechanism_only with the limits spelled out: the CLI
and the vendor were both written for this contract and cannot falsify it,
so this is not independent real-tool acceptance.

Not yet executed; committing before the run because the last three turns
died mid-flight.
linux/arm64, docker 29.5.2 under colima. Full artefacts: both MCP
transcripts, holder and vendor logs, vendor counter snapshots at four
points, per-step verdicts, manifest.

The load-bearing numbers, all read from the host rather than from the
holder: login_count 1 across two sequential jobs and across a daemon
restart, transform_count 2, auth_failures 0 until the revocation step
deliberately caused one, login_count 2 only after explicit re-enrolment.

Audited before committing: no credential, session token or secret-shaped
string appears in any artefact. The per-run secret is generated on the
host, mounted read-only, and removed on exit.

The manifest records mechanism_only with its limits stated rather than
implied.
The scope correction moved the tool from per-job grant to per-seller
enrolment, which removes the premise of 04 Part I entirely: the award
boundary is not the tool-lifecycle boundary, so the careful argument about
which award edge to hook was answering a question that should not have
been asked.

Annotated rather than deleted. The reasoning is a record of how the wrong
model was reached, and 04 Parts II onward — custody, containment,
persistence, re-enrolment — were never about awards and are what the
implementation honours.

00 also corrects its own status line: "paper contract only" was true when
written and is not now. The epistemic caution survives the change and is
restated, because a fake vendor and a CLI written for it still cannot
establish third-party acceptance.
01 §2: the survey findings are verified and stand; what is withdrawn is
the inference drawn from them. "The job record has no grant" was written
as a gap to close and is in fact the correct shape.

02: the layering stack loses its third layer. A job endpoint and directory
narrow where a call may act but are not a layer of the offering.
grant_policy is marked withdrawn in place rather than cut, so the
surrounding schema still reads.

05 Step 5 and 07 check 13: superseded outright, with what replaced them
named — attach rather than mint, and validation plus confinement enforced
on every call rather than a grant checked per call. 07 points at the tests
that actually run.

06 Step 4: doubly inert, since walk B was already deferred at rung 3. Its
tenant-isolation question is untouched and still open.
Two demo runs started twelve seconds apart and both were committed, which
my previous commit message described as "the container run" as though
there were one. The overlap was an accident of a turn killed mid-flight by
a provider error, not a deliberate second run.

Kept both rather than pruning one: deleting an inconvenient artifact is a
worse habit than explaining it, and the two runs are separately networked
and volumed, agree on all 27 checks, and so corroborate each other.

The caveat is stated in the README rather than left implicit — sharing one
Docker daemon means they are not independent in the strong sense.
The previous version blamed the duplicate bundle on a turn killed by a
provider error. That was a guess written as fact. Two sessions were
writing this worktree and branch at the time; the second was ordered to
stop on 2026-09-09 15:06 PDT and this author made sole writer, which makes
the other session the likelier source of the second run.

Attribution is not recoverable from the artifacts: both sessions committed
under the worktree identity and nothing records the launching process. The
file now separates what the evidence supports (two runs, separate networks
and volumes, same daemon, both 27/27) from what it does not (which session
ran which), instead of filling the gap.

Also verified this turn, since the branch carries two commits this author
did not write: c4d02ba (Cargo.lock workspace entry) leaves the lock
consistent under --locked --offline, and 2041c93 (image default command)
does exit 0 on a fresh build, as it claimed. Both fixed real defects,
2041c93 in this authors own Dockerfile. Tests still 32/32 at HEAD.
… gates

Advisor F5: the banner claimed "Parts II onward stand", but those parts
carry admission rows, holder-enforced grant expiry, per-call token
verification, per-job durable budgets, and a rule that only a newly
authorized job may run after re-enrolment. Those contradict per-seller
enrolment and same-job recovery, so the global withdrawal notice was not
enough on its own.

Each contradictory section is now marked in place, so a reader landing
mid-document cannot mistake a withdrawn gate for a live requirement. What
is retained is named exactly: trust boundary, credential custody,
job-directory confinement, session persistence, re-enrolment. Per the fix
order these are withdrawn requirements, not a backlog, and are not to be
implemented.

Two claims of my own are corrected rather than left standing: file
confinement is NOT safe against a checked path being replaced between
validation and use (F2), and the container evidence for credential absence
and for the stop/restore lifecycle is under repair (F1, F3) and must not
be cited as established.

fixtures/README.md pointed at a scripts/demo.sh that does not exist, and
blurred two different credential paths under "generated at run time". The
demo derives a fresh random secret per run; the Rust fixture writes a fixed
synthetic literal that lives in the test source. Stated without
reproducing the value.

Remaining F5 item, deliberately deferred to F1: demo.sh:88 and :306 still
claim the credential is never on a command line. That claim is false today
because of the -e NEEDLE probe, and it becomes true when F1 removes the
injection. Correcting the prose before removing the defect would be the
wrong order.
…captures

Advisor F4: the image was built from mutable rust/debian tags with an
unlocked cargo build, so the same Dockerfile produced different images on
different days and no acceptance run could identify what it tested.

Both stages are now pinned by digest (rust@sha256:ebd900ba…,
debian@sha256:88200866…), resolved on linux/arm64 and recorded in the
Dockerfile with the rustc version they carry. Updating a base image is now
a visible edit rather than a silent drift.

Cargo.lock is generated standalone for the crate and copied into the
crate-only build context, so the image resolves no versions of its own and
--locked fails rather than quietly updating a dependency. 11 packages.

Verified, not assumed: docker build --no-cache with both pins and --locked
exits 0; image sha256:0fece64074862f51d7b51a9547a8a340f67ff134ede2c350b6f
c6409b7b263b7 carries all five binaries and the fixture config.

.gitignore:18 (*.jsonl) had silently swallowed every raw MCP transcript the
demo recorded, leaving summary PASS lines standing in for observations
nobody could re-read. Evidence bundles are now excepted and the eight
dropped captures from both 2026-09-09 runs are retained. They are the
weaker, pre-repair evidence and are kept as superseded record, not as
proof: F1 and F3 invalidate the credential-absence and lifecycle claims
those runs made.

Still owed on F4 and deliberately not claimed yet: the source-to-build
receipt and recording the loaded image ID inside the demo manifest. That
lands with the F1 rewrite of the same script.
… KNOWN DEFECT

WIP. Handoff stopped implementation mid-change on human order. This commit
is recorded so nothing is lost; it is NOT verified and MUST NOT be read as
a working fix.

KNOWN DEFECT, INTRODUCED BY THIS COMMIT AND NOT FIXED: $RUN_TMP is used in
the new code and is never defined anywhere in the script. demo.sh runs
under `set -euo pipefail`, so the first expansion aborts the run. The
script is broken-on-arrival until a scratch directory is defined. The
existing host scratch dir is $HOSTDIR ("$HOME/.mtk-demo/$RUN_ID", created
mode 0700 and removed by the EXIT trap); pointing RUN_TMP at a subdirectory
of it, or substituting $HOSTDIR, is the intended one-line repair. I found
this by grepping for the definition after writing the code, and stopped
before fixing it.

NOT DONE: bash -n passes, but that only proves syntax. The demo has NOT
been executed since this change. No check in it has been observed to pass.
The previously reported 27/27 belongs to the OLD script at a5e6880 and does
not describe this file.

What the change intends, and why (advisor F1). The old absence check passed
the live secret into the probe container as -e NEEDLE and grepped from
inside, so the credential was placed in the very container whose
cleanliness was the claim; `grep -rl ... || echo NOT_FOUND` printed
NOT_FOUND on any non-match exit including a scan that never ran; and a
trailing `|| true` swallowed docker failures, so an unstarted container
also read as absent. The check as written COULD NOT FAIL. This is my own
defect, conceded before review pressed on it.

The replacement exports the job container filesystem to the host with no
secret in its environment, scans on the host where the secret already
lives, treats capture failure as fatal instead of as absence, and adds a
negative control that plants the secret into a copy of the same archive and
asserts the SAME scanner reports 1 hit. Whether it behaves as intended is
unknown and untested.

Also still false and deliberately left alone: demo.sh:88 and the manifest
credential line still claim the secret is "never on a command line". That
prose was to be corrected once the injection was gone; the injection is
gone from the source but unverified, so the claim stays flagged rather than
quietly reworded.
Human order: stop implementing, hand off to another agent. This commit
makes the branch self-contained so a stranger can continue without access
to the authoring machine.

docs/handoff/README.md states head/base/remote, the architecture and file
map, F1-F5 status with the full review findings, build and run commands,
image digests and lockfile facts, production integration gaps, the owed
skill/templates/examples, and the seller-daemon scope with Petar routing
decisions preserved but unactioned.

It keeps one distinction sharp, because reports from here previously blurred
it: tests actually executed (cargo 32/32, and the pinned --locked --no-cache
build exiting 0) versus checks that were invalid or never ran. Named as
invalid: the old credential-absence check, which passed the secret into the
probe container, printed NOT_FOUND on any non-match exit and swallowed
docker failures - it could not fail. Named as never run: everything in the
F1 rewrite, and the demo since 11e6dee. The 27/27 belongs to the old script
at a5e6880 and does not describe the current file.

It also documents, prominently, that 11e6dee leaves demo.sh broken on
arrival ($RUN_TMP used and never defined, under set -euo pipefail) and
gives the one-line repair, rather than leaving the recipient to discover it.

The five referenced documents are copied into docs/handoff/reference/
rather than cited by local path, since paths on this machine are unreachable
to the recipient and therefore worthless: both advisor verdicts, the
governing scope correction (sha256 verified 683da095... after copy), the
implementation brief and plan v3. Each gets a provenance header; content is
otherwise unmodified. A private seat memory file was excluded deliberately
as a private log, with its load-bearing content restated in the handoff.

Scanned before commit: no credentials, tokens, keys or private seat logs in
any staged file.
The holder used to canonicalize a job-supplied path, check it, and return a
string that the vendor CLI re-opened later. A job could swap the checked name,
or a parent directory, for a symlink between the check and the open (advisor F2).

The fix removes the re-open:
- validate_call checks grammar and confinement by name only; it no longer
  touches the filesystem or returns a canonicalized path.
- src/safeio.rs opens each file with openat + O_NOFOLLOW on every component, so
  a symlink on any component is refused rather than followed. This adds libc,
  the one dependency beyond serde, because std has no openat.
- The holder copies an input into a private staging directory the job cannot
  reach, runs the CLI against the staged copy, and publishes the output with a
  no-follow create.

Tests in negative_controls.rs prove the boundary: a symlink planted after
validation is refused, at the unit level and at the live endpoint, and the
outside file is neither read nor written. Also two clippy cleanups to keep the
crate warning-clean. 35 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F1 - credential absence:
- Define RUN_TMP, which was used but never set; the script aborted on its first
  expansion under set -euo pipefail.
- capture_job_fs: exclude *.sock, so tar does not fail on the job's own socket.
- scan_capture_for_secret: separate grep's clean no-match (exit 1) from a real
  scanner error (exit >=2). A no-match must not abort the run under pipefail, and
  an error must not read as "0 hits", which would manufacture a false absence.

F3 - lifecycle:
- Re-attach the job after each restart and prove call, loss, and restore on a
  live endpoint, not on an endpoint that was already detached.
- Compare the full parsed tool list across both jobs and the seller control
  view, instead of a regex prefix that stopped at the first bracket.

F4 - reproducibility:
- Add a build receipt to manifest.json: source commit, crate tree object,
  lockfile and Dockerfile hashes, base-image digests, the built image id, and a
  hash of every raw capture.

Demo run: 39 checks, 0 failures. The two F1 bugs above were latent in the
earlier WIP and surfaced only once the demo could run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deliverables owed by the ordering seat, for onboarding a new vendor tool.

- .claude/skills/seller-tool-onboarding/SKILL.md: the repeatable procedure. It
  states the per-seller model, the steps, the safety invariants, how to test,
  and what is out of scope.
- templates/seller-tool-config.template.json: a skeleton of the real config the
  holder loads (config.rs), with placeholders.
- templates/README.md: a field-by-field guide, the safety rules the holder
  enforces, a runnable example (the text-transform config), and an illustrative
  example that maps a choice and a text parameter.

Written in Simplified Technical English.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docs/handoff/CONTINUATION-2026-09-10.md: continues the handoff README. It
  records how each finding was closed, the verification results, the owed kit,
  and a precise production-integration plan with confirmed insertion points.
- 04-token-grant-contract.md: replace the "F2 not yet safe, F1/F3 under repair"
  status with the fixed state, and cite the safeio module and the new controls.

Written in Simplified Technical English.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
evidence/20260910T110320Z is a run of the repaired demo: 39 checks, 0 failures.
It demonstrates F1, F2, and F3 end to end. Its CAVEAT.md records that the image
was built offline from rust:1-bookworm, not the pinned digests, because the
Docker buildkit resolver was wedged on the pinned base metadata; the bundle
stands for mechanism, and the F4 digest-pinned build must be re-run once registry
resolution works.

- README.md: mark the current bundle, and mark the two 2026-09-09 bundles
  superseded (pre-repair). The old ones are kept, not deleted.
- .gitignore: keep the multi-MB filesystem-capture tar out of the repository;
  the scan summary and the transcript hashes are the evidence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Petar's decision, 2026-09-10. Browser-based authentication is not supported for
now. It fits the enroll-once holder model only when the vendor login persists and
the holder can refresh it without a browser. A vendor that issues only
short-lived, non-refreshable tokens would force a per-job re-login, which the
enroll-once-per-daemon model cannot hold. It becomes supportable when a concrete
vendor offers a refreshable browser session; the holder already accommodates that.

Recorded in the continuation handoff, the onboarding skill, and 08-gaps C.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A code-grounded plan for wiring the holder into the product. Every site was read
in the source on 2026-09-10.

- Step 1: supervise tool-holderd from SellerNode::boot_with_lock (run.rs:3991)
  and the shutdown seam; hold the handle on the node.
- Step 2: add an optional HeldToolConfig to SellerConfig (home.rs:193).
- Step 3: mount the per-job socket via launch_with_mounts extra_mounts
  (seller_exec.rs:711) at /run/holder, nothing else.
- Step 4: a JobToolEndpoint RAII guard that attaches on new and detaches on
  Drop, mirroring NetnsHolder and JobContainer.
- Step 5: set mcp_servers to one tool-mcp-bridge McpServer (seller_exec.rs:2411);
  bake the bridge into the sandbox image.

Includes the feature gate (held_tool = None behaves as today), the credential
and egress boundary, a test plan, and open decisions. Linked from the
continuation handoff and the spec index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pmilic021 and others added 4 commits September 14, 2026 12:13
…-09-14

One holder container, two jobs on one enrolment (one under egress containment), an escape refused,
a daemon-style restart that resumed the persisted login, and a real claude-agent-acp turn that
called the seller's tool through tool-mcp-bridge over the job's socket - all through
HeldTool::start/attach/shutdown, prepare_launch, launch_with_mounts and the cleanup capture, with
the kit's fake vendor as the oracle. The synthetic secret appears in no file. The README states
what each run proves, the facts, one thing learned about docker volumes, the limits, and how to
rerun.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…et and one bridge per tool

`[sandbox.held_tool]` becomes the list `[[sandbox.held_tools]]`. Each entry carries a required,
unique `server_name`: the name the agent addresses (`mcp__<name>__<operation>`), the name of the
holder container and its two volumes (`maxplayer-held-tool-<seat>-<name>`), and the name of the
job's socket directory (`/run/holder/<name>`). Config resolution refuses a name that is not plain,
a duplicate, or one a `[[sandbox.mcp_tools]]` entry also claims, because both lists land on the
job's session.

- `held_tool.rs`: `holder_names(seat, server_name)`; `job_socket_mount` / `job_socket_path`
  per tool; `JobToolEndpoint::attachments` mounts the tool's `jobs/<job>` subpath at its own path
  and gives the bridge `--socket /run/holder/<name>/job.sock` as an ARGUMENT - a stdio server's
  arguments reach the child on every harness, its environment only if the harness maps it.
- `tool-mcp-bridge` (kit) gains `--socket <path>`; the environment form stays for the demo.
- `seller_exec.rs`: `JobAttachments::merge` joins the per-tool attachments into the one list the
  launch takes; the validation of the list at config resolution.
- `seller_node/run.rs`: the runner holds `Vec<Arc<HeldTool>>`; boot starts every holder
  concurrently, applies the fail posture per tool, and stops the holders that did start when a
  required one refuses the boot; attach and detach loop over the tools on both job paths; shutdown
  loops too.

Tests: the unit tests follow the names; the config tests cover the list, the missing name, the
duplicate and the cross-list clash; the live tests now run TWO tools - two holders enrolled once
each, one job driving both through their own sockets, a contained job, a restart resuming both
logins, and a real claude-agent-acp turn calling both tools. Both green on 2026-09-14 (bundle
`evidence/20260914T103608Z-holder-route-two-tools/`, committed next). The sandbox image was rebuilt so its
bridge understands the flag.

Docs: the template comment, the quickstart, the skill, doc 09's table, doc 10's row, the kit's
templates README, and CONTINUATION section 11.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two holders enrolled once each; one job drove both tools through their own socket mounts; a job
under egress containment; a restart that resumed both logins; and a real claude-agent-acp turn
that called both tools, each vendor seeing one login and one transform. The kit's fake vendors are
the oracle, so this is the mechanism through production code. Neither synthetic secret appears in
any file. The evidence index names this the current Holder bundle and keeps the single-tool one as
the record of its run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
main brought the resolver file for contained jobs (`JobLaunch::resolv_conf`, `[sandbox]
dns_servers`, the DNS and egress live gates) and the relay-push authority work. This branch brought
the tool attachments (`JobLaunch::mcp_servers`, `[sandbox] mcp_tools`, `[sandbox] held_tools`,
`ExtraMount`, `JobAttachments`). Four files conflicted, and every hunk was the same shape: both
sides added a field, or a literal's line, at the same spot. Both were kept everywhere.

Two follow-ups the merge needed beyond keeping both sides: the resolver field landed after the
`JobLaunch` struct's closing brace and was moved back inside it; main's DNS live gate builds
`ExtraMount::Bind` values for the launch, which takes typed mounts now.

Verified on the merged tree: every CI feature set compiles; both routes ran live again under egress
containment and passed (the Holder route with two tools, the Proxy swap against GitHub).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
mobee Ready Ready Preview Sep 16, 2026 9:25am UTC

Request Review

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pmilic021 and others added 4 commits September 15, 2026 12:33
…FOs, stream SSE in the bridge

Review round 2 of pull request #1004, findings 3, 4 and 8.

Finding 3. A job connection resolved its job id through the holder's table at call
time. A connection held across a detach and a re-attach of the same id read and wrote
the new attachment's directory. Now each attach creates one immutable `Attachment`
instance. The accept loop binds every connection to that instance. Each call checks
the instance's `stop` flag before validation, before the tool runs, and before each
publish. A live id cannot be attached a second time.

Finding 4. A job-planted FIFO at an input or output name blocked a holder thread in
`open`, and an output FIFO could receive bytes after detach. `safeio` now opens with
`O_NONBLOCK`, checks the held descriptor with `fstat`, and refuses everything that is
not a regular file before any read or truncation. Connections per attachment are
bounded (16). An idle job connection ends 30 s after a detach.

Finding 8. `mcp-http-bridge` read a response to EOF before it wrote anything, and
read stdin only between responses. A server request sent in the middle of an SSE
stream could never be answered. The bridge now runs one worker per stdin message,
reads the response as it arrives (`http::request_streaming`, `SseSplitter`), and
writes each JSON-RPC message as soon as its event is complete. A stdin line is
classified as a request, a notification, or a response. A response's `202` with an
empty body draws no error line. `vendor-mcp --server-request` and the `swap-proxy`
test double stream too, so the suite can prove the exchange.

Tests: 74 pass (`cargo test -p maxplayer-tool-kit --locked`), clippy clean. New:
`tests/attachment_binding.rs` (six, against the real daemon), six `safeio` unit
tests with `mkfifo`, two `proxy_swap_suite` tests, and unit tests for the streaming
reader, the SSE splitter, and the message classifier.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ins, refuse tools under launcher

Review round 2 of pull request #1004, findings 1, 2, 9 and 11.

Finding 1 (DENY). The proxy substituted a placeholder in every request header. A job
could put its placeholder in a header the vendor reflects into a response body, and a
`\u`-escaped reflection returned the credential past the byte scrubber. `JobCredential`
gains `substitute_in: HeaderScope`. An MCP tool registers with
`HeaderScope::authorization()`: the placeholder is recognized and substituted in the
`Authorization` header only, every other header goes to the vendor as the job wrote
it, and a placeholder that appears only outside the scope is `NoKnownPlaceholder`.
Env and file credentials keep `HeaderScope::Any`, the behavior they had. A scope that
names no header is refused at registration.

Finding 2. `allows_paired_redirect` compared authorities and treated `:443` and `:80`
as equal, so a redirect from `https` to `http` on the same host was approved. It now
compares origins: scheme, lowercased host, effective port. `same_authority` no longer
equates two different explicit ports.

Finding 9. Launcher mode accepted `mcp_tools` and `held_tools` and served neither.
Both tables now require `mode = "docker"`.

Finding 11. Live test A read the credential's absence off the redacted diagnostics
capture, which proves the redactor, not the boundary. It now also reads `docker logs`
of the job container raw, before the redacting capture, and asserts absence there.

Tests: the stub vendor of the round-trip test records a reflected header and sees the
placeholder; new proxy tests for the scope, the origin comparison, and the port rule;
`launcher_mode_refuses_both_tool_tables`. Core gates on this tree: 449, 498, 1564 and
1629 pass across the four feature rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it, bound every docker call

Review round 2 of pull request #1004, findings 5, 6, 7 and 10.

Finding 5. A start that failed after `docker run` (the status wait, the subpath probe)
returned before any guard owned the container. An enrolled holder stayed running with
no owner. `HeldTool::start` now arms a `StartGuard` before its first `docker` call and
runs the rest in `start_owned`. A failure removes the container and the runtime
volume on the async path; a cancellation removes them from the guard's drop. The state
volume stays.

Finding 6. `shutdown` set `stopped`, and `detach` set `detached`, before the docker
call. A failure disabled the drop fallback, and `shutdown` reported a stop that had
not happened. Both flags now follow a confirmed result. `remove_container` is `Ok`
only when the container is gone. Both return `Result`, and the daemon logs an `Err`.

Finding 7. Every `docker` call ran with no deadline. `run_bounded` spawns the child,
drains both pipes on threads, polls for exit, and kills it at the deadline. Queries
get 20 s, `holderctl` calls 30 s, `docker run` 120 s, `docker rm` 60 s. The drop
fallbacks run through it too.

Finding 10. A boot removed only the configured holders' stale containers. A holder
whose tool was removed or renamed while the daemon was killed stayed enrolled.
`reconcile_stale_holders` runs at boot on a docker seat: it lists the seat's holders
by label, removes every one the config no longer names, and keeps their state volumes.

Tests: `stale_holders_are_this_seats_unconfigured_holders_and_nothing_else`,
`a_bounded_docker_call_is_killed_at_its_deadline`. The Holder live proof with two
tools passed on this code and the rebuilt holder image on 2026-09-15.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e reflection limit

Review round 2 of pull request #1004 (a Codex agent, 2026-09-15): eleven findings, all
agreed and fixed in the three commits before this one. Section 12 of the continuation
document lists each finding, its fix, and the test that proves it, then the reruns on
the fixed tree: the kit (74), the four core rows (449 / 498 / 1564 / 1629), the CLI rows,
both rebuilt images, the Holder live proof with two tools, and the GitHub acceptance of
the Proxy swap route with the streaming bridge.

The routing document, the skill and the quickstart now say what the proxy does: it
substitutes in the `Authorization` header only, for the vendor's host only, and it does
not read a response body. The residual risk is a vendor that reflects its own
`Authorization` header into a body. Such a vendor is not to be onboarded on this route.

The GitHub evidence README separates the raw observations (the container's inspect,
argv, session entry, transcript, and now its raw logs) from the redacted diagnostics
capture, whose absence check proves the redactor and not the boundary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pmilic021

Copy link
Copy Markdown
Contributor Author

Review round 2 (2026-09-15): all eleven findings are accepted and fixed in four commits on this branch.

Commit Findings
d51f136 fix(tool-kit) 3 (a connection is bound to its attachment instance), 4 (O_NONBLOCK opens, regular-file check, connection bound), 8 (the bridge streams SSE, reads stdin while a stream is open, answers a server request mid-stream)
5a48f53 fix(core) 1 (HeaderScope: the MCP swap substitutes in Authorization only), 2 (redirects compare origins: scheme, host, effective port), 9 (launcher mode refuses both tool tables), 11 (live test A reads docker logs raw before the redacting capture)
3fd1564 fix(core) 5 (StartGuard owns a failed or cancelled start), 6 (shutdown and detach set their flags after a confirmed result and return Result), 7 (every docker call has a deadline and is killed at it), 10 (boot reconciles stale holders by the seat label)
367e899 docs The routing doc, the skill and the quickstart state the swap scope and the reflection limit; the evidence README separates raw observations from the redacted capture.

docs/handoff/CONTINUATION-2026-09-10.md section 12 has the table of finding, fix, and the test that proves each, plus the open items after this round (a vendor that reflects its own Authorization header is the residual risk; env and file credentials keep the any-header scope; the bridge opens no GET stream; a detach does not kill a tool run in flight).

Gates on this tree: kit 74; core 449 / 498 / 1564 / 1629; CLI 161 of 162 and 199 of 200. The one CLI failure is doctor::tests::sandbox_image_check_is_wired_into_the_boot_gate, and its cause is the local Docker Desktop credential helper, which hangs on this machine; the test passes with an anonymous docker config. Both images were rebuilt from this tree. The Holder live proof with two tools and the GitHub acceptance of the Proxy swap route (run A, contained) passed again on them. The agent-turn live tests were not rerun.

🤖 Generated with Claude Code

pmilic021 and others added 3 commits September 15, 2026 17:24
…n inspect as unknown, fail a held pipe

Review round 3 of pull request #1004 (the re-review of round 2), findings 2, 3, 4, 7 and 8,
and the cancelled-attach gap round 2 left open.

Finding 2. `StartGuard` removed the holder's resources at once when the start future was
dropped, while `docker run` was still creating the holder on the blocking pool; the
container then appeared with no owner. `OwnedCall` replaces the guard: the blocking task
records when the call ended, the future's owner records the abandonment, and whichever
comes second runs the cleanup, so it runs exactly once and only after the effect exists.
Every call in `start_owned` goes through it. `attach` owns its attachment the same way: a
holder that attached the job after the daemon stopped waiting is told to detach it.

Finding 3. `container_exists` read any non-zero `docker inspect` as "absent", so a docker
daemon that was down let `shutdown` mark a running holder stopped and disarm its fallback.
`inspect_outcome` now reads exit 0 as present, the daemon's own "no such" words as absent,
and anything else as an error.

Finding 4. `run_bounded` collected the child's pipes for two seconds and replaced a timeout
with empty output and a success code; a descendant that held a pipe made `docker ps` read
as empty and the reconcile remove nothing. The child now runs in its own process group,
one deadline covers the run and the collection, a pipe held past it is an error, and the
group is killed so the descendant does not outlive the call.

Finding 7. The boot reconciled stale holders only on a docker seat. It now reconciles
whatever the mode is, because the holders to remove are the previous configuration's; a
host with no docker CLI is quiet.

Finding 8. Live test A asserts that `docker logs` succeeded before it scans the raw logs.
The evidence README says that run B has no raw observation of its own.

Tests: `a_descendant_that_holds_the_pipes_fails_the_call_instead_of_emptying_its_output`,
`inspect_words_decide_presence_and_an_unknown_answer_is_an_error`,
`an_owned_call_dropped_in_flight_cleans_up_exactly_once_after_the_child_ends`,
`an_owned_call_that_completes_cleans_up_only_when_it_stays_armed`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rker at its answer, expire idle connections

Review round 3 of pull request #1004 (the re-review of round 2), findings 1, 5 and 6.

Finding 1. A call that passed the last detach check could pause before it wrote; detach
completed, the same job id attached again at the same pathname, and the old call wrote
into the new attachment's directory. `Attachment` gains a publish lock. Every staged
output is read into memory first, outside the lock. The `stop` check and the writes are
one unit under the lock. `stop_attachment` sets `stop`, then takes and releases the lock,
so it waits for a publication in flight before it removes the socket and returns. After
detach returns, no old call can write.

Finding 5. A vendor that sent the final response and then kept its SSE stream open left
the bridge's worker reading forever, and every request added a worker and a connection.
A request worker now stops reading and drops its connection once its answer is written.
Request workers are bounded (32): a request over the bound gets one immediate error line
on its id. Notifications and responses are never bounded, so an answer to a server
request always goes out. `vendor-mcp` gains `--hold-stream` and `--delay-ms` for the tests.

Finding 6. The holder's read timeout only checked for a detach and continued, so silent
connections on an attached job held every slot. `tool-mcp-bridge` opens one connection
per message, so idle expiry is safe: a job connection with no complete request within the
idle timeout is closed, attached or not. New daemon flag `--job-idle-timeout-secs`
(default 30).

Tests: 78 pass (was 74), clippy clean. New:
`attachment_binding::a_call_that_pauses_before_publishing_cannot_write_after_the_detach`,
`attachment_binding::silent_connections_expire_after_the_idle_timeout`,
`proxy_swap_suite::a_request_worker_ends_when_its_answer_arrives_even_if_the_vendor_holds_the_stream`,
`proxy_swap_suite::requests_over_the_in_flight_bound_get_one_error_line_and_the_rest_are_served`.
The `HEAD` daemon fails both holder tests as the reviewer described.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nt mode; record review round 3

Review round 3 of pull request #1004, finding 7, completed. The reconcile of stale holders
at boot ran only on a docker seat, so a seat that left docker mode or dropped `[sandbox]`
kept the holders of its previous configuration. The mode is no longer the test: a marker
file in the seat's home (`held-tools-started`) is written when holders start and removed
once a boot with no held tools has reconciled. The reconcile runs when the config names
held tools or the marker exists, so a seat that never held a tool makes no `docker` call
at boot (an unconditional call at every boot, including every test boot, was the first
attempt, and it is not needed).

The handoff document's section 12 gains the round-3 table (eight findings, all agreed and
fixed), the reruns on the fixed tree, and the closed cancelled-attach item.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pmilic021

Copy link
Copy Markdown
Contributor Author

Review round 3 (2026-09-15): all eight findings on the round-2 fixes are accepted and fixed in three commits, 367e899..5ed3b08.

Commit Findings
a0368ce fix(core) 2 (OwnedCall: a docker call's effect is owned across a dropped future; the blocking task and the future's owner hand the cleanup off, so it runs once and only after the effect exists; attach is owned the same way, which also closes the cancelled-attach gap), 3 (inspect_outcome: an unknown docker inspect is an error, never "absent"), 4 (the child runs in its own process group, one deadline covers run and collection, a held pipe is an error and the group is killed), 7 (first attempt), 8 (docker logs must succeed; run B has no raw view)
c015c66 fix(tool-kit) 1 (Attachment.publish lock: staged outputs are read first, the stop check and the writes are one unit under the lock, detach waits for a publication in flight), 5 (a request worker ends at its answer; 32 requests in flight at most, notifications and responses never bounded), 6 (real idle expiry, --job-idle-timeout-secs, default 30; tool-mcp-bridge connects per message)
5ed3b08 fix(core) 7 (the reconcile runs when the config names held tools OR a marker file held-tools-started in the home exists, written when holders start and removed once a boot with no held tools reconciled; a seat that never held a tool makes no docker call at boot)

docs/handoff/CONTINUATION-2026-09-10.md section 12 has the round-3 table with the test that proves each fix. Negative controls: the HEAD holder daemon fails the new publish-race and idle-expiry tests as described.

Gates on 5ed3b08: kit 78; core 449 / 498 / 1568 / 1633; CLI 162 / 162 and 200 / 200 (with an anonymous docker config, so the doctor test reaches the registry client; the local credential helper hangs). Both images were rebuilt from this tree; the Holder live proof with two tools and the GitHub acceptance of the Proxy swap route (run A, contained) passed again on them. Two fixture flakes were seen once each in full rows and pass alone (offer_backfill_recovers_an_offer_the_deaf_live_sub_never_delivered, and your second_acquire_fails_closed_while_first_is_held); neither module is touched by the branch.

🤖 Generated with Claude Code

pmilic021 and others added 2 commits September 16, 2026 11:01
Review round 4 of pull request #1004, the one non-blocking follow-up. The attach error
path ran a best-effort detach for every failure, so a duplicate attach that the holder
refused ("already attached; detach it first") detached the attachment that already
existed and belonged to someone else. `attach_answer` now classifies the call: the holder
attached (exit 0), the holder refused (a non-zero exit: nothing was attached by this call,
nothing to take back), or unknown (the call did not complete: killed at its deadline, or
`docker` failed), and only the unknown case runs the best-effort detach.

No normal daemon path issues a duplicate attach; the fix keeps the endpoint safe if one
ever does.

Test: `an_attach_the_holder_refused_is_not_taken_back_but_an_unknown_one_is`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…low-up, the red money-path check

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pmilic021

Copy link
Copy Markdown
Contributor Author

Review round 4 (2026-09-16): the one follow-up is fixed, and CI is green on the current head d900aa8.

  • 5d74245 fix(core): an attach the holder refused is not taken back. attach_answer classifies the holderctl attach call as attached (exit 0), refused (a non-zero exit: nothing was attached by this call, and an attachment a duplicate refusal names is someone else's), or unknown (killed at its deadline, or docker itself failed). Only the unknown case runs the best-effort detach. Test: an_attach_the_holder_refused_is_not_taken_back_but_an_unknown_one_is.
  • d900aa8 docs: section 12 of the handoff records round 4.

The red Money-path check on 5ed3b08 was seller_node::run::tests::a_losing_open_pool_claimant_releases_its_slot_when_it_sees_the_award, at its 5 s relay pump (run.rs:12928, a test from 2026-08-06 that the branch does not touch); 1577 other tests in that row passed. Locally it passed 5 of 5 in debug and 3 of 3 in release with the job's exact features (--no-default-features --features gateway,git-delivery,wallet,live-mints). On d900aa8 all seven CI jobs passed, Money-path included, so the failed check is cleared by a fresh run, not by a local pass.

🤖 Generated with Claude Code

@pmilic021
pmilic021 merged commit 00c05bb into main Sep 16, 2026
9 checks passed
pmilic021 pushed a commit that referenced this pull request Sep 16, 2026
…1004)

Derived from git log b9ee77f..00c05bb and the #1004 PR body. States which
claims are live validation (Proxy swap against GitHub's remote MCP server;
the Holder route's live run with the kit's fake vendors) and which are
source-level only. No historical section and no version pin is touched.
pmilic021 added a commit that referenced this pull request Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant