Uh oh!
There was an error while loading. Please reload this page.
feat(node): scope CORS per route+method, hold the DIG loopback rule, keep the service stoppable - #291
Conversation
e35f977 to
21df7c3Compare21df7c3 to
64d354cCompareMichaelTaylor3d
commented
Aug 21, 2026
Resume-ready progress — lane pickup after the 600s watchdog
Done
Remaining
|
0ec6b2a to
a095039Compare…keep the service stoppable
Three fixes that share `dig-node-service`, plus a measurement that says the fourth ticket needs no
code.
CORS (#702) is now decided per request rather than once for the router. Local web/extension origins
keep the whole surface; desktop-app origins are reflected for content reads only. The discriminator
has to be the method, not the route: `POST /` multiplexes content reads and the open wallet-read
methods onto one JSON-RPC endpoint, and `/{method}` serves the Sage-parity wallet RPC on POST and
content on GET, so a route-keyed decision must answer both traffic classes the same way. That is
what #693 deferred. Every open wallet-read method is reached by POST and every cross-origin content
read is a GET, so the split closes the wallet-read reach with #669 left intact. Preflights are
judged against `Access-Control-Request-Method` so the preflight answer matches the real one, and a
preflight declaring no method fails closed.
The DIG loopback rule (#767) gains a single source of truth in `loopback.rs` and a build-time guard.
The ephemeral content server was still on `127.0.0.1` despite the P0 #745 fix the ticket asks to
confirm; it now takes the DIG address, falling back only where that address cannot be bound at all
and logging when it does. The guard fails the build on a new literal-loopback bind, ignoring test
fixtures and dials, and it immediately found two binds a manual sweep had missed. Three sites keep a
literal address for cross-repo dial-contract or crate-layering reasons and are enumerated in the
guard with their reasons rather than left to memory.
The SCM 1061 wedge (#2880) was neither of its two usual causes: the service does reach RUNNING and
does report a STOP-accepting mask. The stop was bridged into the serve future by
`spawn_blocking(recv)`, putting it on tokio's blocking pool — the same pool the wallet replica's
synchronous database work draws from. With the pool saturated the receiving task never ran, so an
accepted stop was never observed and the service kept serving HTTP while the SCM could not stop it.
That is also why the wedge correlated with the frozen replica: one blocked resource, two symptoms.
The stop is now a watch signal delivered by the runtime itself, and graceful shutdown is bounded, so
a body that will not wind down no longer holds the service RUNNING and a forced stop is reported as
a failed run rather than a clean one.
One defect found by CI and fixed at the call site, not by widening the check that caught it. The new
ephemeral bind first built its URL as `format!("http://{host}:{port}/...")`, which dig-node-core's
`banned_address_patterns` sweep correctly rejects: text concatenation loses the brackets every IPv6
literal needs. The URL is now formatted from the `SocketAddr` itself, whose Display brackets v6 and
leaves v4 alone, and the bind helper returns only the listener so the advertised address can no
longer disagree with the bound one.
The trusted-Chia-peer surface (#2870) is already shipped and no code was added: `control.chiaPeers.*`
(PR #248) and `dign chia-peers` (PR #45) already wire the user-facing surface to the existing
`user_managed` writer, and the help text already names the custody grant. No env var or config key
was added on purpose — that would be a second configuration path into write authority.
Closes #767
Closes #702
Closes #2880
Closes #2870
Co-Authored-By: Claude <noreply@anthropic.com>a095039 to
a078386CompareMichaelTaylor3d
commented
Aug 21, 2026
Gate-ready — head |
MichaelTaylor3d
commented
Aug 21, 2026
loop-security: pre-merge audit IN PROGRESSHead audited: Diff read in full (11 files, +1258/-53). Areas under audit:
Findings posted as they are formed. Verdict posted before my final summary. |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Correctness GATE: PASS
Head reviewed: a0783868c0984ea7ef59cd70f22d610a8b0fb3cf (resolved from gh pr view 291 --json headRefOid). Combined delta gated against main (8f2f416), not the last commit, because the branch had more than one author.
Required checks asserted BY NAME at this head — Lint commit messages, Check version increment, Rustfmt, Clippy, Test + coverage: all SUCCESS. unresolvedReviewThreads=0, mergeStateStatus=CLEAN, blocked solely by draft=true (correct — the gate had not returned).
1. CORS (#702) — attacked the ALLOW list, no widening found
reflects_origin is is_local_origin(o) || (is_app_origin(o) && is_content_read(m, p)) (crates/dig-node-service/src/server.rs:349-354). Against the prior is_local_origin(o) || is_app_origin(o), that is a strict subset for every (origin, method, path) triple. Specifically:
- No state-changing method reachable from a browser origin that could not reach it before. App origins lost POST reach; the local/extension family is unchanged.
allow_methods,allow_headers,expose_headers,allow_private_network(true)are all untouched by the diff.- No
allow_credentialsanywhere in the crate — grepped; the layer never sets it, so the reflected-origin + credentials combination does not arise. - The origin is matched against fixed sets (
is_local_origin/is_app_origin+ the operatorAPP_ORIGINS_ENVallowlist, both unmodified) and only then echoed — never blind-reflected. - Preflight judged on
Access-Control-Request-Method(server.rs:377-389) with an absent declared method falling through toOPTIONS, i.e. closed for the app family. Preflight answer and real answer agree. is_websocket_pathmatches the exact registered paths, not a prefix (server.rs:367-369), so a future/ws-fooroute is not swept into the carve-out.
Authorization derives from the request's own Parts (method, path) plus the request's Origin header — no process-global or ambient flag participates in the decision.
Revert-proofs discriminate placement, not outcome. The two nearest wrong implementations fire different assertions: router-wide fails tests/server.rs:433 (POST /), route-only fails tests/server.rs:440 (POST /{method}), and the GET/POST pair on the one path /get_sync_status is what separates them. The chrome-extension:// control at tests/server.rs:466 fails if a fix tightened the method for every origin. This is a property test, not an outcome test.
2. Loopback rule (#767)
loopback.rs is a bind-side allocation, not an authorization predicate, so the "recognises 127.0.0.1 but not ::1" bypass class does not apply here — nothing in this diff gates trust on loopback-ness. The §5.2 tension is addressed explicitly rather than skipped (loopback.rs:33-41, SPEC.md §4.0a): ::1 is a single address so a per-service v6 allocation is not expressible, and v6 is a second listener rather than the DIG-owned address. No hostname is treated as equivalent to a loopback socket; the guard flags binds only and deliberately leaves the §5.3 dialling tiers (localhost) alone, with that non-flagging proven by a control at tests/loopback_bind_guard.rs:170-181.
open.rs:293-303 reads the address back off the bound listener and formats from the SocketAddr, so the advertised URL cannot disagree with the bind and cannot drop IPv6 brackets. Spot-checked the sweep claim: git grep 'format!("http://{<ident>}:{' over crates/**/*.rs at this head returns zero hits, and crates/dig-node-core/tests/banned_address_patterns.rs is untouched by the diff.
3. Stoppability (#2880) — reachable, not harness-only, with the vacuity stated
The primitive is exercised under genuine saturation (max_blocking_threads(1) + one parked task, service_control.rs:172-200), which is the only fixture that can fail against the old spawn_blocking(recv) bridge — semantics did not change, only the executor, so a non-starving fixture would have passed both ways.
Reachability: win_service.rs:76-160 is the shipped run-service path and is wired to the same primitive; it is not a test double. Deadlock: raising a stop is watch::Sender::send (service_control.rs:74-76) — takes no lock a handler holds and no blocking thread. Stop-during-startup is handled, and handled for the right reason: StopWaiter::wait checks *rx.borrow() before changed() (service_control.rs:106-112), because changed() only reports post-subscription changes — a bare changed() would wedge on exactly the stop an update tool sends first.
The honest gap, already disclosed in the PR body and not charged against it: run_service itself is a cfg(windows) bin-adjacent path with no test, so the wiring is reasoned, not reproduced on a wedged host. Under §2.6 that is the right call for an MVP batch, and the primitive it wires is proven.
4. Trusted chia peers (#2870) — verified already-shipped, and correctly so
control.rs is not in this diff. On main: dispatch at control.rs:911-913, declared at :208-210. Trust comes only from an explicit operator act — no peer can assert its own trust, and there is no second writer. The master-token gate is delegated to ControlMethod::requires_master_token in dig-node-control-interface rather than restated as string literals (control.rs:294-313), with a lockstep test; an unrecognised control.* name fails closed to master-only. §908 holds — nothing here signs. Declining to add $DIG_CHIA_TRUSTED_PEER is the right call and the reasoning is recorded: an env var is a second configuration path into a custody grant.
5. Cross-ticket bleed — none found
Each ticket's change is present and independent: #702 touches only the CORS predicate, #767 only bind sites and a new module, #2880 only the stop path, #2870 nothing. No change relaxes a check another relies on. dig-node-core has zero files in the diff, which confirms the an_eviction_advertises_after_the_victim_is_gone flake is genuinely pre-existing — it arrived on main in 1db4d6e (#280) and is untouched here. Not charged.
6. §2.5 / rustdoc
New public items (loopback::*, service_control::*) all carry doc comments that state the WHY, not the WHAT, and both modules are declared with doc comments in lib.rs:60-63 and lib.rs:92-95. Reads cleanly.
Two non-gating observations are posted inline and resolved by me so they cannot block merge. Neither changes the verdict.
Verdict: PASS. Not undrafted, not merged, nothing edited.
MichaelTaylor3d
commented
Aug 21, 2026
Orchestrator note for the gate round — four ticket premises were WRONG, and the implementer said soRecording this on the PR so both gate legs read it and so it survives the lane that found it. Three #767 — the fix it asks you to "confirm" was already GONE, and a test was holding it that way
This is why the new Gate should check: the guard strips #702 — its stated blocker did not existThe ticket claimed the router needed restructuring. It did not: #2880 — NEITHER of the two causes the ticket names was the causeThe service does reach RUNNING with a STOP mask ( That also answers scope 4: the replica freeze and the service wedge are the same blocked resource, #2870 — no code was needed; it had already shipped
The revert-proof set is stronger than usual — one entry is worth copyingFive mutations, each read for which assertion fired, not merely that something failed. The pair
Two mutations in opposite directions firing two different assertions is what proves the fixture Two follow-ups the gate should NOT charge against this PR
Both are tracked together, since they are one problem: the loopback literal has no home low enough for Also confirmed, so the gate need not redo itNothing was hidden behind the |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Two non-gating notes, resolved by me. Verdict is in the separate PASS review.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
MichaelTaylor3d
commented
Aug 21, 2026
SEC-1 (GATING, MEDIUM) — the forced-stop branch tells the SCM |
MichaelTaylor3d
commented
Aug 21, 2026
SEC-2 (LOW, defense-in-depth — do NOT gate) — a GET-approved preflight still seeds the browser's CORS-preflight cache with |
MichaelTaylor3d
commented
Aug 21, 2026
loop-security VERDICT: CHANGES-REQUIREDHead audited: Exactly one finding gates: SEC-1. Everything else is clean or explicitly non-gating.
I did not soften anything on the assumption the correctness gate covers it, and I did not coordinate with it. SEC-3 (informational, no action required)
This is not a finding and needs no follow-up, because it grants strictly LESS than what already exists: every capsule served from Per ticket: does it introduce a security regression?#767 - hold the DIG loopback rule: NO
I separately confirmed the pre-existing loopback authz boundary is sound and family-complete, and that this diff does not touch it: the decision is taken from the socket peer address via Reachability of Guard vacuity: NOT vacuous. #702 - scope CORS per route+method: NOThe change is STRICTLY NARROWING on every input:
#2880 - keep the service stoppable: YES, see SEC-1SEC-1 is the only defect. Everything else here is clean:
#2870 - trusted chia peers: NO, because there is no code for it in this deltaVerified: zero chia or trusted-peer files in the diff, and the only occurrences of But this PR carries
This is a bounded read of the trust ORIGIN, not a full audit of that subsystem - I did not examine what a trusted peer claims are then permitted to do. If you want the "what does trust buy" half audited, that is a separate ticket against already-shipped code and should not block this PR. What must change to flip this to PASSSEC-1 only. In the SEC-2 and SEC-3 do NOT need to change for my PASS. SEC-2 is worth the one-line I ran no mutating git command in any shared checkout. The only build I performed was a standalone |
…e drop SEC-1 (gating): the forced-stop branch reported `Stopped` to the SCM and then returned, dropping the tokio runtime. `Runtime::drop` joins the blocking pool with no timeout, and a forced stop is by definition the case where a blocking task never finished, so the drop blocked forever AFTER the SCM had been told the service was stopped. Measured on the pinned tokio 1.53.0: still blocked after 8.03s, then 50.19ms once the wedged closure was released. That is a privileged action reporting success without taking effect: `sc stop` succeeds, the service is marked stopped, and the process stays alive holding its binary image locked - the exact symptom dig_ecosystem#2880 exists to remove. It also leaves an SCM-stopped-but-alive state from which a `StartService` yields a second process against the same wallet replica. A forced stop now abandons the pool (`shutdown_background`) and exits. The graceful branch, the watch-based stop signal and the 20s deadline are unchanged. SEC-2 (low): `Access-Control-Allow-Methods` now mirrors the requested method. tower-http emits it on every answered preflight independent of the origin verdict, so a static [GET, POST, OPTIONS] answered an approved app-origin GET preflight by also advertising POST, seeding the browser preflight cache with a POST entry a later `POST /` could use to skip its preflight. SEC-3: narrow two overclaiming sentences. The sandboxed-blob comment claimed origin isolation from "anything else"; cookies ignore port, so the blob shares the 127.0.0.2 cookie jar with the bare-IP content surface. The SPEC's app-origin scoping sentence is true of Access-Control-Allow-Origin, not of the whole response. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
commented
Aug 21, 2026
Security findings addressed — |
MichaelTaylor3d
commented
Aug 21, 2026
Orchestrator verification of the SEC-1/SEC-2 delta — |
Uh oh!
There was an error while loading. Please reload this page.
MVP batch for dig-node. One branch and one PR because all four tickets touch
dig-node-service, and #767 reaches intodig-wallet— four branches would conflict on the same files.Closes #767
Closes #702
Closes #2880
Closes #2870
Version: 0.134.0 → 0.135.0 (minor — new capability, no removed or renamed public API).
Blast radius checked
gitnexusis not indexed in this worktree and a freshanalyzewas not run, so blast radius was established by call-graph grep + direct read, and this PR says so rather than implying a tool it did not use (§2.0 bound 2: falling back is permitted, concealing it is not).is_allowed_origin→reflects_originCorsLayerpredicate inserver.rs::router+ one unit testRealLocalServer::serve_until_fetchedopen.rsonly (traitLocalContentServer); the URL is generated and handed to the browserwin_service.rs::run_servicerun-servicesubcommand onlyserve_with_shutdowndign chia-peersandcontrol.chiaPeers.*were read, not modified (see #2870 below).No HIGH/CRITICAL-risk edit in this diff. Nothing here touches signing, key handling, or a spend path. The one security-relevant change narrows an existing surface (#702).
1. #767 — DIG loopback rule
New:
crates/dig-node-service/src/loopback.rs— one place that answers which loopback address a DIG service binds.127.0.0.1is reserved for the rest of the machine;127.0.0.2is dig-node (dig.local),127.0.0.5is dig-dns.Fixed — and this one was a live regression. The ticket asks to confirm the P0 #745 ephemeral content server is on
127.0.0.2. It was not:open.rsbound("127.0.0.1", 0)and advertisedhttp://127.0.0.1:<port>. Now it walksloopback::ephemeral_bind_candidates()— DIG address first,127.0.0.1only where the DIG address cannot be bound at all (macOS without anlo0alias), and a fall-back is logged, because a silent one is indistinguishable from the rule not being applied.Made mechanical:
tests/loopback_bind_guard.rsfails the build on a new literal-loopback bind in product source. It strips#[cfg(test)]modules by brace matching (so an ephemeral test fixture on127.0.0.1:0is correctly ignored) and flags bind calls only — dials are untouched, becauselocalhostis a canonical §5.3 dialling tier and a guard that flagged it would assert something the ecosystem does not believe.The guard immediately earned itself: it found two binds a manual sweep had missed, at
crates/dig-wallet/src/sage/transport.rs:276-277.What is NOT moved, and why — stated plainly rather than scoped away silently
Three bind sites keep a literal loopback address. All three are recorded in the guard's own
DECLARED_EXCEPTIONSwith their reason, so the coverage gap lives next to the guard instead of in someone's memory.dig-node-service/src/wallet_mtls.rs:106—127.0.0.1:9776, the Sage-parity wallet mTLS listener. The address is a dial contract; moving it without the consumers is the half-migration §1.3b forbids. It is already off Sage's own9257(v0.128.0), so the collision that caused the incident is closed.dig-wallet/src/sage/transport.rs:276-277—serve_dual's mTLS + HTTP-mirror binds. Not fixed for a structural reason:dig-walletsits belowdig-node-service, so it cannot depend on the SSOT, and duplicating the constant would create exactly the rival implementation the allocation exists to prevent. Mitigating fact:serve_dualhas no production call site (dig-node serves that surface viawallet_mtls.rs), so no shipped listener is on those lines.config.rs:299→server.rs:2044,127.0.0.1:9778) — a cross-repo dial contract for dig-app, the extension anddign, and canonical docs(spec): correct the dig-node-core P2P dependency list to match Cargo.toml #132. Its address is computed, so the text-scanning guard cannot see it either; that reach floor is documented in both the module and the guard.The follow-up this implies: the allocation's correct home is a foundation-level crate (
dig-constants, L00) that every consumer can reference downward. That is release-first and cross-repo, so it is not this PR's, and #767 isstatus:needs-useron the ratified table regardless.2. #702 — route/method-scoped CORS
CORS is now decided per request, not once for the router.
GET/HEAD, and not/wsor/ws/status.Why the discriminator is the method. A route-only split cannot express this policy, which is exactly why #693 deferred it:
POST /multiplexes content reads and the open wallet-read methods onto one JSON-RPC endpoint, and/{method}serves the Sage-parity wallet RPC onPOSTand content onGET. Every open wallet-read method is reached byPOST; every cross-origin content read a browser client makes (dig-urn-resolver's node-first tier, the reason #669 widened the set) is aGETcarrying theX-Dig-*headers. So the split removes the wallet-read reach and leaves #669 intact.Preflights are judged against
Access-Control-Request-Method, not againstOPTIONS, so the preflight answer matches the answer the real request gets. A preflight declaring no method fails closed for the app family.SPEC.md §4.3rewritten — the paragraph documenting this exposure as accepted-and-unenforced would otherwise have described behaviour that no longer exists.3. #2880 — the SCM 1061 wedge
Which of the two causes it was: NEITHER, and the ticket asks, so here it is explicitly.
1061normally means the service never reached RUNNING or never reported its accepted-controls mask. Measured against this code,win_service.rsreportsRunningwithServiceControlAccept::STOPbefore it serves, and the SCM caches that.The defect is one layer in — in how the accepted control was acted on. The stop was bridged into the serve future by
spawn_blocking(move || shutdown_rx.recv()), putting the stop path on tokio's blocking pool, which the wallet replica's synchronous database work also draws from. With the pool saturated the receiving task never ran, so the accepted stop was recorded and never observed: the service kept reportingRunning, kept serving HTTP, and never stopped.That also answers the ticket's fourth scope item. The correlation with the frozen replica (
watched_addresses: null, staticpeak_height) is not a coincidence — a saturated blocking pool is the shared blocked resource, and it produces both symptoms. Fixing the stop path does not unfreeze the replica, but it does mean a frozen replica can no longer make the node unstoppable.Two changes in
service_control.rs:tokio::sync::watchsignal — delivered by the async runtime, needing no blocking thread. Raising it cannot block and cannot fail, so the control handler always answers the SCM promptly.Running; the service reportsStoppedanyway and reports that run as failed, because a forced stop reported as clean is the same class of lie as the updater'sDeferredbehind exit code0.SPEC.md §9.0records the guarantee normatively.Not in this PR: scope items 2 and 3 (the updater must not report a repeated
Deferredas a clean pass; whether it should escalate to a kill after N deferrals) are dig-updater changes, a different repo. Flagged for a sibling ticket rather than silently dropped.4. #2870 — trusted Chia peer: ALREADY SHIPPED, no code in this PR
The already-shipped check (§2.0) says build nothing. The whole deliverable the corrected ticket asks for — "wire the surface to the writer that already exists" — is on
main:control.chiaPeers.add/.list/.remove—crates/dig-node-service/src/control.rs:911-913, dispatched; declared at:208-210, master-token gated at:263-265. Shipped by PR fix(control)!: gate chiaPeers on the master-token tier and adopt dnci 0.18 #248 (2e73fd0).dign chia-peers add|list|remove— the clap subcommand atcrates/dig-node-service/src/entrypoint.rs:181-184, sub-actions at:466-485, mapped at:775-779; rendering atcontrol_cli.rs:490-518. CLI plane shipped by PR feat(cli): control-parity subcommands + peer management (#426 #559) #45 (ed0e10a).user_managedrow viadig-wallet'sadd_peer— no second writer, exactly as the ticket requires.One deliberate non-action. §5.3's idiom is CLI flag +
$ENV+ stored config, and there is no$DIG_CHIA_TRUSTED_PEERenv var or config key. That is not an oversight: this row is a custody grant, the ticket says "Do NOT build a second writer", and an env var is a second configuration path into write authority — one that a stray value in a service environment could exercise with no user act. Recording the reasoning rather than the omission.Evidence
Toolchain
rustc 1.98.0— the CI version, so-D warningswas evaluated against the same compiler.cargo clippy --workspace --all-targets --all-features -- -D warningscargo check -p dig-node-service --all-targets--lib service_control:: loopback::--test loopback_bind_guard--test server -- cors--lib open::tests::real_local_servercargo test --workspaceis not locally runnable (adig-node-coretest exceeds 60s); CI covers the workspace.Revert-proofs — four mutations, and which assertion fired
Each fix was reverted at the site the fix lives, on a committed tree, backed up by file copy (never
git checkout). The assertion that fired was read in every case, not just the pass/fail.1. #2880 — stop wait back onto the blocking pool (
StopWaiter::wait)Exactly the intended assertion, and the other four stayed green — so the fixture isolates the delivery mechanism rather than breaking the module.
2. #702 — predicate back to router-wide
3. #702 variant B — a ROUTE-only split (the nearest wrong implementation, and the trap "a read-only reflection on a route that also accepts a mutating method is not scoped")
A different assertion fired (
:440, not:433). That is the point of running both: a fixture that failed identically under either mutation would be pinning "the app origin is denied somewhere" instead of the placement. TheGET/POSTpair on one path is what separates them.4a. #767 — ephemeral bind back to
127.0.0.1DIG loopback available: trueis load-bearing: it proves the host-derivation took the strict branch, so the derived expectation was not quietly satisfied by the macOS fall-back allowance.4b. #767 — the guard itself. A new literal-loopback bind was injected into
peer_ping.rs(a file with no prior violation):The guard also carries its own two vacuity tests: every banned pattern is proven capable of matching (a ban entry that matches nothing bans nothing), the dial/classifier controls are proven not flagged, and the
#[cfg(test)]stripping is proven to drop the fixture bind while keeping the product one.All four reverts restored by copy;
git statusandgit diff --statboth clean afterwards.Not finished / follow-ups
status:needs-useron the ratified allocation table, and its checklist is one child per repo. dig-node's row is done except the three declared exceptions above; thedig-constants(L00) rehoming that would letdig-walletand every other consumer share the SSOT is a release-first cross-repo change.Deferredmust not exit0; escalation after N1061s). Not attempted here.1061fix is reasoned from the code and the reported symptoms, not reproduced on a wedged host. The mechanism fits every observation and the guard test fails against the old code, but an SCM wedge cannot be reproduced in a unit test — so the attribution is strong, not proven.cache_lock_is_exclusive_then_releaseduntouched (eviction tests take the cross-process cache lock outsideENV_GUARD).One more limitation in the guard, found while writing it
The scanner matches per line, so a bind call that
cargo fmtsplits across lines would evade it — theTcpListener::bindtoken and the"127.0.0.1"literal would land on different lines and neither alone trips the check. Recording it rather than leaving it to be discovered: when you find that a thing you meant to guard is invisible to your guard, that is a coverage finding, not a footnote.Why it is not fixed here: in practice
bind(("127.0.0.1", port))is far too short forfmtto split, so the evasion needs a contrived call site, and hardening the scanner into a multi-line matcher is a change that needs its own revert-proof and another CI cycle. Flagged for the gate to redirect if it disagrees.Related and already handled:
fmtwas applied to this branch and then all four fix tests plus the guard were re-run, precisely because formatting can move a bind onto a new line and silently blind the check. No bind in the workspace is currently split.CI — GREEN at
a0783868c0984ea7ef59cd70f22d610a8b0fb3cfRebased onto
origin/main(8f2f416, the v0.134.0 release commit) so the branch is not stale understrict=true; the0.134.0 -> 0.135.0bump was re-verified as surviving the rebase in bothCargo.tomlandCargo.lock. One commit, 11 files.check-merge-preconditions.shasserts the required set BY NAME (not from the rollup):The script reports
BLOCKEDsolely becausedraft=true, which is deliberate — the gate round has not returned, and a ready green PR is a merge invitation to a sibling lane.The one CI red, and how it was cleared
Test + coveragefailed on the previous head withdig-node-core::banned_address_patterns :: no_source_file_builds_a_socket_address_from_concatenated_text, TRY 1/2/3 — deterministic, not flaky. The finding was genuine and mine.Two things were checked before touching anything, because a sweep that cannot fail for the right reason is worse than no sweep:
rust_sources(crates_root())andstd::fs::read_to_strings real files, and it carries its own anti-vacuity floors: aMINIMUM_FILES_SCANNEDcount and adig-node-service/-sibling-file floor, so acrates_root()that collapsed to one crate fails loudly rather than passing clean. It also shipsthe_scanner_flags_the_banned_construct_in_every_spelling, proving its matcher can fire.KNOWN_VIOLATIONSentry. The ban is right:format!("http://{host}:{port}/...")loses the brackets every IPv6 literal needs. The URL is now built from theSocketAddr— whose Display brackets v6 and leaves v4 alone — and the bind helper returns only the listener, so the advertised address is read back off what was actually bound and cannot disagree with it.Revert-proof for that fix too. Restoring
format!("http://{host}:{port}/{filename}")fires the same assertion, naming the exact line:Nothing was hidden behind the fail-fast bail
CI's run stopped at 882/2154 tests, so the whole workspace was re-run locally with CI's exact runner and
--no-fail-fast:Clean. The 3 flaky ones are pre-existing and unrelated to this diff —
the_chain_reads_are_open_reachable_and_degrade_honestly,the_push_and_the_arrival_cursor_are_gated_while_the_chain_reads_are_open,wallet_rpc_answers_core_reads_on_loopback, all indig-node-service::server, all from the process-global env serialization that file documents. Each passed on retry, and CI runs--retries 2as well.Note for whoever re-runs commitlint: the first failure was the PR title at 109 chars (the commit subject is 95), and re-running the failed run replays the frozen event payload with the old title. The green run is the one the title edit created, which the re-run had cancelled.