fix(cli): the CLI/service/updater batch — six user-visible dead ends - #458
Conversation
…inary is the one running `systemctl enable --now` is a no-op on a unit that is already enabled and running, so a `dpkg`/`apt` upgrade replaced /usr/bin/dig-node and left the OLD process serving. The failure was silent in both directions an operator checks: `dig-node --version` reads the on-disk image and reports the new version at once, and `systemctl is-active` reports active because the old process is genuinely healthy. Only MainPID moved. A security fix shipped through the .deb therefore did not take effect on upgrade. postinst now runs `systemctl try-restart` when dpkg passes a previously-configured version ($2), i.e. on an upgrade only. `try-restart` rather than `restart` so a node the operator deliberately stopped -- including one held back by the #317 no-autostart marker -- stays stopped. macOS needs no change: its postinstall already does bootout + bootstrap + kickstart -k. Refs #305 Co-Authored-By: Claude <noreply@anthropic.com>
…to upgrade Two user-visible lies on the CLI's network surface. `network-info` was documented and expected but never existed as a subcommand, so it presented as empty output on three healthy fleet boxes -- what an unrecognised subcommand looks like once a shell has swallowed the usage text. The node has always ANSWERED the question: `dig.getNetworkInfo` serves peer id, network + genesis, advertised candidates (IPv6-first, §5.2), reachability and the relay reservation. Only the verb was missing, so this adds the verb rather than the data. It reads through a new token-free `control_client::call_open`. Every field it prints is already published to any peer that dials this node, so the control token would buy no confidentiality while costing real availability: on a .deb install the master token is 0600 root:root (#501), and an ordinary user asking "what is my node's address" would be told to elevate for a read the network performs for free. `call_control` is untouched and still carries the token. `peers` conflated two different empty states behind one message: a node with zero peers was told "a per-peer list needs a newer node" -- on 0.138.0, the newest build that existed. The advice was false and pointed at a release that does not exist, and it appears on first run, when a user has zero peers and is least able to tell. The connected count distinguishes them: zero connected is stated plainly with something to try; peers counted but not enumerable keeps the version note, which is the only state that actually implies one. Refs #303, #304 Co-Authored-By: Claude <noreply@anthropic.com>
…lure `dign updater check-now` probing inside the restart window reported IO_ERROR, which reads to an operator exactly like an update that broke the node. It is the opposite: a successful pass installs new bytes and cycles the service, so the restart is the normal END of the thing that succeeded. Same class as dig-updater#77, on the surface #77's fix does not reach. IO_ERROR says an I/O operation was attempted and failed -- a claim about the request. An unreachable node is a failure to MEASURE, not a measured failure, and sharing one encoding is what trains an operator to ignore the surface, which is how the one real failure gets ignored too. That matters more under the silent-staged-install policy (dig_ecosystem#3180), where the status surface is all an operator has. So the loopback control client's ConnectionRefused class now resolves to a new exit code 7 NODE_UNREACHABLE, catalogued in cli.rs, README, SPEC and USER_JOURNEY. Its message is restated at the one choke point in control_cli::run: `control.updater.*` gets the restart explanation because it has a specific expected cause, everything else gets the general statement. Only the message changes and only for that kind -- a node that ANSWERED and declined has measured something, and its own words pass through untouched. No timeout was widened: that converts a wrong answer into a slower wrong answer. Refs #407 Co-Authored-By: Claude <noreply@anthropic.com>
#403 -- the dead end was the ADVICE as much as the permission. The unreadable-token remedy told the operator to "reinstall the current dig-node so the service grants your account read access". On Windows that is true: the installer keeps an explicit read grant for the interactive install-user. On Ubuntu it is false -- the .deb leaves /var/lib/dig-node at 0700 root:root and the token at 0600, so reinstalling changes nothing and the operator loops. The decision, recorded in full on the ticket: do NOT widen the token. It is the MASTER capability -- it authorizes pairing administration (mint/list/revoke) and chiaPeers.add, which grants chain authority over the wallet replica. A `dig` group at 0640 hands that to every member permanently and outlives the app that motivated it. The node already has the right primitive for a client that cannot read a file: pairing, built for exactly this and used today by the MV3 extension. It yields a scoped, revocable per-client token that cannot mint or revoke pairings and cannot grant chain authority. So the Unix remedy now names `sudo dign pair` and its revocation. The platform is a function ARGUMENT rather than a cfg! branch, so both sentences are asserted on any host -- a cfg-gated assertion is untested on exactly the platform this defect was found on. #392 (the logging half) -- the file-sink degrade was RECORDED on control.status but never ANNOUNCED. A non-admin run denied C:\ProgramData\DigNetwork\logs came up console-only in silence, so an empty log directory could not be told apart from a quiet node. The console layer is installed on that path, which is why the warning reaches someone. Refs #403, #392 Co-Authored-By: Claude <noreply@anthropic.com>
Minor. New capability: the `network-info` subcommand. The exit-code catalogue gains 7 NODE_UNREACHABLE, which is additive to the table but does CHANGE the code a script sees when the node is unreachable (was 6 IO_ERROR); called out here and in the PR body so the gate can weigh it. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
|
Gate — IN PROGRESS, not the verdict. Head read: Cleared so far — the
Still open in this pass: the |
|
Gate — IN PROGRESS, not the verdict (head GATING —
GATING — NON-GATING — the CLEARED — #305, in the package. One correction to the PR body's evidence claim: |
|
Gate — IN PROGRESS, not the verdict (head The decision itself: I agree, and would not widen the token. The reasoning holds on inspection rather than on assertion — GATING — but the new Unix remedy names a command that does not do what the sentence says it does.
It does not. The test cannot catch this and should be strengthened in the same change: Two things the fix must NOT do: do not delete the pairing route (it is the correct answer), and do not restore the reinstall clause on Unix (it is the false one). The whole change is the missing word While there: the sentence also under-describes the flow. Approval presupposes a pending request, which the client raises through the open Still running: the |
|
Gate — IN PROGRESS, not the verdict (head GATING — the logging change has NO test, and the evidence line counts pre-existing ones as if it did. The PR body reports The cheap fix is to make the announcement testable rather than to test NON-GATING — that warning's text is mangled and ships to operators that way.
CLEARED — #304 distinguishes all three states, and by cause rather than symptom. |
de237fc to
672f0a8
Compare
|
Gate — IN PROGRESS, not the verdict (head GATING — exit code 7 is already taken, by the sibling CLI that exists to share this numbering.
and The PR body's compatibility analysis reads "additive to the catalogue" and considers only consumers matching on 6. That framing missed the collision, because the risk was never only "who matches 6" — it is "who else owns this number space". I searched for the 6-matchers as asked and found none: no script in this repo, nothing in dig-installer, and dig-app references This is not fatal to the design — the two meanings are nearly the same event (the engine is not answering), which is why it is worth resolving deliberately rather than by coincidence. The fix is a decision, and it belongs in this PR because a released 0.190.0 makes the number immutable:
Whichever is chosen, the shared numbering needs a home so this cannot recur — the CLEARED on the mechanism itself. |
|
Gate — IN PROGRESS, not the verdict (head Baseline suite reproduced. GATING (cheap) — "gated in CI" is not true as written. The problem is that the job does not gate anything. Branch protection's required contexts on "Release-script tests" is not among them. So the only executing proof of #305 anywhere — locally it exits 0 after printing Two acceptable outcomes, and this PR should pick one rather than leave the sentence standing: add Independent revert spot-check running now. |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CHANGES-REQUIRED
Head read: de237fc24aa67e3c5a0ed207600f6ce45b74edc5 (resolved from the remote; it did not move during this review). Reviewed in a private worktree — the primary checkout and the seven live sibling worktrees were not touched. .gitnexus is ~300 commits stale and registered against the primary checkout, so a query there would return a false-safe zero; this was done with grep + direct read, and where I could not reach something I say so at the end.
GitHub rejects a verdict-bearing review event from the PR author's identity with 422, so this is recorded as a comment review. The six inline threads are what bar the merge, via required_conversation_resolution.
The batch is largely right, and the parts that matter most are right
I want to be plain about this, because five of the six findings are about the edges rather than the core:
call_opendoes not widen anything.server.rsis not in the diff, and every plane's gate —control.*at:1134, wallet at:1212,cache.*at:1267,chat.*at:1298, WS parity at:1556— keys on the method name server-side, never on whether a token was presented. Acall_opencall naming a gated method gets-32030. No previously-gated read became reachable.grep -rn call_open crates/returns exactly three hits: the definition, the import, and one call site with a literal method name (network_info.rs:29). Nothing silently adopted it.- The claim that the node already served it holds on the loopback path specifically, not merely on the peer channel:
seams/dig_rpc/dispatch.rs:280returnsnode.network_info()through the samehandle_rpc_asfall-through the HTTP handler reaches atserver.rs:1335, after all four gates decline it. - The renderer's fixtures are faithful, not self-consistent inventions.
lib.rs:4082-4095emitspeer_id,network_id,genesis,listen_addr,candidate_addresses,reachability,relay— all seven read byformat_network_info. This was the likeliest false green in the batch and it is not one. - #304 distinguishes all three states by cause, and the tests are a real pair:
peers.rs:713and the pre-existing version-gap test vary only the connected count against the same shape, so an unconditional reword fails one of them. - #305 is scoped correctly in the package.
build-deb.sh:145guards on[ -n "${2:-}" ], which dpkg supplies only on upgrade, so a first install cannot fight the installer.try-restartoverrestartis the right primitive anddeb-contents.test.sh:180asserts the semantics rather than the spelling. The first-install control at:189is a genuine control. - I agree with the #403 decision.
requires_master_tokenis the tier the server actually enforces (server.rs:1218-1230) and it does cover pairing administration andchiaPeers.add/.remove, so the token really is the master capability. Adiggroup at0640is a permanent, unrevocable, transitive grant of chain authority over the wallet replica, bought for one app's convenience and outliving it. Pairing is the right primitive: scoped, per-client, revocable. Making the platform a real argument rather than acfg!branch is right too, and the Windows clause is a truthful control rather than a restatement.
Revert-proof, verified independently. Baseline: cargo test -p dig-node-service --lib = 657 ran, 657 passed, 0 filtered out (count checked, not exit status). With two mutations applied by file copy — if connected == 0 forced to if false, and the ConnectionRefused => ExitCode::NodeUnreachable arm deleted — 657 ran, 654 passed, 3 failed, and each failed for its own reason: an_unreachable_node_is_distinguished_from_a_measured_io_failure (left: IoError, right: NodeUnreachable), plus both peers tests printing the restored version-gap sentence. Files restored; git status --porcelain empty.
The findings, ranked
control.rs:182— the new Unix remedy names a command that does not do what the sentence says.sudo dign pairlists; approving isdign pair approve <id>. An operator following it is stuck at the same wall, one sentence further along. The test at:213asserts onlycontains("dign pair"), so it cannot tell the right string from this one.cli.rs:47— exit code 7 is already taken bydiga, the sibling CLI whose own doc-comment says it shares this numbering.dig-app .../gateway/outcome.rs:28holds 7 =NOT_CONNECTED(and 8-11). One number, two symbols, under a SPEC clause forbidding renumbering, about to be frozen by a release. The 6-matchers the body worried about are genuinely clean — I searched this repo, dig-installer and dig-app and found none.SPEC.md:2626— the mapping sentence is false in the commit that added the row above it. "anything else -> 6" whileConnectionRefused -> 7. Also silently wrong aboutInvalidInput -> 2already; sweep both.SPEC.md:2404—network-inforeaches no doc at all, including the normative subcommand list, and §8.6 currently asserts the opposite token property for the parity set. No test can see this drift.logging.rs:92— the new behaviour has zero tests (all five in the file predate this PR and exercisehealth(...)), and its message ships with two runs of ~22 literal spaces from a lost continuation.deb-contents.test.sh— "gated in CI" is not true. It does run for real onubuntu-latest, butRelease-script testsis not a required context, so #305's only executing proof cannot block a merge. Plus one evidence claim to correct:try-restartunder the #317 marker is reasoned, not tested — there is no marker-plus-upgrade fixture.
Non-gating, and I am not opening threads for these
- The
Closeskeywords are inside inline code spans (`Closes #303`). GitHub does not resolve issue references inside code spans, so on merge these four very likely do not autoclose — the exact manual-close gap the two-queue split exists to remove. Unbacktick them. The intent is verified correct: no closing keyword anywhere in the body or in any of the six commit messages touches #403 or #392, so neither can close by accident. TheClosesset is exactly #303, #304, #305, #407. - The
0.189.0 -> 0.190.0minor bump is right for a new CLI capability.
What I could not reach
- Windows service upgrade (#305's third platform) — not checked, as the lane says. It runs through dig-updater's install path and nothing in this diff touches it.
- #392's primary finding is genuinely unfixed and correctly left open; proving it needs a real out-of-service run under
DIG_WALLET_PORT/LOCALAPPDATA. - A live
dign network-infoagainst a running node. I verified the dispatch path and the field names statically; nobody has yet watched the verb print on a real machine, which is the §2.6 bar. Worth one run before release rather than before merge.
…degrade Six gate findings on #458, each a claim the branch made that its own tests could not see. The Unix token remedy named `sudo dign pair` as the approving verb. It is not: `entrypoint.rs` maps a bare `pair` to `PairAction::List`, so the sentence added to break a reinstall loop was itself a dead end one step further along. Its test asserted `contains("dign pair")`, which the wrong string satisfies as its own substring. The remedy now names `sudo dign pair approve <pairing_id>`, the test asserts that command in full plus a one-sided row against the false attribution, and a new parser test pins the bare/approve mapping so the phrase is measured against the parser rather than against itself. `NODE_UNREACHABLE` took exit 7 by reading this CLI's own table, where 7 was genuinely next. dig-app's `diga` holds 7 = NOT_CONNECTED and says in its own doc that it shares dig-node's numbering; 8-11 are taken too. This is the reasoning that already cost a yank in the JSON-RPC error space. Renumbered to 12, the first number free ecosystem-wide, with the measured occupancy table written into SPEC.md and a test that fails if any shared number carries different meanings. SPEC's I/O-error mapping claimed "anything else -> 6" while omitting InvalidInput -> 2 and the new arm; both are now listed. `network-info` reached no doc at all and is token-free by design, contradicting 8.6's rule, so it gets 8.8 stating that exception and a README section. The logging change had no tests and shipped two runs of ~22 literal spaces from a lost string continuation. The decision is extracted as a pure function and the text as a constant; four tests cover announce, stay-silent, the ambiguity the message must resolve, and the whitespace. The whitespace row immediately earned itself: the first repair used a backslash continuation and `cargo fmt` rejoined it and materialised the indentation straight back into the string. `concat!` has no whitespace a formatter can reinterpret. The #317 marker surviving upgrades was reasoned in a comment and never exercised. The stub-systemctl harness now runs a marked upgrade and asserts both directions: nothing unconditional starts, and try-restart still cycles a running unit. Co-Authored-By: Claude <noreply@anthropic.com>
Correction to the "still unverified" listThe body says the new marked-upgrade rows in Stated precisely, because the two are not the same claim: the check is green; I have not read its log. The exit-code occupancy was corroborated by a second, independent sweepA slower full-tree Two other hits are different namespaces and are not rival exit-code numberings:
Noting the |
|
Verified fixed at head
Thanks — finding 2 was the valuable one and I would have merged the collision. It is the third instance of "free in my own table" in this ecosystem, so it is now filed as its own problem with an owner and a guard: https://github.com/DIG-Network/dig_ecosystem/issues/3189. |
Six lanes each computed "main is at 0.189.0, so I take 0.190.0" while open concurrently. #458 merged first and took that number, so every sibling's bump became a no-op against the new main -- the rebase was clean, no conflict, no `dropping` line, and the commit log still reads as a bump. Only the file on disk showed it. Member crates are unaffected: #458 moved the workspace version alone, so dig-node-core 0.65.0 and dig-wallet 0.44.0 are still ahead of main.
Clean rebase, zero conflicts, no `dropping` line -- and the bump commit still reads correctly in the log while being a no-op against the new main. Only the file on disk shows it. 0.191.0 and 0.192.0 are claimed by sibling lanes still in flight.
Clean rebase, no conflict, no `dropping` -- the bump commit survives and simply stops meaning anything against the new main. Read from the file, not the log. 0.191-0.193 are claimed by siblings still in flight.
…ing input surfaces (#453) * chore(secpeer): open lane for the peer/network-facing security batch Batch: #349 #352 #348 #285 #282 -- five defects where a stranger's input decides something, each fixed by correcting the failure direction. Co-Authored-By: Claude <noreply@anthropic.com> * fix(relay,config): fail closed on an unreadable relay endpoint and share one off-token #285: relay endpoint parsing accepted any scheme and turned an unparsable port into None, which relay_socket_addr resolved with .unwrap_or(443) -- so one malformed string made the node silently dial 443 at a host that may not be the one the operator wrote. Replaced with a fail-closed parse transcribed from dig_nat::relay::parse_relay_endpoint (the designated survivor, still private in the published 0.21.0), so adopting the export later is a deletion. #282/#352: the three network-reaching isolation knobs each carried a private off-token predicate reading a different vocabulary, so DIG_PEER_NETWORK=OFF left the peer network running while DIG_RELAY_URL=OFF disabled the relay. One shared is_off_token now serves all three, and DIG_RELAY_URL= means no relay rather than the compiled-in public one. Bootstrap resolution announces which branch it took. Co-Authored-By: Claude <noreply@anthropic.com> * fix(peers,reservation): withhold non-destination addresses; hold a possibly-in-flight bundle #349: dig.getPeers served the wildcard address to REMOTE peers as a dial candidate, because is_usable_contact had been adopted call site by call site and pool_peers was the fifth site nobody looked at. The check now lives in a ContactAddr type whose construction IS the question and whose address_json is the only renderer of the wire entry. The ADDRESS is withheld, never the peer: the row survives with an empty addresses array, the shape both consumers already handle. #348: reservation was gated on an untrusted 'accepted' alone, so an unexplained denial or a post-transmit transport failure freed coins of a bundle that may be in flight. Anything short of a STATED mempool rejection now holds to the TTL. Co-Authored-By: Claude <noreply@anthropic.com> * docs(spec): state the getPeers address rule and the possibly-in-flight reservation rule Both behaviours are now normative rather than incidental: a peer row with no dialable destination keeps the row and empties the addresses ARRAY (#349), and a push reserves its inputs unless the mempool DEFINITIVELY refused it (#348). Co-Authored-By: Claude <noreply@anthropic.com> * chore(release): 0.190.0 -- minor, two accepted-input sets narrow observably Root 0.189.0 -> 0.190.0, dig-node-core 0.64.0 -> 0.65.0, dig-wallet 0.43.0 -> 0.44.0 (both touched). Minor rather than patch because two behaviours narrow in ways a consumer can observe: parse_relay_host is pub and now refuses a scheme-less endpoint it used to accept, and DIG_RELAY_URL= changes meaning from 'the compiled-in public relay' to 'no relay'. Not major -- a 0.x line, and nothing left the public surface but the private relay_port. Cargo.lock carries the three version lines and no dependency churn. Co-Authored-By: Claude <noreply@anthropic.com> * style(peers): rustfmt the ContactAddr type-boundary comment Pure reflow of lines the type-boundary change made over-width. No behaviour change; `git diff -w` against the prior head is empty. Refs #349 * fix(wallet): a bare verdict is not a stated rejection, so the #348 hold can fire `is_definitive_rejection` (`sage/rpc.rs`) frees a bundle's inputs back into selection only for a refusal the mempool STATED, and holds them to the TTL otherwise -- because a peer that relayed the bundle and then answered with a bare verdict may still have put it in flight. `ChainTransport::push` manufactured a reason from `status.status`, so `rejection` was `Some(..)` on EVERY non-admitted answer, the guard was true every time, and the hold could never fire. The fix read as shipped and was vacuous; the double-select window #348 exists to close was untouched. SPEC.md §18.7 was already correct -- "a bare denial carrying no reason ... MUST be treated as POSSIBLY IN FLIGHT" -- so this is the code being brought to the spec, not a spec change. The decision is extracted as `stated_rejection` and asserted directly, because an end-to-end assertion on a held reservation passes for many reasons and only one of them is this mapping being right. Revert-proved: restoring the manufactured reason fails it on `left: Some("PENDING"), right: None`. Also corrects two comments that claimed more than the code does: the off-token is shared by the three ISOLATION knobs, not by every network-reaching `DIG_*` knob (four others read a narrower vocabulary -- #459); and `ContactAddr` closes the `dig.getPeers` emitter, not the class, since `provider_json` still relays remote-supplied candidates unchecked. Refs #348 #349 #459 Co-Authored-By: Claude <noreply@anthropic.com> * fix(wallet): carry the mempool's verdict label beside the stated reason Narrowing `rejection` to a STATED reason is what lets #348's hold fire, but taken alone it left an operator debugging a bare `PENDING` with `accepted:false`, `transaction_id:null`, `rejection:null` and no label at all. The response did not lie -- "not accepted, no reason given" is true -- but it said strictly less than before, which is its own regression on a money path. `PushOutcome` gains `verdict`, always present. The two fields answer different questions and only one of them may drive the hold, which is why they are separate rather than one string: folding the label back into `rejection` would re-break the guard, and dropping it re-blinds the operator. `control.wallet.broadcast` surfaces it, and the doc above it no longer claims a refusal always answers `{accepted:false, rejection}` -- a shape that stopped always holding when `rejection` narrowed. Caught by the security re-gate as a LOW observability finding on my own fix. Refs #348 Co-Authored-By: Claude <noreply@anthropic.com> * chore(release): 0.191.0 -- rebased past #458, which took 0.190.0 Six lanes each computed "main is at 0.189.0, so I take 0.190.0" while open concurrently. #458 merged first and took that number, so every sibling's bump became a no-op against the new main -- the rebase was clean, no conflict, no `dropping` line, and the commit log still reads as a bump. Only the file on disk showed it. Member crates are unaffected: #458 moved the workspace version alone, so dig-node-core 0.65.0 and dig-wallet 0.44.0 are still ahead of main. --------- Co-authored-by: Claude <noreply@anthropic.com>
What this is
The dig-node CLI / service / updater batch. Six tickets that are each a user-visible lie or dead end on a real machine, which is why they group.
dign network-infodocumented but absent.debupgrade left the old binary runningtry-restarton upgrade, verified in the packageIO_ERRORin the restart windowNODE_UNREACHABLECloses #303
Closes #304
Closes #305
Closes #407
(Outside code spans deliberately — inside backticks GitHub does not parse the keyword, so the issues would not autoclose.) #403 and #392 stay open deliberately — see below; closing them would be the paperwork lie this batch is about.
The #403 shape decision, and why
The token is the master capability, not merely "control": it authorizes pairing administration (mint/list/revoke) and
chiaPeers.add, which grants chain authority over the node's wallet replica.diggroup at 0640 hands that to every member, permanently, and outlives the app that motivated it.--userunit changes what the node is on a server — no boot start, per-login lifetime — for the one shape where an always-on node matters most.So: do not widen it. The node already has the right primitive for a client that cannot read a file — pairing (
pairing.rs), built for exactly this and used today by the MV3 extension. It yields a scoped, revocable per-client token that cannot mint or revoke pairings and cannot grant chain authority. The principal admitted is one operator-approved client on this host with mutation rights minus the master tier; strictly less than a group grant, and revocable without touching a file mode. The token stays0600 root:root.What lands here is the half that was in dig-node's hands: the remedy text told the operator to "reinstall so the service grants your account read access". True on Windows (the installer keeps an explicit read grant for the interactive install-user), false on Ubuntu — the
.debleaves 0700/0600 and reinstalling changes nothing, so the operator loops. The platform is now a function argument, not acfg!branch, so both sentences are asserted on any host.Blast radius
.gitnexus's registered index points at the primary checkout and is ~300 commits behind, so a query would return a false-safe zero. This was done with grep + direct read, and is stated as such.format_statuspeers::run)call_asyncOption<&str>;call_controlbyte-identicalExitCode::from_io_errorConnectionRefusedarmcontrol_token_remedy_forpostinstThe one contract change, called out so the gate can disagree with the bump: an unreachable node now exits 12 where it exited 6. Additive to the catalogue, but a script matching 6 sees a change. Bumped 0.189.0 → 0.190.0 (minor: new
network-infocapability).Why 12 and not 7 — the occupancy was measured, not assumed
NODE_UNREACHABLEwas first assigned 7 by reading this CLI's own table, where 7 genuinely was the next number. 7 isNOT_CONNECTEDin dig-app'sdiga, whoseoutcome.rsstates in its own doc comment that it deliberately shares dig-node's numbering — and 8–11 are taken as well. This is the identical reasoning that already cost a yank in this repo's JSON-RPC error space, where-32015was taken as "the next free code" from the owning crate's own list and collided with a releasedMETADATA_TOO_LARGE.Measured across dig-node, dig-app, dig-installer, dig-updater and this repo's scripts and e2e harnesses. The installer / digstore / example
std::process::exit()calls are separate binaries with their own namespaces, not this shared catalogue, and are excluded on that basis.digndigaOKOKNOT_SERVINGUSAGEUSAGEPERMISSION_DENIEDSERVICE_FAILEDBIND_FAILEDIO_ERRORIO_ERRORNOT_CONNECTEDENGINE_ERRORLOCKEDNOT_FOUNDDENIEDNODE_UNREACHABLE(this PR)0–11 were therefore taken before this CLI added a code, and 12 is the first free number. The map is now written into
SPEC.md§8.4 so the next person does not re-derive it, andno_exit_code_collides_with_the_dig_app_gateway_numberingfails if a shared number ever carries two meanings.Evidence — what was measured, and where
Test counts checked, not just exit statuses.
cargo test -p dig-node-service --lib— 663 passed, 0 failed, 0 filtered out (the whole lib, no filter, so the count is the suite rather than a filter that matched nothing).logging.rspredate this PR and exercisehealth(...); the logging change had zero tests. It now has four, listed below.if connected == 0forced false, theConnectionRefusedarm deleted, and the platform branch pinned — 3 ran, 3 failed, 0 passed. All three are load-bearing.cargo clippy -D warningsRC=0;cargo fmt --checkclean.systemctl. Before: upgrade emittedenable --nowonly. After:try-restarton upgrade, nothing on first install (the control that proves the assertion measures the upgrade argument), andtry-restartunder the no supported way to install without first joining the public network — the node auto-starts before configuration #317 marker — a no-op on a stopped unit, so a node the operator stopped is never started.Stated as unverified
dpkg-debis unavailable on this Windows host, sodeb-contents.test.shSKIPs locally and is gated in CI. The local proof used the same extracted postinst and stub.bootout→bootstrap→kickstart -kalready cycles the daemon.DIG_WALLET_PORT/LOCALAPPDATAare read only in the dig-wallet crate; the listener dig-node itself spawns is already non-silent (dig-node silently takes Sage's RPC port 9257 and demands mTLS — a reboot after install breaks Sage for users #260). Reproducing needs a real out-of-service run under those overrides.Gate round 2 — the six findings, and what each fix is proved by
sudo dign pairas the approving verb; a barepairmaps toPairAction::List. The test assertedcontains("dign pair"), satisfied by the wrong string as its own substring.sudo dign pair approve <pairing_id>, and says the bare verb only lists.the_unreadable_token_remedy_offers_a_scoped_credential_not_a_wider_filenow asserts the full command, plus a one-sided row that fails on the false attribution`sudo dign pair` approves. Newthe_bare_pair_verb_lists_and_only_approve_approvespins the mapping against the parser viaCli::try_parse_from, so the claim is measured against behaviour rather than against itself.NOT_CONNECTEDindiga; 8–11 taken too. Releasing 0.190.0 would freeze the collision.SPEC.md§8.4.no_exit_code_collides_with_the_dig_app_gateway_numberingwalks everyExitCodeagainst the transcribeddigamap and fails if a shared number carries two meanings.InvalidInput → 2.from_io_erroris now listed, in match order.network-inforeached no doc, including §8.1's normative subcommand list, and §8.6 asserts the opposite token property.dig.getNetworkInfobody); §8.6 cross-references it; §8.1 lists it; README gains a section.degrade_announcement; text asFILE_LOGGING_DEGRADED, built withconcat!.a_live_file_sink_announces_no_degrade(the silent control, so an unconditional warner cannot pass),a_failed_file_sink_announces_and_names_the_reason(asserts the reason by value),the_announcement_says_an_empty_directory_means_denied_not_quiet(the ambiguity the message exists to resolve),the_announcement_text_has_no_lost_string_continuation.Release-script testsruns onubuntu-latestbut is not a required context, so #305's only executing proof cannot block a merge.try-restartunder the #317 marker was reasoned, not tested.main. A marked-upgrade case added to the existing stub-systemctlharness.try-restartstill emitted (one-sided in the opposite direction, so neither is satisfied by a postinst that does nothing), and adaemon-reloadcontrol proving the configure branch actually ran.The whitespace test earned itself within the hour
The first repair of finding 5 used a -continued string literal.
cargo fmtrejoined the lines and materialised the leading indentation straight back into the string — reintroducing the exact defect, silently, between writing the fix and running it.the_announcement_text_has_no_lost_string_continuationfailed and caught it. The constant now usesconcat!, which has no whitespace a formatter can reinterpret.Revert-proofs — committed first, reverted by file copy
Three fixes reverted independently — the pairing verb, the exit code, the whitespace. 660 passed, 3 failed, exactly one failure per test, each for its own reason. Restored afterwards; the tree hashes identical to the commit.
cargo fmt --all --checkclean.Branch protection was changed
Release-script testswas added tomain's required contexts — now Lint commit messages, Check version increment, Rustfmt, Clippy, Test + coverage, Release-script tests — and read back to verify. The job already ran on every PR tomain, so live sibling PRs already report the context and are not blocked by this.Still stated as unverified
dpkg-debis unavailable on this Windows host, sodeb-contents.test.shSKIPs locally (confirmed: it prints the SKIP) and runs in CI, which is now required. The new marked-upgrade rows have not executed anywhere yet — CI is their first run.Branch
loop/batch-cli, heada4e436292d96dcafa983c62a187215919bd81844.