0.5.195: a node vouches about itself on infra:attest — config:load, self-attested, expiring - #504
0.5.195: a node vouches about itself on infra:attest — config:load, self-attested, expiring#504emooreatx wants to merge 4 commits into
Conversation
…ted, expiring
Third attempt at the same thing, and the first one that reads the constitution
rather than reasoning around it.
THE AUTHORITY WAS ALREADY CONFERRED. Every node holds `infra:attest` — "vouch as
the delegator's infrastructure" — from its owner-binding, and the canonical holds
it from the accord's charter (CC 4.4.3.8: "a root serves and vouches, or it is
inert"). It is conferred SO THE NODE CAN VOUCH ABOUT THE DELEGATOR'S
INFRASTRUCTURE, and "I am at capacity" is a fact about that infrastructure only
the infrastructure can observe. This is not a node exceeding its grant; it is the
grant working. Clause E says it directly: a node "may sign its own refusal
without new authority."
THE DIMENSION WAS ALREADY RESERVED. CC 3.1 `config:{scope}` — "a node's declared
operating configuration, published as an auditable record rather than inferred
from behaviour… only ever about the emitting node" — with open scope vocabulary.
CC 3.4.5 gives its emitter rule, SELF-OR-OWNER: "a node's running configuration is
a self-report; a third-party assertion of what you are running is a rumour." So
`config:load` joins `admission` / `replication` / `moderation` / `transport`, and
attesting == attested == subject by construction.
WHAT THE TWO WRONG VERSIONS GOT WRONG. The first would have automated
`OP_SELF_SHED`, an `admin_action:` — refused at the write door without a
`delegation_id`, which a node does not have. The second authored a HardCaseEvent
with an expiry, which is on NO replication plane, so "the artifact a peer reads
to stop offering" could never reach a peer. I then over-corrected to "a node has
no standing at all", which CC 4.2.1 does not say: that section governs
`mesh_config`, the ROOT's plane, which changes what OTHER nodes carry. Two
planes, two authors, one reason — the root relieves ACROSS nodes; a node vouches
ABOUT itself. This module never touches the first.
WHY IT IS AN ATTESTATION. Because attestations replicate — that is why they are
the universal primitive. `expires_at` is a real signed instant persist enforces
at read (`expires_at IS NULL OR expires_at > NOW()`), so a stale declaration is
not merely ignorable, it is invisible. The lifetime encodes the authority; there
is no lift because recovery is the lift.
ONLY THIS NODE'S OWN STALL MAY RENEW. `/proc/pressure/*` describes the whole
box, and a noisy neighbour there would make this node ATTEST, UNDER ITS OWN
SIGNATURE, that it is shedding — a false self-report replicated to every peer.
Only a `Cgroup`-scoped, `full`-line reading at `degradation`'s own degrade bound
counts; host scope, a kernel with no `full` line, or PSI unavailable renews
nothing. The loop acts on the FRESH probe result, never the registry, because
the probe deliberately preserves a stale warning when PSI vanishes (no-evidence
discipline) and a registry read would renew a "short-lived" row forever.
Under the NODE key, not the engine's: on an agent-carrying node those differ, and
this is a statement about the infrastructure.
A gap named, not closed: persist gates `mesh_config:` at admission but does NOT
enforce CC 3.4.5 self-or-owner on `config:*`. This producer honours it by
construction; the substrate check is asked for upstream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSa6L2MEDAXmoJuBVUZNVg
Substrate unchanged: persist v38.6.0 / edge v18.11.0 / verify v13.6.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSa6L2MEDAXmoJuBVUZNVg
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 675d448879
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// The `config:{scope}` leaf for carried load. Open vocabulary per CC 4.5.1.1; | ||
| /// `admission` / `replication` / `moderation` / `transport` are the canonical | ||
| /// scopes and this joins them as the node's self-report of what it is carrying. | ||
| pub const DIMENSION: &str = "config:load"; |
There was a problem hiding this comment.
Add a version segment to the scores dimension
spec() emits a scores attestation with dimension config:load, but persist's DimensionAdmissionPolicy requires every scores dimension to contain a :vN segment (the existing producers use values such as config:v1 and capacity:sustained_coherence:v1). Consequently, whenever a cgroup stall triggers this path, put_attestation rejects the row and the observer only logs config:load attestation failed, so no load declaration can ever reach peers. Use a versioned dimension such as config:load:v1.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — fixed. config:load:v1, with a test that pins the :vN segment.
This is the worst kind of defect: every local test passed because none of them reach the put door, and in production the loop would have logged "attestation failed" once a minute while nothing ever reached a peer. Thank you.
| let row = crate::attest::Emit::stamp(node_key_id, spec(node_key_id, expires_at)) | ||
| .map_err(|e| anyhow::anyhow!("stamp config:load for {node_key_id}: {e}"))? | ||
| .sign_and_assemble(crate::attest::KeySigner::Engine(engine)) | ||
| .await |
There was a problem hiding this comment.
Sign split-node attestations with the node key
On an agent-carrying node, wire_identity() deliberately returns the separately minted node key while KeySigner::Engine signs with the actor Engine key. The row is therefore stamped with the node key as attesting_key_id but signed by the actor key; admission verifies the signature against the registered node public key and rejects every emission. Retain/use the LocalSigner returned by node identity resolution when the wire and Engine identities differ.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — fixed. NodeIdentityResolution.signer was built for exactly this ("author rows AS the node while the engine signs as the actor") and was being dropped after boot. It is now carried into the observer as NodePen::Node; unsplit nodes (the binary, and the bare-agent path where the engine key IS the node key) use NodePen::Engine.
A row stamped as the node and signed by the actor failing verification on exactly the topology this exists for — that would have been invisible until an agent-carrying node hit contention.
| let load_observer_key = crate::node_key::wire_identity() | ||
| .map(str::to_owned) | ||
| .unwrap_or_else(|| cfg.key_id.clone()); | ||
| let _load_observer_join = crate::load_shed::spawn(Arc::clone(&engine), load_observer_key); |
There was a problem hiding this comment.
Stop the load observer during server teardown
When shutdown_node() is used for the supported in-process restart path, dropping this JoinHandle detaches the infinite task; the teardown below neither signals nor aborts it. The old observer therefore retains its Engine and continues probing and renewing load attestations while the node is stopped, and every restart adds another observer. Give this loop a shutdown channel and join it alongside the retention and configuration loops.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — fixed. Shutdown channel added and the observer is joined at teardown beside the retention loop. On the delivery path the same channel is owned by the controller.
| // clearing it (no-evidence discipline), so reading the registry here | ||
| // would renew a "short-lived" row forever off a stale entry (Codex, | ||
| // PR #503). A fresh `Measured` stall is the only thing that renews. | ||
| let (cpu, io) = crate::degradation::probe_contention(); |
There was a problem hiding this comment.
Serialize the observer probe with health collection
The health route holds degradation::COLLECT_LOCK across its probes and final verdict() specifically so the returned resource readings cannot be paired with another producer's warning state, but this new background caller invokes the state-mutating probe_contention() without that lock. If its minute tick lands after a health request's contention read but before its verdict, the response can again contain CPU/IO readings from one instant and status/warnings from another. Acquire the same collection lock around this probe so it cannot interleave with health collection.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — fixed. The probe now runs under COLLECT_LOCK, the same guard /v1/health holds across its probes and verdict(). probe_contention is synchronous file IO, so the std mutex is held for microseconds and never across an await.
| let load_observer_key = crate::node_key::wire_identity() | ||
| .map(str::to_owned) | ||
| .unwrap_or_else(|| cfg.key_id.clone()); | ||
| let _load_observer_join = crate::load_shed::spawn(Arc::clone(&engine), load_observer_key); |
There was a problem hiding this comment.
Start the load observer on the embedded delivery path
The wheel's bare-agent topology enters through federation_delivery::start_and_hold, whose own comments confirm that it never executes serve_with_adapter; this compose-only spawn therefore leaves those agent-carrying nodes without any load observer. Under CPU or IO contention they will never emit the new self-attestation, even after the dimension and signer defects are fixed. Factor the observer startup into the shared composition machinery and invoke it from both entry points, as is already done for trust-root installation and peer de-admission arming.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — fixed. The observer now starts from start_and_hold too, and on that path it is owned by DeliveryController with shutdown wired into its Drop beside the reconcile loop, rather than spawned and detached. No identity split occurs on the bare path, so the engine pen is right there.
Five findings, all correct. Two are the kind where the feature does not work AT
ALL and every local test passes, because the test never reaches the put door.
THE DIMENSION HAD NO VERSION SEGMENT. persist's `DimensionAdmissionPolicy`
refuses any `scores` dimension without `:vN` (`MissingVersionSegment`, T3), and
every existing producer carries one — `config:v1`, `consent:replication:v1`. So
`config:load` would have been rejected on every emission while the loop logged
"attestation failed" once a minute and nothing ever reached a peer. Now
`config:load:v1`, and a test pins the segment.
THE SIGNER WAS THE ACTOR'S ON A SPLIT NODE. `wire_identity()` returns the
separately-minted node key on an agent-carrying node, but `KeySigner::Engine`
signs with the actor's engine key. A row stamped as the node and signed by the
actor fails signature verification against the node's registered public key —
every emission rejected at admission on exactly the topology this exists for.
`NodeIdentityResolution.signer` was built for precisely this ("author rows AS the
node while the engine signs as the actor") and was being dropped after boot. It
is now carried into the observer as `NodePen::Node`; unsplit nodes — the binary
and the wheel's bare-agent path, where the engine key IS the node key — use
`NodePen::Engine`.
THE OBSERVER NEVER STARTED ON THE BARE-AGENT PATH. The wheel topology enters
through `federation_delivery::start_and_hold` and never runs `serve_with_adapter`,
so agent-carrying wheel nodes would never self-report under contention. It now
starts from both entry points, and on the delivery path it is OWNED by
`DeliveryController` — shutdown wired into its `Drop` beside the reconcile
loop's — rather than spawned and detached.
THE OBSERVER OUTLIVED THE NODE. Dropping a `JoinHandle` only detaches; on the
in-process `shutdown_node()` restart the old observer kept its `Arc<Engine>`, kept
probing, kept renewing rows against the old node, and every restart added one
more. It now has a shutdown channel and is joined at teardown beside the
retention loop.
THE PROBE RACED HEALTH COLLECTION. `/v1/health` holds `COLLECT_LOCK` across its
probes and final `verdict()` precisely so one response cannot pair resource
readings from one instant with warning state from another. A background probe
without the lock could land between those two steps and reintroduce that. The
observer now probes under the same lock; `probe_contention` is synchronous file
IO, so the std mutex is held for microseconds and never across an await.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSa6L2MEDAXmoJuBVUZNVg
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 165ec59d8c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| crate::load_shed::spawn( | ||
| Arc::clone(&engine), | ||
| crate::load_shed::NodePen::Engine, | ||
| load_observer_key, |
There was a problem hiding this comment.
Sign embedded load reports with the node key
On the wheel path for an agent-carrying node, provision_node_identity explicitly keeps the Engine as the actor signer while minting a separate node key and storing that key in wire_identity; this call therefore stamps the node ID into the report but signs via NodePen::Engine with the actor key, causing put_attestation to reject every load report. Fresh repository evidence contradicts the earlier thread's no-split assumption: provision_node_identity calls set_actor_identity(actor) and separately set_wire_identity(&key_id). Retain or reopen the node's LocalSigner for this observer, as the composed path does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and this one corrects a claim I made explicitly — I said the bare-agent path does not split. It does: provision_node_identity mints a node key and sets BOTH set_actor_identity and set_wire_identity, leaving the engine signing as the actor. Thank you for going back to the repository rather than taking the earlier thread at its word.
Fixed by retaining the signer that was already being created and discarded (let (_signer, ...)) — provisioning now keeps it and the delivery path reads it back. So the embedded observer signs as the node on exactly the topology this feature exists for.
| (paths::DIMENSION): DIMENSION, | ||
| "attesting_key_id": node_key_id, | ||
| "subject_key_ids": [node_key_id], | ||
| "score": 1.0, |
There was a problem hiding this comment.
Include confidence in the load score envelope
When a peer evaluates this replicated scores row through the repository's standard compose_policy::Composer, screen requires an explicit confidence field and returns MalformedEnvelope("confidence") when it is absent; it deliberately does not default confidence to 1.0. Consequently the new load declaration cannot contribute to the composed verdict a peer would use to stop offering work, despite being otherwise admitted and replicated. Add the producer's intended confidence alongside score.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — fixed. confidence: 1.0 added, and the integration test now asserts a peer can screen the row rather than only that admission accepted it.
Your framing is the useful part: admission is not the last gate. The row would have been admitted, replicated, and contributed nothing to the verdict a peer uses to stop offering work — which is the entire purpose of replicating it. 1.0 because the producer is the subject under witness_relation: self; a consumer discounts self-attestation by relation rather than the producer pre-discounting its own measurement.
| continue; | ||
| } | ||
|
|
||
| match emit(&engine, &pen, &node_key_id).await { |
There was a problem hiding this comment.
Bound expired load-attestation growth
While a node remains under cgroup pressure, this branch authors a fresh UUID-backed federation attestation every 60 seconds, and expiry only makes old rows inactive—it does not remove them. The repository's retention loop explicitly has deletion levers only for traces and audit entries and documents attestation-heavy databases as an unenforceable disk-bound case, so one continuously stalled node adds 1,440 permanent attestation and wire-index rows per day to its own store and every receiving peer, worsening the storage pressure that can trigger this loop. Add an expired-attestation pruning path or another bounded renewal representation before emitting indefinitely.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — fixed. Renewal is now spaced to just inside the TTL rather than running at the observation cadence, so a continuously stalled node keeps a live declaration at roughly a third of the rows.
The detail that made this land: expiry makes a row inactive but does not remove it, and retention has deletion levers for traces and audit entries only. A permanently stalled node adding 1,440 permanent rows a day to its own store and every peer's would worsen the very storage pressure that can trigger this loop. Detection stays at 60s — noticing quickly still matters; it is the writing that is throttled.
Bounded pruning of expired attestations is the real fix and is a substrate ask; filing it rather than leaving it implied here.
THE TEST FIRST, because it is why the rest were findable at all.
`tests/config_load_reaches_the_put_door.rs` runs the producer's row through a REAL
`put_attestation` on a real engine, in both topologies, and asserts a peer can
screen what it receives. Every unit test in `load_shed` was green while the row
was unadmittable in production; the rule this encodes is that A PRODUCER IS NOT
TESTED UNTIL ITS ROW HAS BEEN THROUGH THE PUT DOOR. One case deliberately emits
the unversioned dimension the first cut shipped and asserts persist REFUSES it, so
the defect that passed every local test now fails loudly.
THE WHEEL PATH SPLITS TOO — I told the operator it did not, and that was wrong.
`provision_node_identity` mints a node key and sets BOTH `set_actor_identity` and
`set_wire_identity`, leaving the engine signing as the ACTOR. So the embedded
observer was stamping rows as the node and signing them with the actor's pen:
rejected at admission, once a minute, on exactly the topology this feature exists
for. The signer was already being created and thrown away (`let (_signer, ...)`);
it is now RETAINED at provisioning and read back by the delivery path.
CONFIDENCE WAS MISSING FROM THE ENVELOPE. `compose_policy::Composer::screen`
refuses an envelope without it (`MalformedEnvelope("confidence")`) and
deliberately does not default to 1.0 — so the row would have been admitted,
replicated, and contributed NOTHING to the verdict a peer uses to decide whether
to stop offering work. Admission is not the last gate, and the whole point of
replicating this is that a peer can act on it. Now asserted against the consumer,
not just the producer.
A STALLED NODE WAS MINTING A PERMANENT ROW EVERY MINUTE. Expiry makes a row
inactive; it does not remove it, and the retention loop has deletion levers for
traces and audit entries only — an attestation-heavy store is a documented
unenforceable disk case. One continuously stalled node would add 1,440 permanent
rows a day to its own store AND every peer's, worsening the storage pressure that
can trigger this loop in the first place. Renewal is now spaced to just inside the
TTL: a live declaration continuously, at ~1/3 the rows. Detection stays at 60s
because noticing quickly still matters — it is the WRITING that is throttled.
Only a successful emission moves the renewal clock, or a transient refusal would
suppress renewal for a whole gap and the node would go quiet while still
struggling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSa6L2MEDAXmoJuBVUZNVg
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7de999a46c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// cadence: the node keeps a live declaration continuously, at ~1/3 the rows. The | ||
| /// observation interval stays short because DETECTING quickly still matters — it | ||
| /// is the *writing* that is throttled, not the looking. | ||
| pub const MIN_EMIT_GAP_SECS: i64 = TTL_SECS - OBSERVE_INTERVAL.as_secs() as i64; |
There was a problem hiding this comment.
Avoid overlapping live load renewals
With the 120-second gap and 180-second TTL, every renewal overlaps the previous row for roughly a minute. Composer computes the self-attestation weight from the number of live self emissions, so one load row contributes 0.5, but two overlapping rows from the same node each contribute only 1/3 and their signed mean remains 1/3. A peer using a threshold such as 0.5 therefore changes from Affirm to Deny while the node is still continuously stalled, then changes back when the older row expires. Renew without overlapping live rows, or replace/supersede the prior declaration, so continued pressure cannot weaken the load verdict.
Useful? React with 👍 / 👎.
| pub fn spec(node_key_id: &str, expires_at: chrono::DateTime<chrono::Utc>) -> crate::attest::Spec { | ||
| let envelope = serde_json::json!({ | ||
| (paths::DIMENSION): DIMENSION, | ||
| "attesting_key_id": node_key_id, |
There was a problem hiding this comment.
Make node load reports pass peer trust screening
When a receiving peer evaluates this self-authored row with the repository's standard Composer, TrustSet::bootstrap pins only accord_holder and steward keys, not ordinary node keys, so screen returns NotInTrustSet before considering the load fields. There is no production path in the repository that pins the emitting node for composition. Fresh evidence beyond the earlier confidence fix is that a_peer_can_screen_the_row_it_receives merely reads confidence and score; it never constructs a Composer or exercises the trust gate. Wire the node's infra:attest authority into the consumer trust set, or author the report through an identity peers already trust.
Useful? React with 👍 / 👎.
| // self`, and a consumer discounts self-attestation by relation rather than | ||
| // by the producer pre-discounting its own reading. | ||
| "confidence": 1.0, | ||
| "cohort_scope": cohort_scope::FEDERATION, |
There was a problem hiding this comment.
Keep federation-scoped reports out of the config namespace
This assigns federation scope to a config:* row even though the repository's explicit namespace invariant says every config row must use cohort_scope=self; graph_config::CONFIG_COHORT_SCOPE, check_config_cohort_scope_self, and tests/config_cohort_scope.rs all pin this because federation scope makes config rows directory-advertised and cohort-replicable. Downstream processors are therefore entitled to treat config:* as structurally invisible, while this new subtype violates that contract. Since this report intentionally needs replication, use a namespace whose scope permits federation visibility or update the config namespace contract and its conformance checks explicitly rather than silently introducing an exception.
Useful? React with 👍 / 👎.
|
Converting to draft. Three findings, and together they say the peer-facing half has no home in this substrate yet — so I am stopping rather than patching a fourth time.
No peer can compose it. Renewal would weaken the signal. With a 120s gap and 180s TTL two rows overlap for ~60s, and Third attempt at this, each time fixing the named defect and hitting a new structural one: What stands and is unaffected: the local self-protection shipped in 0.5.194, the put-door test discipline, and the substrate asks — CC#96, CIRISPersist#777/#778/#779. Filing the missing capability so it is tracked as one thing rather than rediscovered a fourth time. |
|
Retitled — this claimed "0.5.195", and 0.5.195 has since shipped as a different cut ( This remains withdrawn pending CIRISConstitution#97: a node self-report has no peer-readable route. The durable output of this attempt is on main already — |
0.5.195 — a node vouches about itself on
infra:attestSubstrate unchanged: persist v38.6.0 / edge v18.11.0 / verify v13.6.1.
Supersedes #503 (withdrawn). Third attempt at one thing, and the first that reads
the constitution rather than reasoning around it.
The authority was already conferred
Every node holds
infra:attest— "vouch as the delegator's infrastructure"— from its owner-binding; the canonical holds it from the accord's charter
(
[infra:attest, infra:serve, infra:store, infra:transport], CC 4.4.3.8: "a rootserves and vouches, or it is inert"). It is conferred so the node can vouch
about the delegator's infrastructure, and "I am at capacity" is the one fact
about that infrastructure only the infrastructure can observe.
This is not a node exceeding its grant. It is the grant working. Clause E says
it directly: a node "may sign its own refusal without new authority."
The dimension was already reserved
CC 3.1
config:{scope}— "a node's declared operating configuration,published as an auditable record rather than inferred from behaviour… only ever
about the emitting node" — open scope vocabulary. CC 3.4.5 gives the emitter rule,
self-or-owner: "a node's running configuration is a self-report; a
third-party assertion of what you are running is a rumour."
So
config:loadjoinsadmission/replication/moderation/transport.Attesting == attested == subject by construction;
witness_relation: self;federation scope so it replicates;
expires_ata real signed instant persistenforces at read.
What the two wrong versions got wrong
OP_SELF_SHED— anadmin_action:, refused at the write doorwithout a
delegation_ida node does not have.HardCaseEventwith an expiry (0.5.195: a node may say it is struggling, and the saying expires on its own #503) — on no replication plane, so "theartifact a peer reads to stop offering" could never reach a peer.
does not say. That section governs
mesh_config, the root's plane, whichchanges what other nodes carry.
Two planes, two authors, one reason: the root relieves across nodes; a node
vouches about itself. This PR never touches the first.
Only this node's own stall may renew
/proc/pressure/*describes the whole box. A noisy neighbour there would make thisnode attest, under its own signature, that it is shedding — a false
self-report replicated to every peer. So only a
Cgroup-scoped,full-linereading at
degradation's own degrade bound counts; host scope, a kernel with nofullline, or PSI unavailable renews nothing.The loop acts on the fresh probe, never the registry — the probe deliberately
preserves a stale warning when PSI vanishes (no-evidence discipline), and a
registry read would renew a "short-lived" row forever. Both pinned by test.
TTL = 3 × the 60s interval. No lift: recovery is the lift. Under the node
key, not the engine's — on an agent-carrying node those differ, and this is a
statement about the infrastructure.
Filed
config:loadasa named canonical scope.
load.ceilingkey; the "node formwithdrawn" reasoning retracted.
for
config:*. Today anyone can attest what a node is running. This producerhonours the rule by construction; the substrate leg is missing.
Verification
466 lib tests,
release_gates42,envelope_vocabulary_single_source7,field_conformance6,compliance_map1. Clippy--all-targetsclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01BSa6L2MEDAXmoJuBVUZNVg