Skip to content

fix(config): a mistyped capability flag must be said out loud, not silently guessed - #472

Merged
MichaelTaylor3d merged 5 commits into
mainfrom
loop/mc-drain-459
Sep 1, 2026
Merged

fix(config): a mistyped capability flag must be said out loud, not silently guessed#472
MichaelTaylor3d merged 5 commits into
mainfrom
loop/mc-drain-459

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What this changes

Closes #459.

Five default-ON capability knobs each read their own private off-vocabulary, narrower than the one
#282/#352 unified for the three isolation knobs, and all five applied their default silently on
an unrecognised value.

#459 asks for two separable decisions. This takes a different answer on each.

Decision 1 — failure direction: neither flag inverts; both now warn

The ticket's own discriminator gives the wrong answer for one of its two flags, and that correction
is the substantive part of this PR.

The proposed test is "can the flag's behaviour reach the network? If yes, fail closed."
DIG_WALLET_ENABLE_CHAIN_SYNC does — it dials chia peers — so the rule says fail closed. Applied, it
would silently stop the replica advancing on a typo, and dig-node#416 records that a stale replica's
zero balance is indistinguishable from an empty wallet. It converts a typo into a surface asserting a
falsehood about the operator's money: the exact defect class the sibling fix was protecting against.

The generalisation the proxy was standing in for, and which survives:

Fail in whichever direction cannot make a surface assert a falsehood.

Closed for an isolation knob (fail-open leaves a node dialling a network its operator asked it to
leave). Open for a default-ON read path (fail-closed manufactures a false zero). Same principle,
opposite outcome — recorded at the call site so the next reader inherits the reasoning, not the proxy.

DIG_NODE_DIGLOCAL binds loopback only (127.0.0.2:80/443, [::1]:443), so it never engaged the rule.

What both share is that the residue was never the direction — it was the silence. An unrecognised
value now names the variable, the rejected value, and the default applied, and says the setting had no
effect.

Decision 2 — vocabulary: adopt the shared TOKENS, subtract the empty rule

All five capability knobs adopt the shared off/on tokens, so disabled works everywhere off does.
DIG_NODE_STORE_MELT most needed it: it stops the node's only irreversible-delete path, so an
unrecognised off-token meant content deleted by a node whose operator believed they had stopped it.

The empty-is-off rule is deliberately NOT inherited. For an isolation knob an empty value names the
empty LIST (#312, correct, untouched). A capability knob holds no list, and X= is what
export X="$UNSET_VAR" produces — so inheriting it would reach #416's false zero through the
vocabulary, having just refused it through the failure direction.

peer::is_off_token is pub(crate), which is how five private copies came to exist. Rather than
write a sixth (§2.0), the vocabulary is exposed UP from the crate root. peer.rs is untouched — it
belongs to a live lane.

Blast radius checked

gitnexus indexes for this repo are ~300 commits stale (§2.0: a stale index returns a false-safe
impactedCount: 0), so this was taken by grep + direct read and is stated as such.

  • parse_dig_local_flag / parse_chain_sync_flag: callers are Config construction and their own
    tests. ingest_enabled, resolve_store_melt_enabled, profile_sync_enabled: one env-reading caller
    each plus tests.
  • New public surface on dig-node-core (is_capability_off_token, is_capability_on_token,
    classify_flag, FlagWord, describe_unrecognised_flag) — additive only, no signature changed.
  • Excluded files untouched: peer.rs, mirror/*, spend_audit.rs, server.rs, download.rs,
    dispatch.rs, content_serve.rs, admission.rs, conduct.rs.

Evidence

New tests: 4 passed; 0 failed (count read, not exit status). Full suites on the final tree:
dig-node-core 1030 passed / 0 failed, dig-node-service 692 passed / 0 failed.

#459 warns that the existing peer_network_enabled test passed under both the old and new parsers,
because it listed only lowercase exact tokens. So every alternative was actually implemented and run,
each on a committed tree:

alternative implemented result caught by
fail closed on unrecognised (the mechanical rule) 3 passed; 1 failed an_unrecognised_capability_flag_keeps_its_default_and_says_so
fail open silently (the shipped behaviour) 3 passed; 1 failed the same test, on its disclosure assertions
adopt is_off_token wholesale, empty included 3 passed; 1 failed an_empty_capability_flag_is_absent_not_off
keep the narrow shipped vocabulary 1 passed; 3 failed the_capability_flags_read_the_shared_off_vocabulary

The typo fixture is "fasle" — a real misspelling of false, where the two candidate failure
directions give opposite answers — not a token neither implementation would accept.
a_recognised_capability_flag_says_nothing is the control against warning unconditionally.

SPEC

SPEC.md gains a normative Capability-flag vocabulary and failure direction clause. Writing it
surfaced a fifth capability flag in neither the ticket's list nor peer.rs's —
DIG_NODE_PROFILE_SYNC — which is fixed here, because leaving it would have made the clause false in
the commit that introduced it. The clause is written as a definition rather than a list, so a sixth
flag is bound without anyone remembering to extend an enumeration.

Two section references I had drafted (§4.1b, §4.1c) cite nothing in this SPEC and were removed.

Status

DRAFT — the gate round has not returned.

…lently guessed

Settles both halves of #459 separately, taking a different answer on each.

FAILURE DIRECTION. Neither flag inverts. #459 proposed the discriminator "can the
flag's behaviour reach the network?" -- DIG_WALLET_ENABLE_CHAIN_SYNC does, so that
test says fail closed, and applied here it would produce a defect: failing closed on
a typo silently stops the replica advancing, and #416 records that a stale replica's
zero balance is indistinguishable from an empty wallet. The surviving generalisation
is to fail in whichever direction cannot make a surface assert a falsehood -- closed
for an isolation knob, open for a default-ON read path. What both cases share is that
silence is wrong, so an unrecognised value now names the variable, the rejected value,
and the default applied.

VOCABULARY. Five capability knobs adopt the shared off-tokens, so 'disabled' works on
all of them as it does on the three isolation knobs. The empty-is-off rule is NOT
inherited: it is correct for a knob holding a LIST (#312) and would, for a capability
knob, reach #416's false zero through the vocabulary having just been refused through
the failure direction.

is_off_token is crate-private to dig-node-core, so the vocabulary is exposed UP from
the crate root rather than copied a fifth time.

Closes #459
…nob it named

Writing the SPEC clause for #459 found a FIFTH capability flag the ticket did not
list: DIG_NODE_PROFILE_SYNC carried its own private off-vocabulary. The clause
asserts that every capability flag reads one vocabulary, so leaving it would have
made the clause false in the commit that introduced it -- the four knobs #459 names
were the four someone had listed, not the four that exist.

Also removes two section references (4.1b, 4.1c) that this SPEC does not contain;
they were invented while drafting and cite nothing.
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review September 1, 2026 09:38
@MichaelTaylor3d
MichaelTaylor3d merged commit cd4c477 into main Sep 1, 2026
20 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/mc-drain-459 branch September 1, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two more config flags fail OPEN on an unrecognised value — decide whether that is correct for them

1 participant