ecash stage 3a: the issuer sidecar becomes real (wizard, counters, retirement, producer) - #969
Open
maxy-player wants to merge 10 commits into
Open
ecash stage 3a: the issuer sidecar becomes real (wizard, counters, retirement, producer)#969maxy-player wants to merge 10 commits into
maxy-player wants to merge 10 commits into
Conversation
Stage 1 of the ecash mutual-credit build. One new OPTIONAL tag on the seat announcement, `["issuer_mint", url, cap, outstanding, retired, last_seen]`, carrying the seat's own mint URL and its issuance counters. Emitted from `HeartbeatDraft` (`with_issuer_mint`), parsed into `ParsedHeartbeat.issuer_mint`. Absent or malformed reads as unstated, never a rejection. Announcement only, never on the kind-3402 claim (protocol-v1 §4.2 "Issuer mint"). No version bump: a reader MUST ignore unrecognised tags (§2.1). No production publish path sets it yet; stage 3 wires live counters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner decision (Bob, 2 Sep, "lets keep it simple for now - no wrapper, no limit"): nothing enforces a ceiling on an issuer's outstanding tokens, so the tag no longer declares one. The tag is positional and shrinks to four values: `["issuer_mint", url, outstanding, retired, last_seen]`. Both index tables (the doc's position table and the malformed-shape test's field table) are renumbered: outstanding 3->2, retired 4->3, last_seen 5->4. `IssuerMintAd` loses `cap_sats`; serializer, parser destructure, the wire-shape fixture and the arity labels follow. The counters stay: they are the only trust signal an operator reads before extending credit.
…the tag The tag shrank to four values when `cap` was removed; one module-doc sentence at the top of the section still said five. Doc only.
…stage 2) The buyer wallet learns a mint CLASS (docs/protocol-v1.md §4.2 "Issuer mint"): Lightning, or ISSUER — a seat's own Cashu mint whose tokens are an IOU for that seat's work and have no Lightning route in or out. Markers, each recorded with WHO said it (`mint_class::IssuerMarker`): - Own: this seat's own mint, from a new optional `issuer_mint` config key. - Info: a mint whose NUT-06 info lists no bolt11 method under NUT-04/05, learned by a bounded GET /v1/info per accepted mint at accept. - Declared: the seller's own kind-30340 `issuer_mint` tag, read off its announcement at accept by (author, kind, d). Rules: - The real-mint fence admits an Own/Info issuer mint whatever `allow_real_mints` says (it carries no sats). A Declared mint does NOT widen the fence: a seller's signed word must not open the buyer's fence to any mint it names. Every marker refuses the hop. - `plan_payment` refuses a Lightning hop INTO or OUT OF any issuer mint with the plain reason "you hold none of this seller's currency"; an issuer entry is never a hop target. Direct payment at one is unchanged. - `select_source_mint` prefers the buyer's OWN mint when the seller lists it. - The hop executor asks both mints their class before any leg (run_hop gate + plan_quotes guard); `wallet fund` / `wallet melt` refuse at a mint whose info lists no bolt11. An unreachable info read is UNKNOWN, i.e. Lightning, so reachability errors keep their existing labels. - The class knowledge is SEALED into the accept-bind (`issuer_mints`, with markers) and re-derived at pay; legacy binds read as none known. Unit stays `sat`; the three sat gates (gateway.rs parse_offer, payment_wallet.rs terms_for_offer + seller receive) are untouched.
`complete_mint_async` opened the wallet and went straight to `poll_and_mint` (`check_mint_quote`, then `wallet.mint`) with no issuer-class check — the guard sat only on fund-begin and melt. Insert the same `refuse_lightning_op_at_issuer(&wallet, "wallet fund", &mint_url)` after the wallet opens, before any quote call; nothing else in the function changes. NEGATIVE test, offline: a same-process mint stub that answers `/v1/info` (and the keysets refresh cdk makes alongside it) and RECORDS every request path. At a stub whose NUT-04/NUT-05 list no bolt11 (Issuer), completion returns `WalletOpsError::IssuerMint` and the mint sees only the info read — no `check_mint_quote`, no `wallet.mint`. Control: the identical call at a bolt11-listing stub passes the guard and fails later inside `poll_and_mint` as an ordinary Wallet error, so the gate is the class, not the stub.
…ape)
A mint is an issuer mint because an operator said so — this seat's own
config (`IssuerMarker::Own`) or the counterparty's kind-30340 ad
(`IssuerMarker::Declared`) — and never because its /v1/info document
listed no bolt11 method. Owner's ruling, 3 Sep 2026 ("lets do 2").
Dies: `class_from_info`, `IssuerMarker::Info`, `IssuerMints::from_urls`,
`probe_issuer_mints`, and the three production network reads that
existed only to obtain a class — the probe in mint_class.rs,
`CdkHopEffects::class_of` in crossmint_hop.rs (`load_mint_info`), and
`refuse_lightning_op_at_issuer` in wallet_ops.rs (`load_mint_info`).
Replaces: `IssuerMarker::admits` is `matches!(self, Self::Own)`; nothing
else about admission changes. `CdkHopEffects::open` takes the caller's
`IssuerMints` and fixes both legs' classes there — the bind's seal on
the pay path, this seat's own config on the recovery sweep — and
`HopEffects::mint_classes` returns them. `wallet_ops`'s guard reads own
config and runs before the wallet opens. Accept builds
`IssuerMints::none().with_own(..).with_declared(..)`.
Kept: `IssuerMintSeal` and accept-time sealing. A seal written under the
retired `info` marker still loads: `#[serde(alias = "info")]` reads it
as `Declared` — the hop it refused it still refuses, it admits nothing,
and it re-serializes as `declared`.
Tests: 3 removed (a_mint_listing_no_bolt11_method_is_an_issuer_mint,
bolt11_on_either_table_reads_as_lightning,
every_marker_refuses_but_only_own_and_info_admit), 4 added
(every_marker_refuses_but_only_own_admits,
a_legacy_info_seal_still_reads_as_a_declaration_and_admits_nothing,
an_undeclared_mint_is_fenced_exactly_as_before_whatever_it_is,
crossmint_hop_plan_quotes_refuses_a_declared_issuer_leg_without_asking_the_mint);
the wallet_ops check-H test now asserts the declared mint receives NO
request at all, not even /v1/info, while its stub would answer as a
Lightning mint if asked.
`grep -rnw -E 'class_from_info|load_mint_info|get_mint_info'` under
crates/: 17 hits in 4 files at 3f13a1c, 1 at this commit (doctor.rs:93,
the reachability probe, which obtains no class).
…ters, retirement, producer) The `issuer_mint` protocol tag (§4.2) shipped in stage 1 and nothing in production had ever produced one. This is the producer. `maxplayer issuer init` writes the sidecar's files and nothing else: a fresh BIP39 mnemonic at `<home>/mint-seed` mode 0600 (kept, never overwritten, if one exists), `<home>/mint/mintd-config.toml` with NO `mnemonic` key — the seed reaches cdk-mintd by `--seed-file` — and `issuer_mint` / `accepted_mints` / `extra_mints` in config.toml. It does not install, spawn or supervise the mint; it prints the command. `issuer status` reads the counters out of the mint's own sqlite, opened read-only: issued = sum(blind_signature.amount), redeemed = sum(proof.amount where state='SPENT'), outstanding = issued - redeemed. `retired` cannot come from there — the mint burns a proof without recording who presented it — so it is the seat's own durable count in `<home>/mint/retired.jsonl`, gated by retired <= redeemed. `issuer retire` burns. Measured against cdk-mintd 0.17.2 on 4 Sep: a NUT-05 melt of a well-formed bolt11 the mint never issued, that nothing pays, moves every input proof to SPENT and signs NO new blind signature — 18 proofs / 100 sat burned with sum(blind_signature.amount) unchanged. No second mint, no Lightning. `wallet melt`'s stage-2 refusal is untouched: retirement is not a melt on the wallet surface. `heartbeat_for_state` and `retraction_for_state` take the advertisement as a REQUIRED `Option<IssuerMintAd>`, for the reason `admission` is required: a caller that could omit it would publish a seat whose silence is indistinguishable from one too old to speak. Both publish sites pass it. The negative is the load-bearing half. No issuer mint, a sidecar that is DOWN, a sqlite that will not open, a ledger line that will not parse, or a URL outside `accepted_mints` all yield an ABSENT tag — the beat still publishes and the seat stays on the market. Liveness is a `/v1/info` GET, not a file read: a dead mint's sqlite still reads, and publishing its stale counters with `last_seen = now` would be the wrong number §6 forbids. `send_async` and `receive_async` get the class-aware fence the rest of the product already has, so the seat can hold the currency it issues. Only the `Own` marker admits; a counterparty's declaration widens nothing. Out of scope by maxie's ruling: the two-seat loop (stage 3b, after MakePrisms#964). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Someone is attempting to deploy a commit to the MakePrisms Team on Vercel. A member of the Team first needs to authorize it. |
added 3 commits
September 4, 2026 06:10
`ensure_seed` took `path.exists()` as the whole answer, so the 0600 promise only ever covered a seed this seat created. A file some earlier hand left at 0644, or a symlink pointing somewhere the seat does not own, was adopted silently and the wizard still reported success. Now an existing path is checked before it is kept: - Anything that is not a regular file is refused by name, symlink included. The check is `symlink_metadata`, which does not follow links — plain `metadata` reports the TARGET's type and would let a link through. A symlink is refused even when its target is a fine 0600 file, because the seat cannot promise the mode of a path it does not own. - On Unix a mode with any bit beyond owner read+write (`mode & 0o177`) is narrowed to 0600 in place. `set_permissions` does not open the file, so no byte is read or rewritten. A mode already tighter than 0600 is left alone. The phrase stays unread, unreturned, unlogged: the added errors name the path and the file type and never open the file. Two regressions, both proved load-bearing against the previous body (they FAIL on it, PASS here): - `an_existing_over_readable_seed_is_narrowed_to_0600_without_touching_its_bytes` plants a 0644 non-mnemonic file, then asserts the bytes and the mtime are unchanged and the final mode is 0600. - `a_seed_path_that_is_not_a_regular_file_is_refused` covers the symlink and the directory branches and checks nothing was written through the link. `init_keeps_an_existing_seed_and_is_idempotent` is untouched — it reuses the already-safe file the first init wrote, which is exactly why it could not catch this.
The existing-seed narrowing read the mode with `& 0o7777` and then tested it against `0o177`. Those masks disagree: `0o4600 & 0o177 == 0`, so a regular seed file carrying setuid scored clean, the branch was skipped, and the bit survived on money-shaped state while the code reported the file narrowed to 0600. The regression could not see it either. It asserted `mode & 0o777 == 0o600`, and `0o4600 & 0o777 == 0o600` passes. Both mistakes are one mistake — a mask narrower than the value being judged. - The disallowed set is now spelled `const DISALLOWED: u32 = 0o7000 | 0o177`: the special bits (setuid, setgid, sticky) as well as everything below owner read+write. `0600` is the one mode with neither, so `mode & DISALLOWED != 0` is exactly "not 0600 or tighter". Masking the read down to `0o777` instead would have hidden the bit rather than cleared it — the check has to see what `set_permissions` is about to overwrite. - Both mode regressions now assert `& 0o7777`, never `& 0o777`. - `an_existing_setuid_seed_has_its_special_bits_cleared` plants a regular file at `04600` and asserts bytes unchanged, mtime unchanged, and `mode & 0o7777 == 0o600`. It also asserts the two properties of the old code that made this invisible, so the hole cannot be reopened quietly. - The `ensure_seed` doc comment now describes the mask the code actually uses. Everything already passing is unchanged: symlink refusal through `symlink_metadata`, refusal by name and file type, no byte read or rewritten, a mode already tighter than 0600 left alone, and `init_keeps_an_existing_seed_and_is_idempotent` untouched. Proved load-bearing against the previous body: the new test FAILS there, leaving the mode at decimal 2432 (0o4600) against an expected 384 (0o600), and passes here. The other 14 tests pass in both arms.
`the_seed_is_owner_only_and_never_leaves_its_file` read a real seed file's mode and masked it with `0o777` before asserting `0o600`. Every other mode assertion in this module moved to `0o7777` with the mask fix; this one was missed. No production hole follows and the assertion was never wrong: that seed is created fresh by `init` in the same test, so no special bit can be present and the two masks cannot disagree there. It was weaker than it claimed to be, which is the entire defect — a file-mode assertion masks the whole mode word so the special bits are witnessed rather than discarded. `:1081` is deliberately left at `0o777`. It is arithmetic about the OLD mask (`0o4600 & 0o777 == 0o600`), asserted so the trap cannot be reopened quietly, not an assertion about any file's mode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 3a — the issuer sidecar becomes real
The
issuer_mintprotocol tag (docs/protocol-v1.md§4.2) shipped in stage 1 andnothing in production had ever produced one: at the base below, all five
with_issuer_mintcall sites are tests. This is the producer. A seat stands up its ownCashu mint, reads its own counters out of it, and tells the truth about them on the wire
— including when it has nothing to say.
This branch STACKS on #962 and #966, so it shows their commits too
Measured at write time with
git ls-remoteandgit log upstream/main..HEAD:fc67661069461e1ee7728e9921aab3312433011b(= mint class: the issuer class is declared, never sniffed (stage 2) #966's head)upstream/main:9d41b7faf3a4f5140dbcc3809935f0191572b36d, read withgit ls-remoteatthis write. It moved since this PR was opened — it was
8c3bc9b834fefaf1fc8985ef5eee172a10f4684cthen. The mover is #968 (relay
scoped token lifetime), which touches nothing this branch touches. heartbeat: issuer_mint seat-ad tag + reader (agent-issued ecash, stage 1) #962, mint class: the issuer class is declared, never sniffed (stage 2) #966 and one mint concept: retire the real-mint fence and the play-money class, leaving the seat's accepted list as the only gate #964 are
all still OPEN, so the stack below is unchanged.
main; 4 of them are new here(
17b42837f6aafb9b9bbb258715915a29cc47df89,1d7869f35d00822408c3fe6cdf46e8e4f67bafd0the seed-custody fix,a22c554c9dbe8d718d01fd633378b557da8cec52the mask correction, ande0a36d5e129b02ef84f90af5c85dc0d28e925b04, which brings the last file-mode assertion inissuer.rsto0o7777). The other six are heartbeat: issuer_mint seat-ad tag + reader (agent-issued ecash, stage 1) #962 (e78b177and its two predecessors) andmint class: the issuer class is declared, never sniffed (stage 2) #966 (
b77d371,3f13a1c,fc67661).It needs the same rebase over #964
that #966 needs. #964 dissolves
the loopback-
http://fence into a shape rule and makesaccepted_mintsmembership theone gate; that rebase will collapse the class-aware predicate this PR adds at two sites in
wallet_ops.rsinto whatever #964 leaves standing. I did not pre-empt it.The two-seat loop is STAGE 3b, deferred by maxie's ruling
A real job paid in issuer credit is deliberately out of scope, not an oversight: at this
base only the
Ownmarker admits (mint_class.rs:79-81), so a counterparty that readsthis seat's
issuer_minttag records itDeclaredandhome::mint_allowedrefuses aloopback
http://URL outright — a second seat cannot hold or return these tokens atthis base. No fixture pretends otherwise. It lands on top of #964.
What's here
maxplayer issuer init— local files only. No relay, no wallet, no network. It doesnot install
cdk-mintd, does not spawn it and does not supervise it: it writes files andprints the exact command.
<home>/mint-seed, mode0600, a fresh BIP39 mnemonic — never printed, never logged,never in an error message. An existing one is kept and said so, the same shape as
the seat key: a lost mint seed is lost money-shaped state.
path.exists()as the whole answer, which meant the0600promise above only evercovered a seed this code had itself created — a
0644file, or a symlink, left at thatpath by an earlier hand was adopted silently and
initstill reported success. Fixed in1d7869f: anything that is not a regular file is refused by name, symlink included (thecheck is
symlink_metadata, because plainmetadatareports the target's type andwould let a link through), and on Unix an over-broad mode is narrowed to
0600in place—
set_permissionsnever opens the file, so not a byte is read or rewritten. A modealready tighter than
0600is left alone.a22c554corrects the mask that narrowing used.1d7869fread the mode with& 0o7777and then tested it against0o177, and those two masks disagree:0o4600 & 0o177 == 0, so a regular seed file carrying setuid scored clean, the branchwas skipped, and the bit survived while the code reported the file narrowed. Its
regression could not see it either — it asserted
mode & 0o777 == 0o600, and0o4600 & 0o777 == 0o600passes. One mistake made twice: a mask narrower than the valuebeing judged. The disallowed set is now spelled
const DISALLOWED: u32 = 0o7000 | 0o177— the special bits (setuid, setgid, sticky) as well as everything below owner read+write
— so
mode & DISALLOWED != 0is exactly "not0600or tighter". Every mode assertionin the tests now masks
0o7777, never0o777. Masking the read down to0o777would have hidden the bit rather than cleared it; the check has to see what
set_permissionsis about to overwrite. No residue is left un-narrowed on Unix —0o7777is the whole mode wordset_permissionswrites, and every bit in it outside0o600is inDISALLOWED. On non-Unix targets the branch does not compile and this PRmakes no mode claim at all. Three regressions cover these two bullets, and each was run
against the body it fixes: they FAIL there and PASS here.
e0a36d5is what makes the bolded sentence above true. Ata22c554one file-modeassertion still masked
0o777—the_seed_is_owner_only_and_never_leaves_its_file, whichreads a real seed file's mode. Nothing was wrong with the assertion (that seed is created
fresh by
initin the same test, so no special bit can be present) but it was weaker thanthe sentence claimed, and a completeness sentence is only worth what its weakest member is.
Re-derived at this head over the whole file: seven file-mode assertions in
mod tests,all masking
0o7777, and zero masking0o777. The one remaining live0o777isassert_eq!(0o4600 & 0o777, 0o600, …), which is arithmetic demonstrating the old trap soit cannot be reopened quietly — not an assertion about any file's mode.
<home>/mint/mintd-config.tomlwith nomnemonickey at all — the seed reaches themint by
--seed-file. It carries the four[ln]bounds that are not optional in0.17.2 even though the shipped
example.config.tomlcomments them out (that exampledoes not parse).
listen_host = "127.0.0.1"; a bare::1is refused with a messagethat names the brackets, because cdk builds its bind address by string concatenation and
the unbracketed form aborts the process at startup.
config.toml:issuer_mint, plus the URL appended toaccepted_mintsand toextra_mints, idempotently. The third key is not in the original scope — see"one deviation" below.
maxplayer issuer status— url, issued, redeemed, outstanding, retired,last_seenand the work dir. Counters come from the mint's own sqlite opened read-only (
mode=roenforced by the handle, not intended by the caller); there is no API for them — the
management RPC's 22 methods retire nothing.
retiredis the seat's own durable countin
<home>/mint/retired.jsonl, because the mint burns a proof without recording whopresented it. A test asserts
retired <= redeemedandoutstanding == issued - redeemedwith both sides printed.
maxplayer issuer issue/retire. Retirement's mechanism was measured, notassumed — see below.
The producer.
heartbeat_for_stategains the advertisement as a requiredOption<IssuerMintAd>, for exactly the reasonadmissionis required: a caller that couldomit it would publish a seat whose silence is indistinguishable from a seat too old to
speak. Both publish sites pass it, including the terminal beat — a seat leaving the market
still owes what it issued.
The negative is the load-bearing half
Five cases yield an ABSENT tag, a beat that still publishes, and a seat that stays on
the market: no
issuer_mint; a sidecar that is down; a sqlite that will not open; aledger line that will not parse; a URL missing from
accepted_mints. Each is provedseparately.
"Down" needed a liveness check, not a file read. A killed
cdk-mintdleaves itssqlite fully readable, so a beat built from the file alone would state
outstanding = Nwith
last_seen = nowfor a mint that died a week ago.advertisementasks the mint(
GET /v1/info, 2 s) before it reads a counter.Retirement — what deliverable 0 actually proved
retiredis a protocol-required counter and nothing in this tree could burn one. Theobvious route is closed on purpose:
wallet meltat an issuer mint is refused by stage 2,and that refusal stays — retirement is not a melt on the wallet surface.
Measured 4 Sep against
cdk-mintd 0.17.2(fake wallet), captured strings in the worker'sevidence package:
18 input proofs to
state='SPENT'— 100 sat redeemed — whilesum(blind_signature.amount)stayed at 100. No new signature. Outstanding 100 → 0.with nothing paying it. The invoice was generated in-process with a fresh random key
and payment hash; the mint's
mint_quotetable held only its own, different invoice.Payment successful: state=PAID, amount=100, fee_paid=0— and the preimage isempty. The fake wallet never asks whether it issued the invoice.
So
retirebuilds a burn instrument: a bolt11 for the amount, signed by an ephemeralkey discarded on the next line, naming a payment hash nobody holds a preimage for. Nothing
can route it and nothing will settle it. It is honest for a mutual-credit issuer — the
issuer destroys its own IOU — and the mint's "payment" is decoration, as the 3 Sep
prove-out already recorded.
Two things found along the way, both noted in code:
cdk-fake-wallet 0.17.2reportstotal_spent = amount + 1unconditionally, so themint logs an "Over paid … Fee was too high" line and returns no change. The recorded
retirement is the melt quote's amount, never that inflated figure.
verify_transaction_balancedrequiresoutputs == inputs - feeexactly and rejects an empty output set. Melt is the only burn.
send/receiveadmit the seat's OWN mint, and nothing else newTwo sites, and only these two, still called the class-blind
home::mint_allowed. They nowcall
mint_class::mint_admittedwithIssuerMints::none().with_own(config.issuer_mint())— built from this seat's config alone, so only the
Ownmarker admits and acounterparty's signed declaration widens nothing.
wallet_ops.rs's melt fence is notone of them. Proved with controls: the same loopback stub, configured and reachable but
undeclared, is still
RealMintDisallowed.The live proof
crates/maxplayer-core/tests/issuer_sidecar_live.rs,#[ignore = "needs a local cdk-mintd 0.17.2"]. One seat, one sidecar, no counterparty:init → sidecar up from the written config and
--seed-file→ mint 100 →outstanding == 100→ the beat carries
["issuer_mint", url, 100, 0, ts]read off the event → retire 40 →issuedunchanged,outstanding == 60, the beat carriesretired == 40→ sidecar killed→ no tag, and the seat still advertises. Run green:
One deviation from the written scope, named rather than hidden
The wizard writes three config keys, not two.
extra_mintsis the third, and withoutit deliverables 0/2/4 cannot work:
wallet_ops::configured_mintsisaccepted_mints[0]plus
extra_mints— it does not read the rest ofaccepted_mints— so an issuer URLappended at
accepted_mints[1]never reachesopen_wallet_async, and the seat cannot opena wallet at the mint it issues from.
extra_mintsis the existing operator-visible opt-inlist
maxplayer wallet mints addalready writes. Reversible in one line if that is thewrong call.
Also worth stating:
wallet melt's issuer-specific message is unreachable for aloopback issuer mint, because the class-blind real-mint fence above it refuses every
http://URL first. The refusal stands; only its wording differs. Nothing was changedthere.
Checks, measured at
e0a36d5e129b02ef84f90af5c85dc0d28e925b04Every figure below was re-run at that exact commit with the tree clean
(
git status --porcelainempty, printed into each log as command output). None is carriedforward from the earlier head.
cargo build --lockedcargo test -p maxplayer-core --locked --offlinecargo test -p maxplayer-core --features acp --locked --offlinecargo test -p maxplayer-core --features wallet --locked --offlinecargo test -p maxplayer --locked --offlinenpm --prefix web/network testnpm --prefix web/app testtsc: command not foundBuyer-only (
cargo check -p maxplayer --no-default-features --locked --offline) is rc=0.The
web/approw cannot run here:web/app/node_modulesis absent andnpm cineeds thenetwork, which is why
checks.tomldeclares it asprepareand not as a command. Thisbranch touches zero files under
web/(see the diffstat offc67661..e0a36d5), so that row is unrun, not red.web/networkdoes run offline —it declares no npm dependencies — and it passes; the earlier revision of this body called
both npm rows unrun, which was needlessly true of that one.
The ignored live test is not in those totals by design. Run on its own against a real
cdk-mintd 0.17.2at this head:1 passed / 0 failed, printingafter issue: issued=100 redeemed=0 outstanding=100thenafter retire: issued=100 redeemed=40 outstanding=60 retired=40.The flake recurred at this head, and the red run is reported rather than replaced.
credential_proxy::tests::a_declared_over_cap_body_is_refused_before_the_upstream_sees_itfailed the first wallet sweep at
e0a36d5—credential_proxy.rs:3520,reqwesterrorConnectionReset(OS code 54) writing the body to its own loopback test server on127.0.0.1, 1340 passed / 1 failed. Run alone at the same head it passes; the identicaldeclared command rerun immediately afterwards is 1341 / 0 / 2 with exit 0. Both logs are
kept: the red one is not deleted or edited.
Measured, not assumed:
git diff --stat fc67661..e0a36d5 -- crates/maxplayer-core/src/credential_proxy.rsis empty and
git log --onelineover the same range and path lists no commits — thisbranch does not touch that file. Its earlier history on this PR is the same test failing
once at
17b4283and passing alone, on re-run, and in the full sweeps at1d7869fanda22c554. It has not been bisected to base, so "pre-existing" is an inference from thediff and the error shape, not a measurement I have made.
code change. It is not mine to fix under this brief, and I have not touched it.
⛔ No merge, no tag, no release, no push to upstream, no rebase. Bob merges.
🤖 Generated with Claude Code