Skip to content

0.5.195: a node vouches about itself on infra:attest — config:load, self-attested, expiring - #504

Draft
emooreatx wants to merge 4 commits into
mainfrom
feat/0.5.195-node-config-load-attestation
Draft

0.5.195: a node vouches about itself on infra:attest — config:load, self-attested, expiring#504
emooreatx wants to merge 4 commits into
mainfrom
feat/0.5.195-node-config-load-attestation

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

0.5.195 — a node vouches about itself on infra:attest

Substrate 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 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 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:load joins admission / replication / moderation / transport.
Attesting == attested == subject by construction; witness_relation: self;
federation scope so it replicates; expires_at a real signed instant persist
enforces at read.

What the two wrong versions got wrong

  1. Automating OP_SELF_SHED — an admin_action:, refused at the write door
    without a delegation_id a node does not have.
  2. A HardCaseEvent with 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 "the
    artifact a peer reads to stop offering"
    could never reach a peer.
  3. Then an over-correction 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 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 this
node attest, under its own signature, that it is shedding — a false
self-report replicated to every peer. So 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, 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

  • CIRISConstitution#96 (amended) — RC4: two planes for load; config:load as
    a named canonical scope.
  • CIRISPersist#777 (amended) — the root's load.ceiling key; the "node form
    withdrawn" reasoning retracted.
  • CIRISPersist#778 — CC 3.4.5 self-or-owner is not enforced at admission
    for config:*. Today anyone can attest what a node is running. This producer
    honours the rule by construction; the substrate leg is missing.

Verification

466 lib tests, release_gates 42, envelope_vocabulary_single_source 7,
field_conformance 6, compliance_map 1. Clippy --all-targets clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BSa6L2MEDAXmoJuBVUZNVg

emooreatx and others added 2 commits August 28, 2026 18:45
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/load_shed.rs Outdated
/// 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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/load_shed.rs
Comment on lines +116 to +119
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/compose.rs Outdated
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/load_shed.rs Outdated
// 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/compose.rs Outdated
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/federation_delivery.rs Outdated
Comment on lines +280 to +283
crate::load_shed::spawn(
Arc::clone(&engine),
crate::load_shed::NodePen::Engine,
load_observer_key,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/load_shed.rs
(paths::DIMENSION): DIMENSION,
"attesting_key_id": node_key_id,
"subject_key_ids": [node_key_id],
"score": 1.0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/load_shed.rs
continue;
}

match emit(&engine, &pen, &node_key_id).await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/load_shed.rs
/// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/load_shed.rs
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/load_shed.rs
// 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@emooreatx
emooreatx marked this pull request as draft August 29, 2026 00:31
@emooreatx

Copy link
Copy Markdown
Contributor Author

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.

config:* is the wrong namespace, and it is not close. CIRISServer#324 pins every config row to cohort_scope = self, and check_config_cohort_scope_self says why in terms: federation "does NOT suppress and would leave every config key directory-advertised + replicable". Structural invisibility is the point of that scope. So the namespace is either compliant and invisible to peers, or federation-scoped and in breach of its own gate. I read CC 3.1 config:{scope} as reserved for exactly this and it is — for the SEMANTICS (a node's self-report about itself). I did not check the scope contract, which is the opposite of what a peer-readable report needs.

No peer can compose it. TrustSet::bootstrap pins accord_holder and steward only; screen returns NotInTrustSet before it ever reads the load fields, and no production path in this repo pins an ordinary node key. You are also right that a_peer_can_screen_the_row_it_receives does not construct a Composer — it checks the envelope has what a screen needs, not that a screen passes. That test overclaims and I will retitle it.

Renewal would weaken the signal. With a 120s gap and 180s TTL two rows overlap for ~60s, and Composer weights self-attestations by live count: one row contributes 0.5, two contribute 1/3 each with a signed mean of 1/3. A peer thresholding at 0.5 flips Affirm→Deny while the node is still continuously stalled, then back. Renewing makes the claim quieter — the exact inverse of its purpose.

Third attempt at this, each time fixing the named defect and hitting a new structural one: admin_action refused at the write door (no delegation) → HardCaseEvent on no replication plane → now the namespace/scope contract, the trust set, and the composition weighting. That is not three bugs; it is one absent capability. A peer-readable node self-report does not exist in this substrate, and the pieces it needs are upstream and constitutional, not in this PR.

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.

@emooreatx

Copy link
Copy Markdown
Contributor Author

Retitled — this claimed "0.5.195", and 0.5.195 has since shipped as a different cut (/v1/identity states its key and kind, ciris-node as the base alias, plus the put-door gate). Leaving that title would make this read as the released work.

This remains withdrawn pending CIRISConstitution#97: a node self-report has no peer-readable route. config:* is normatively self-scoped and therefore structurally invisible, an ordinary node key screens as NotInTrustSet, and self-attestations are weighted by live count so renewal halves the signal. Three independent walls, one absent capability.

The durable output of this attempt is on main already — tests/every_emitted_dimension_is_versioned.rs, the put-door gate, which exists because a dimension with no :vN compiled, passed every unit test, and would have been refused at admission on every emission.

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.

1 participant