Skip to content

fix(verifier): neither an event's unsigned id nor the events' order decides EvidenceVerifier's verdict; docs: they are uncommitted metadata (E11b/E11c, stacked on #341) - #560

Merged
LamaSu merged 18 commits into
masterfrom
docs/evidence-subject-binding-uncommitted-metadata
Oct 6, 2026
Merged

LamaSu merged 18 commits into
masterfrom
docs/evidence-subject-binding-uncommitted-metadata

Conversation

@LamaSu

@LamaSu LamaSu commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Based on master (#341 has merged). DRAFT. The operator makes every merge.

This PR fixes N118 (board row; the steward states the property in bus #6161). Everything that decides a verdict comes from authoritative, owner-bound task state: the tier, the proof pipeline and the requirements. Nothing comes from the worker's submission or from another caller's task creation. Every pipeline either enforces the tier or refuses. The verifier reads no unsigned field.

It began as the follow-up for E11b's one LOW on #341: an event's id and the order of events are not committed. Cross-family review then found HIGHs in the consumers, and each round fixed them: E11c, E11d, E11e and E11f were all DO-NOT-SHIP. E11g, a full delta round, is in review.

EvidenceVerifier (packages/verifier)

  • Only a committed string payload.stepId covers a step, never the unsigned event.id (E11c).
  • Start, completion and power events are chosen by committed fields (timestamp, then hash): the latest start and the earliest completion. Array position decides nothing (E11c).
  • The evidence a tier requires comes only from options.acceptedTier, the tier the caller knows from authenticated state. Without it, the verdict fails closed. The bundle's own assuranceTier is not signed, so it is never read, not even to compare (E11d, E11e).

TMPValidatorBridge (packages/verifier)

  • validate(envelope, context): the context (acceptedTier, proofType) is the task's own record, never the worker's envelope.
  • A submission on any pipeline other than the task's is refused (task_pipeline).
  • zk_proof and merkle_commitment refuse (tier_unenforceable), because neither evidences the events that every tier requires. This also ends the empty-path Merkle accept.
  • Bittensor and oracle first verify the bundle locally at the task's tier. The bundleHash must be the bundle's own recomputed hash. The network then receives only the hash-committed data as canonical JSON, so it can only add a refusal (E11f).
  • No worker field chooses a tier: proof.requiredTier is never read.
  • A verdict proves the bundle's INTEGRITY, not its ORIGIN. No kernel or device signature is checked yet; that is row N124. Nothing consumes a TMP verdict today, and any consumer must wait for N124.

The TMP task route (packages/gateway)

  • Durable and write-once (E11f). Each task is one file on the gateway's volume, beside pcc.db, named by the sha256 of the milestone id. It is created with O_EXCL and link(), so a second creation is refused (409), across restarts too. The store is injected (server.ts wires FsTmpTaskStore); with none, the routes answer 503. The boundary: one gateway, one volume, as for pcc.db.
  • A tier from 0 to 3 is required. A benchmark task's pipeline must be one that enforces a tier, so ZK and Merkle get 400.
  • Owner-bound. With an injected milestoneOwner resolver, only the milestone's owner creates its task.
  • No resolver yet. An API key's operatorId is self-typed until WP-A, so the gateway has none. Per the steward's ruling on #6182, only an admin creates a task meanwhile, and everyone else gets 403 admin_only:
    • An admin is an API key with the literal admin scope (hasAdminScope).
    • The wildcard does not count, because self-service sign-up mints it.
    • The live consequence is operator item 134.
  • Validation uses only the task's own tier and pipeline.

Documentation

subject-binding.ts gains a section, WHAT THE RESULT DOES NOT COMMIT. The bundle commits the sorted MULTISET of event hashes; neither id nor order is committed.

Tests and evidence (at e881935)

  • One generic test covers every uncommitted bundle and event field, taken from the schema (optional ones included). It injects extra fields and reorders events and payload keys. The verdict never changes; a bridge-level copy shows the same for both networks, down to their input bytes.
  • Bridge and route tests cover every refusal, with positive controls; hasAdminScope has its own tests.
  • verifier 627; gateway 210 files, 3760 passed and 13 skipped; spec 1212; tsc clean.
  • Mutants killed: E11c 7 of 7, N118 12 of 12, E11e 22 of 22, E11f 41 of 41 (21 verifier and bridge, 20 route and store).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst

LamaSu and others added 4 commits October 3, 2026 15:18
…ata of a verified bundle (E11b LOW)

E11b (astra on #341 @f249eb48, SHIP) left one non-blocking LOW. The verifier hashes only type, timestamp,
source and payload, but returns each event's unsigned `id`, in input order. So the same signed bundleHash
yields differently identified or ordered results and archives. It cannot create a false binding or a wrong
settlement, because neither participates in any acceptance decision.

The review offered two follow-ups: document both, or drop `id`. This takes the first, which changes no
behaviour for consumers that archive or display ids:
- the header gains "WHAT THE RESULT DOES NOT COMMIT": the bundle commits the SET of event hashes, and id and
  order are not committed, so identify an event by its hash and order by committed fields;
- the result type's doc points to it;
- a test pins the property: relabelled ids plus a reversed order still verify against the same bundleHash,
  and come back exactly as given.

Stacked on #341 (feat/evidence-subject-binding @f249eb48).
spec 50 files, 1212 tests; tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…ecides EvidenceVerifier's verdict (E11c HIGH)

E11c (astra on #560 @4ad48f4f): DO-NOT-SHIP. The documentation of uncommitted metadata was correct, but a
known consumer relied on both uncommitted properties. Both were REPRODUCED at 4ad48f4 with real hashes
(hashEvent, hashBundle), so the bundle and event integrity checks pass.

1. Step completeness fell back from the committed payload.stepId to the unsigned event id. A
   workflow_step_completed event without payload.stepId: verified with id "unrelated", the verdict is
   INVALID (the declared step has no trace); relabelling ONLY the id to "step-1" gives VALID. Same
   bundleHash, same event hashes. Fix: a trace comes only from a committed string payload.stepId; the id
   never counts.
2. Lifecycle selection used Array.prototype.find, so the first match by array position won. A completion
   before the start plus one after it gave INVALID or VALID depending on the order. The power summary
   checked depended on order too. Fix: selectByCommittedOrder picks by committed fields (timestamp, then
   hash): the LATEST start and the EARLIEST completion, so a positive duration means every completion
   follows every start, in any order; and the earliest power summary. An unparseable timestamp sorts where
   the duration check fails closed. With one start and one completion, the behaviour is exactly as before.

The LOW: "SET" misstated the commitment, because hashBundle keeps duplicates. subject-binding.ts now says
"sorted MULTISET", and its test asserts that a duplicated event changes the bundleHash.

New evidence-verifier-uncommitted.test.ts: relabel invariance with a VALID positive control on a committed
stepId, three orders of a pre/post-start completion, and two orders of power summaries. All three tests
fail at 4ad48f4.

verifier 30 files, 595 tests, tsc clean. spec 1212. gateway 3734 passed, plus the known completion-real-tier
load flake (passes alone). gateway tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
A completion between two starts fails closed in every order, and two power summaries with the same timestamp
are told apart by hash, not position.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…nding itself (E11c)

Its invalid result also came from tier 1's missing power summary, so mutant M7 (the latest completion counts)
survived. It now asserts execution_duration_positive fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
@LamaSu LamaSu changed the title docs(spec): an event's id and the events' order are uncommitted metadata of a verified bundle (E11b LOW, stacked on #341) fix(verifier): neither an event's unsigned id nor the events' order decides EvidenceVerifier's verdict; docs: they are uncommitted metadata (E11b/E11c, stacked on #341) Oct 3, 2026
@LamaSu
LamaSu changed the base branch from feat/evidence-subject-binding to master October 3, 2026 22:44
LamaSu and others added 13 commits October 3, 2026 16:02
…ed state, never a worker's bundle or proof (E11d HIGH, N118)

E11d (astra on #560 @eac51ba5): DO-NOT-SHIP. The id and order routes are CLOSED, but the bundle's unsigned
assuranceTier still chose the evidence required. REPRODUCED at eac51ba: a tier-1 bundle without a power
summary is INVALID, and the same signed bundle relabelled assuranceTier 0 is VALID (same bundleHash). The
steward asked to fix the PROPERTY: the verifier reads nothing unsigned.

What EvidenceVerifier consumes, enumerated:
- bundleHash, recomputed from the events: committed;
- each event's type, timestamp, source and payload, via the verified event hash: committed;
- event.id: labels findings only, display (E11c already removed it from decisions);
- events' order: no longer decides anything (E11c);
- bundle.assuranceTier: NOT committed, so it now chooses nothing;
- id, jobId, stepId, kernelId, createdAt, kernelSignature: never read.

Fix:
- EvidenceVerifier: the tier's required evidence comes from options.acceptedTier, which the caller takes
  from authenticated state (the accepted plan or program, or its own task record).
  - A bundle that claims another tier is rejected (critical "assurance_tier_accepted").
  - Without an accepted tier, or with one that is not 0-3, the verdict fails closed, and no requirement is
    chosen at all.
- TMPValidatorBridge: validate(envelope, context) takes the caller's ValidationContext { acceptedTier }.
  - sensor_evidence passes it to the verifier.
  - The bittensor and oracle routes used the worker's proof.requiredTier, the same class. Now they use the
    accepted tier and refuse a proof that is missing one or claims another.
- tmp-tasks route: a TMP task records its acceptedTier at creation, from the poster's assuranceTier
  (validated 0-3), and the proof route passes the task's own value. MilestoneProcurement gains
  acceptedTier?: AssuranceTier.

Tests:
- ONE generic test mutates every uncommitted bundle field (plus an injected unknown one), every uncommitted
  event field, and the order, on a VALID and an INVALID sealed bundle (over 40 variants each). The verdict
  never changes.
- Without an accepted tier: invalid, and identical for every claimed tier. A mismatching claim is rejected,
  and a non-tier acceptedTier is refused.
- E11d's reproduction now holds.
- Bridge: missing or mismatching tiers are refused on every route.
- Existing bridge and E11c tests pass the accepted tier their scenarios imply.

verifier 30 files, 607 tests. spec 1212. gateway 207 files, 3735 tests. tsc clean for gateway, verifier
and spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…ted tier's requirements (N118)

Mutant M8 (the requirements follow the bundle's own tier) survived, because the mismatch rejection already
made the result invalid. The test now pins that the tier_requirement findings equal the accepted tier's,
whatever tier the bundle claims.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…-bound task state; every pipeline enforces the tier or refuses; the verifier reads no unsigned field (E11e, N118)

E11e (astra on #560 @cde080d3): DO-NOT-SHIP, with 2 HIGH and 1 MEDIUM, all REPRODUCED at cde080d:
- MEDIUM: the verifier still read the unsigned bundle.assuranceTier, in the mismatch check. With
  assuranceTier in the generic mutation loop, a VALID bundle became INVALID.
- HIGH 1: the worker chose the pipeline, and an empty-path Merkle proof passed. merkle_commitment with
  merkleRoot === leaf, path [] and indices [] validated TRUE against a tier-3 task requiring
  sensor_evidence.
- HIGH 2: the task tier was not owner-bound. Two creations for one milestone, at tier 3 then tier 0, both
  returned 201, and the task read tier 0.

The steward restated N118 end to end (#6161): everything that decides a verdict comes from authoritative,
owner-bound task state, every pipeline enforces the tier or refuses, and the verifier reads no unsigned field.

Fix:
- EvidenceVerifier: bundle.assuranceTier is not read at all. The required evidence is the accepted
  tier's only, and a missing or invalid accepted tier fails closed.
- TMPValidatorBridge: ValidationContext gains proofType, the task's pipeline. A submission without it, or
  on another pipeline, is refused (task_pipeline) before anything is verified. zk_proof and
  merkle_commitment now REFUSE (tier_unenforceable): every tier 0-3 requires evidence events those proofs
  cannot show. That also ends the empty-path Merkle accept. The two unreachable private validators are
  removed, and the constructor is unchanged.
- tmp-tasks route: task state is authoritative and owner-bound.
  - An injected TmpTaskRouteOptions.milestoneOwner(milestoneId) names the milestone's poster. Creation
    needs an authenticated caller (401) whom the resolver names as the owner (403 otherwise; a resolver
    that throws or answers null is no owner). Creation is write-once (409) and requires a tier (400).
  - The store is per app instance. Validation passes the task's own tier and pipeline.
  - WITHOUT a resolver, which is the live gateway today, creation returns 503: the routes FAIL CLOSED
    until a milestone-to-poster resolver is wired (decision request #6182).

Tests:
- the generic test covers EVERY bundle field, assuranceTier included;
- the bundle's claimed tier never changes a verdict;
- bridge tests carry the task's own context; ZK and Merkle are refused, even with genuine proofs;
- pipeline mismatch and a missing pipeline are refused;
- tmp-tasks-authority.test.ts: fail-closed without a resolver, unauthenticated, owner-only and write-once
  creation, the tier required, and a Merkle submission against a sensor task refused at the route.

verifier 30 files, 609 tests. spec 1212. gateway 208 files, 3740 tests. tsc clean for gateway, verifier
and spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…rifier at the task's tier (E11e)

Positive control for the owner-bound route: a sensor_evidence submission against a tier-2 sensor_evidence
task passes the pipeline check and is judged on tier 2's requirements (its camera requirement), not on
the bundle's claimed tier 0. It kills the mutant that drops the task's tier from the validation context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
… what E11e made true

The header listed zk_proof and merkle_commitment as validating through their services; both now refuse
(tier_unenforceable), and the pipeline is the task's own. The sensor path's comment still said the
verifier rejects a bundle claiming another tier; it no longer reads the bundle's tier at all.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…in creates a TMP task (#6182)

The steward's ruling on #6182 (the N55 precedent): fail closed with an admin exception. Without an owner
resolver, the route refuses every caller (403 admin_only) except one its injected isAdmin answers exactly
true for; a throwing or non-true answer is no admin. With a resolver wired, only the owner path applies.
Write-once, per-app state and validation from the task's own tier and pipeline are unchanged.

The gateway wires isAdmin to hasAdminScope: an API key listing the literal "admin" scope. The wildcard
does not count, because self-service sign-up mints every key with ["*"]. The resolver's doc says to wire
it only with a proven caller principal (WP-A), since an API key's operatorId is self-typed today.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…s with WP-A #326 in either order

The tests appended to scope-checker-money-path.test.ts conflicted with #326's edits to that file. In their
own file they assert only what holds under both scope parsers: master's getCallerScopes and #326's
parseScopeColumn, which refuses CSV (so no CSV case here).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…itted data; no worker tier is read (E11f HIGH 2, MEDIUM 3)

E11f HIGH 2, reproduced at a121fe1: one bundleHash with fabricated events (fake hashes) passed both network
pipelines, and empty events failed. The mock oracle and MockMiner only compared the JSON's own bundleHash
with the supplied string. Now, before any network:
- bundleData must parse to an evidence bundle that EvidenceVerifier accepts at the task's tier (event and
  bundle hashes recomputed, the tier's required events, the consistency checks);
- the supplied bundleHash must be that bundle's hash;
- the network receives only what the hash commits, as canonical JSON: the bundle hash, and each event's
  type, timestamp, source, payload and hash, ordered by timestamp then hash. No bundle field, event id,
  array order or payload key order reaches it, and its answer can only add a refusal.
The boundary: committed means hash-committed; no kernel signature is checked, as on sensor_evidence.

E11f MEDIUM 3, reproduced: requiredTier 0 on a tier-2 task was refused. acceptedTierOf no longer reads
proof.requiredTier at all. TIER_ENFORCING_PIPELINES names the pipelines that can enforce a tier, for task
creation. The oracle tests use sealed bundles; their old fixture is the reproduction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…the schema, optional fields included (E11f MEDIUM 4)

E11f MEDIUM 4, reproduced at a121fe1: a verifier that raised a critical finding whenever
sessionKeyAuthorization was present kept the generic test green, because the fixture never carried the
optional field. The test now enumerates EvidenceBundleSchema.shape and EvidenceEventSchema.shape, tries
sessionKeyAuthorization with a well-formed delegation too, and reorders each payload's keys (committed
only through its canonical form).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…olume; creation refuses pipelines that can't enforce a tier (E11f HIGH 1, MEDIUM 5)

E11f HIGH 1, reproduced at a121fe1: two app instances both created m-1 (tier 3, then tier 0), because the
store was a per-app Map, and a restart forgot every task. The steward's ruling on #6309, option (d):
- FsTmpTaskStore keeps one file per milestone on the gateway's volume, beside pcc.db, named by the sha256
  hex of the milestone id (no path traversal). The record is written to a temporary file opened with O_EXCL,
  synced, then hard-linked to its name; link() fails with EEXIST, so a second creation is refused across
  restarts and no reader sees a partial record. A record that isn't its milestone's is refused on read.
- The store is injected: the gateway wires FsTmpTaskStore (server.ts, source-pinned), and tests use
  MemoryTmpTaskStore. With no store, or one that fails, the routes fail closed (503).
- Lifecycle status (claimed, completed) is this process's view only and decides nothing.
- The boundary: one gateway on one volume, as for pcc.db itself.

E11f MEDIUM 5, reproduced: a zk_proof task was created (201) and every proof then refused. Creating a
benchmark task now needs a pipeline in TIER_ENFORCING_PIPELINES (400 pipeline_unenforceable otherwise).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…y move money on it before N124

The steward's ruling on #6358/#6360: a hash commitment proves the bundle's integrity, not who produced it.
Origin (each pipeline verifies the bundle's signature against the registered key of the kernel or device
bound to the task's job) is row N124, after #560. No money or reputation path consumes a TMP verdict today
(#6369), and the route now says any future consumer must wait for N124.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…, whoever asks (S1 survivor)

The route mutant that dropped the store check survived: the try/catch around createOnce still answered
503 for an admin's valid request, so the property held, but a non-admin then got 403 and a bad body 400.
The test now pins the documented order: a missing store is named first, for any caller and any body.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…fresh full CI run (steward #6167)

A plain merge: its tree equals git merge-tree of e881935 (E11g SHIP) and master. The fold
#560 + ae69fa6 + #574 was built and tested first (verifier 627/627, gateway 4595 passed, 0 failed);
this tree differs from it only by #571's one kernel test file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…rent master (steward wake-up, 23:2x)

A plain merge, no edits: its tree equals git merge-tree of bf97fc1 and master. CI that ran on an older
master isn't evidence: many PRs merged since.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
@LamaSu
LamaSu marked this pull request as ready for review October 4, 2026 07:30
@LamaSu
LamaSu merged commit 61926da into master Oct 6, 2026
16 of 20 checks passed

This branch had an error being deployed

1 failed deployment
trusted-checks — aef1ba1c Deployed Oct 4, 2026 by LamaSu via post-verdicts #64
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