Skip to content

Fix gaps in recently merged Directory code found by a full sweep - #95

Draft
randomblocker wants to merge 17 commits into
mainfrom
fix/directory-sweep-followups-20261015
Draft

randomblocker wants to merge 17 commits into
mainfrom
fix/directory-sweep-followups-20261015

Conversation

@randomblocker

Copy link
Copy Markdown
Contributor

Two full reviews of the DACS Directory code merged since 2026-10-08 (#87, #88, #92, #93, #94) found places where the Directory's verdicts differ from what the DACS Standard v0.4 requires, and rules no test pinned. This PR fixes them, adds a regression test for each fix, and adds rule tests for the rules a mutation run showed were unpinned.

Each fix's test fails when the fix is reverted. The typecheck is clean at every commit, and the whole suite passes at every commit (353 tests at the tip, up from 273 on main). Mutation testing over the nine modules these findings touch dropped the surviving mutants from 1,411 to 573. The remaining groups are listed at the end.

What changed, finding by finding

Revocation (DACS-1 RB-3, RB-5, RB-6)

  • A listing whose revocation check is incomplete is no longer published as active. The check is incomplete while a candidate marker could not be read because of a transport failure, or is still waiting its turn. The catalog has no indeterminate status, so the listing is held back until a later pass completes the check.
  • A revocation verified on an earlier pass stays revoked with its binding, even if its marker later cannot be read. It is carried forward only for the same listing version.
  • Storage reads now tell a definite answer (404, 401, 403, 400, or content that cannot be an artifact) apart from a transport failure. A deleted candidate is rejected, so it cannot keep a listing held back forever.
  • A rejected marker that is re-published in place with a corrected signature envelope is examined again. Candidates are now readmitted on a hash of the whole stored value.
  • Tests: scan-fairness "a verified revocation stays revoked while its marker cannot be read or no longer verifies (RB-3)", "an unread or unexamined revocation candidate withholds the listing rather than publishing it active (RB-5)" and "a rejected marker re-published in place with a corrected signature envelope is examined again"; chain-storage "a storage read separates a definitive answer from a transport failure (RB-6)"; listing-rules "RB-3 retention carries forward only this listing version's own revoked record and binding".

Deal attribution without program names (DACS-5 §10.4.2)

  • A discovered current-profile deal is attributed through the agreementRef its buyer bundle signs: the locator, the content hash, and a co-signed agreement whose buyer is the bundle owner. Program names are no longer used. Before, deals whose producers percent-encode the colons in program names (most deals on node2) were never attributed to their seller.
  • Name-based resolution remains only on the labelled legacy SDK path.
  • Test: scan-fairness "a discovered current-profile deal is attributed through the agreementRef its bundle signs, never a program name (§10.4.2)".

Canonical signature values (CORE SIG-6)

  • Every current-profile signature value must be unpadded Base64URL with exact re-encoding. Hex, 0x hex, padded or standard Base64, whitespace and non-zero residual bits are rejected without trying other decoders. The legacy SDK profile keeps its own labelled reader.
  • The register page now converts the wallet's hex signature to the canonical value before anchoring.
  • SPEC_PIN records the Standard revision (v0.4, 4bb9e48), and the conformance vector files are vendored byte for byte.
  • Tests: sig6 (the signature-value-encoding vectors, and seven non-canonical spellings on each verifier), signature-value-rules.

Per-jobId reconciliation (DACS-5 §10.5.1)

  • A seller's verified deals now collapse to one authoritative copy per jobId: equal copies merge, a lone fully signed copy wins, and equal standing voids the jobId, whoever the buyers are. fix(dacs-directory): bounded revocation work, owner-scoped deals and separate retry classes #94 keyed this on (buyer, jobId).
  • Tests: verification "verified reputation deals collapse to one authoritative copy per jobId, whoever the buyers are (§10.5.1, BB-6)"; scan-fairness "two buyers' attributed deals with the same jobId both reach the seller's record".

Listing validation (DACS-1 §6.3.4, LR-2, DACS-3 PS-3, DACS-4)

  • A listing is published only once validity.notBefore has passed.
  • The 16 KB LR-2 cap is measured on the canonical form without the signature, not on the raw JSON.
  • The negotiate-sealed-envelope-procurement phase is added, and the full PS-3 pricing table is applied.
  • An auction's selectionRule must come from the DACS-4 enum or be a content-hash-bound rule ref.
  • revokedAt is bounded by magnitude only, as CORE requires.
  • Recognised listing members must have their defined types. That covers the deliverable (all four kinds), BundleRequirement, terms, rails, capabilities and the parameters of each phase kind, including the sealed-envelope rule that its selectionRule must equal the pricing's.
  • Tests: listing-verification "the indexer publishes a current listing only once validity.notBefore has passed", "PS-3: each negotiation pattern accepts exactly the pricing kinds DACS-3 allows", "auction pricing accepts only the DACS-4 selectionRule enum and content-hash-bound rule refs", "LR-2 caps the canonical form at 16,384 bytes, signature omitted, not the raw JSON" and "schema conformance: recognised listing members must have their DACS-1/3/4 types"; verification "standalone revocation verifier bounds a signed revokedAt to the CORE numeric profile, not to non-negative integers".

Agreement and vet records in the evidence graph (DACS-3 §8.5.2, DACS-2 §7.5 and §7.7)

  • An agreement must conform to the pinned listing: currency, price (fixed, band, fixed-price fallback, metered), rail, deliverable, pattern, role direction and commitment artifact.
  • VerifyResult and composite records must carry their required members. A VerifyResultRef's recipeVersion must match the result it points to. A composite must be bound to a session party's bundle and to the listing's requirement, and a successful vet phase or a committed session must rest on a passing record.
  • Graph artifacts are accepted up to the 128 KB DACS-5 storage soft cap. The previous 64 KiB limit rejected valid bundles.
  • Tests: current-indexer "DACS-3 §8.5.2: an agreement conforms to its listing in currency, price, rail, deliverable, pattern and role direction", "the evidence graph rejects an agreement that does not conform to its listing", "current evidence graph resolves VerifyResult authority attestations", "a vet record binds the listing's requirement and must pass for a vet phase or agreement to rest on it (DACS-2 §7.7, VPC-4)" and "a bundle within the 128 KB DACS-5 storage soft cap verifies, and one above it does not".

Browser and API decisions

  • The in-browser deal verdict moves unchanged into browserDealVerdict and is tested input by input.
  • The artifact proxy's locator guard is tested: no ref other than a native storage locator reaches the node.
  • The deal page shows a delivery attestation only when the record's storage owner is the expected seller, and no longer says it is signed before the check runs.
  • Tests: verification "the browser deal verdict is strict only with matching parties, signatures, refs and a registered copy role"; artifact-route; deal-page.

Smaller fixes

  • SB-2 ties now break on the lower evidence hash (current-indexer "SB-2: a settlement id cited at the same time by two jobs counts for the lower evidence hash").
  • The node's 400 INVALID_REQUEST is terminal (scan-addresses "storage failures are classified into public-safe operational causes").
  • The storage-limit comments are corrected, and the README now documents ARTIFACT_REJECTED, the pinned upstreams and the node's storage-read contract, and states the binding ceiling as it is. The notes calling the SDK repository private are removed, and the stale import comments are corrected.

Rule tests from the mutation run

  • New files: listing-rules, revocation-vectors, evidence-graph-rules, bundle-binding-rules, indexer-binding-rules, legacy-deal-rules, identity-rules, reputation-rules and signature-value-rules.
  • Each case mutates one field of a validly signed fixture and signs it again, so only the rule under test can reject it.
  • The legacy SDK deal path gets its first positive end-to-end test.

Wire and behaviour changes

There are no external users yet, but these are visible:

  • Listings, revocation markers and current-profile artifacts with a hex or padded signature value no longer verify. Republish them with the canonical value; their content hash is unchanged.
  • A listing whose revocation check is incomplete is held back from the catalog until a pass completes the check.
  • Listings that fail the schema or PS-3 rules, or whose notBefore is in the future, are no longer published.
  • Two buyers' fully signed, divergent deals for one seller and jobId now void that jobId.
  • DealRecord gains fullSignatureStanding, each settlementTxIds entry gains evidenceHash, and discovered deals gain agreementLocator.
  • New exports: readAnchorResult (chain), hasPendingRevocationCandidates (store), browserDealVerdict (bundlePolicy), agreementListingConformance and normalizedHash (evidenceGraph), and the signatureValue module.
  • The content_hash of revocation_candidates now hashes the whole stored value, so existing rejected candidates are examined once more.

Not in this PR

  • Bounded work. There is no total across signers on stored bindings, which are re-verified every pass (reindexCore.ts:143-146, bundleBinding.ts:108-127). The revocation-candidate queue has no cap (store.ts:127, 257-271; scan.ts:186-189, 552). Because an incomplete check now holds a listing back, a flood of junk candidates can delay its publication.
  • §8.5.2 checks 5 and 6, and CA-7. These need the CommitmentRecord, which a bundle does not reference and which is addressed only by name.
  • Fractional revokedAt. The pinned SDK canonicaliser rejects every fraction.
  • Build pins. The CI actions are pinned by tag rather than by SHA, and the packaged-SDK fast path in scripts/setup-sdk.sh does not check the SDK revision.
  • The legacy SDK deal path. It is reached only when neither bundle copy carries bundleVersion: "1", but the pinned SDK writes "1", so its deals take the current path and fail there.
  • Remaining mutation groups. These are listed in the fix report with exact locations.

🤖 Generated with Claude Code

randomblocker and others added 17 commits October 14, 2026 21:52
…nforming path (SIG-6)

CORE §B.7 SIG-6 says a signature value is unpadded Base64URL. A verifier
must reject every other spelling and must not auto-detect by trying
decoders. The current-listing, revocation-marker, evidence-graph component
and bundle verifiers all accepted hex, 0x hex, padded and standard Base64.
Only BundleBinding was strict.

- Add src/catalog/signatureValue.ts: one strict decoder used by every
  current-profile envelope, the declared-encoding legacy import from the
  standard, and an explicit legacy reader used only by the
  legacy-sdk-v0.1 paths (draft SDK listings and the SDK bundle policy).
- The register page now re-encodes the wallet's hex listing signature
  into the canonical value before anchoring, as SIG-6 requires of
  producers. Listings already anchored with a hex value must be
  re-published; their content hash does not change.
- Per-claim presentation signatures are bare strings, not SIG-6
  envelope values, so they keep the existing decoder.
- Vendor the pinned signature-value-encoding and revocation-binding
  vector files with a Directory SPEC_PIN (v0.4, 4bb9e48). Switch the test
  helpers to base64url and pin the seven non-canonical spellings on each
  verifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-3 rules

- Publish a listing only once validity.notBefore has passed. Validation
  step 3 is notBefore ≤ now ≤ notAfter; only notAfter was checked.
- Add the negotiate-sealed-envelope-procurement phase kind and apply the
  full PS-3 matrix. negotiate-fixed-price takes fixed, negotiable or
  metered pricing; rfq takes negotiable or metered; both sealed-envelope
  kinds take auction. Negotiable pricing with fixed-price negotiation and
  auction pricing with procurement were rejected before.
- Measure the 16 KB LR-2 cap on the canonical form with the signature
  omitted, not on the raw JSON with its signature.
- Accept only the DACS-4 auction selectionRule enum or a
  content-hash-bound rule-ref; any string was accepted before.
- Bound a revocation marker's revokedAt by the CORE numeric profile.
  Negative values are numbers DACS-1 allows. Fractions are still refused,
  because the pinned SDK canonicaliser cannot form them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cation check is incomplete

DACS-1 RB-5 and RB-6 say an incomplete RB-4 check or a transport failure is
indeterminate, never absent. RB-3 says a revoked record keeps its binding.
The indexer had only two outcomes: if a verified marker could not be read
on a pass, or a valid marker sat behind unread or unexamined candidates, it
republished the listing as active with a fresh catalogObservedAt. Readers
of the catalog then accepted new sessions on a revoked listing.

- Compute a three-state RevocationCheck per listing. revoked: a marker
  verifies this pass, or the previous record was revoked; a later pass
  never withdraws a verified revocation. indeterminate: a candidate was
  unread or is still pending. absent: every candidate was examined and
  rejected.
- The catalog schema allows only active and revoked, so an indeterminate
  listing is withheld for that pass. It still resolves past sessions and
  identity.
- Add readAnchorResult, which separates a definitive answer (404, 401,
  403, 400, or non-object or oversized content) from a transport failure.
  Only a transport failure leaves a candidate unread. A deleted or hidden
  candidate is rejected, so it cannot keep a listing withheld for good.
- Update the README's revocation paragraph, which still described a
  16-candidate retention window that no longer exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ored value changes

A rejected candidate went back to pending only when its content hash
changed. The content hash omits the signature envelope, so a marker
re-published in place with a corrected envelope was never examined again.
That includes the canonical re-encoding SIG-6 asks producers to publish.
The same marker at a fresh locator revoked the listing at once.

Admit candidates with a hash of the whole stored value, envelope included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…signed agreementRef, not program names

DACS-5 §10.4.2 makes a StorageProgram name an opaque, colon-free write input:
"A consumer MUST NOT treat the name as a resolution key." The scanner found
a discovered deal's agreement under the raw name dacs3:agreement:<jobId>.
Conforming producers percent-encode that name (199 of the 209 agreement
programs on node2 do), so their deals were never attributed to a seller and
never counted.

- A current-profile buyer bundle now resolves its agreement through the
  agreementRef it signs. The scanner reads that locator, checks the
  content hash, and requires a co-signed current agreement whose buyer is
  the bundle owner. The deal records the agreement locator.
- Name-keyed duplicate invalidation in reindexCore applies only to deals
  attributed by name. Legacy SDK bundles keep resolving by their session
  anchor names on the labelled compatibility path.
- Update the scanner header and the README, which described name-based
  classification for every profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…10.5.1)

DACS-5 §10.5.1 says the deriver "MUST collapse the input to one
authoritative bundle per jobId before partitioning". Canonically equal
copies collapse, a fully signed copy takes precedence over lesser-signed
divergent ones, and equal standing voids the jobId (BB-6). #94 keyed the
dedupe on (buyer, jobId), so two buyers' fully signed completed copies for
one seller and jobId counted twice. A conforming deriver gives zero.

- dedupeVerifiedDeals keeps one record per bundle ref and per (jobId,
  bundleContentHash). Among divergent copies of a jobId it keeps a lone
  fully signed one; any other verified copy stays listed with
  reputationEligible false.
- DealRecord gains fullSignatureStanding, set from the BB-6 inspection on
  the current path and from the SDK signature verdicts on the legacy path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ACS-1, DACS-3 and DACS-4 types

DACS-1 validation step 1 is schema conformance, and LR-2 rejects a listing
that fails any step. The verifier treated the deliverable, buyerRequirement,
terms, rails and most phase parameters as any object, so a correctly signed
listing with a malformed shape was published.

- Check DeliverableSpec (all four kinds, including external),
  BundleRequirement and ClaimRequirement, ListingTerms, PaymentRailRef,
  requiredCapabilities, the optional offering members and validity.notAfter.
- Check per-kind phase parameters: negotiate-rfq needs maxTurns >= 2 and a
  positive timeoutSec; both sealed-envelope kinds need commitDeadline,
  a revealWindow of at least 60 seconds, the auctionMode their kind requires,
  and a selectionRule equal to the auction pricing's (DACS-3 §8.4.3);
  pay-* steps need parameters; rate's `required` is a boolean.
- Unknown members stay open-world, as before (SIG-5, §11.1.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…graph to DACS-3 §8.5.2 and DACS-2 §7.7

The evidence graph accepted a co-signed agreement once its commitment
artifact matched the listing pipeline. It did not check the currency,
price, rail, deliverable, pattern or role direction against the pinned
listing, so a correctly signed agreement on other terms could feed a
completed deal. Vet records missed required members, their VerifyResultRef
recipeVersion was never compared, and a record that did not pass could
back a successful vet phase.

- Add agreementListingConformance: §8.5.2 checks 1-4 and 7-9 in order,
  with CD-1 decimal arithmetic, the inclusive half-up band, the metered
  MTR-1..5 total, the fixed-price fallback of an RFQ step that declares
  fixedPriceFallback, and SE-8 role direction for procurement. Checks 5
  and 6 need the CommitmentRecord's committedAt, which a bundle does not
  reference, so they are not evaluated.
- Require VerifyResult's recipeVersion, method, reason and fetchedAt, and
  the composite record's bundleHash, requirementHash and generatedAt.
  Bind evaluatedParty and bundleHash to a session party, requirementHash to
  the listing's buyerRequirement, and each VerifyResultRef's recipeVersion
  to the resolved result.
- A vet phase reported ok must rest on a passing record (VPC-4), and a
  session that reached its agreement needs a passing record for each party
  it vetted.
- Admit graph artifacts up to the 128 KB DACS-5 storage soft cap; 64 KiB
  rejected valid bundles. Correct the storage-limit comments: the node and
  demosdk 4.0.13 accept 1 MiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roxy's locator guard

The in-browser strict-pass, limited and fail verdict lived inside the
VerifyDeal component, and the artifact proxy's only path guard
(chain.ts, /^stor-[0-9a-f]{40}$/) had no test, so either could regress
silently.

- Move the verdict into bundlePolicy.browserDealVerdict, unchanged, and
  test each input: expected and registered parties, signatures, strict
  refs, the registered copy role, and a missing registration.
- Test that the artifact route forwards only a native storage locator to
  the node: traversal, query, uppercase, short, long and padded refs never
  reach fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… hash

DACS-4 SB-2 assigns a settlement transaction cited by two jobs to the
earliest evidence observation, "ties broken by lower evidence hash". The
deriver broke ties by jobId. Each settlement id now carries the content
hash of the evidence that cites it, ties go to the lower hash, and jobId
only breaks a tie between equal hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wer as terminal

The node answers 400 INVALID_REQUEST for an address no program can occupy
(kynesyslabs/node routes.ts). The scanner classed it as the transient
STORAGE_RPC_UNAVAILABLE and retried it. Class it with STORAGE_NOT_FOUND;
an unexplained 400 stays retryable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on the deal page

The deal page read the delivery attestation at the seller's derived
address and said "The seller signed this delivery attestation" before any
check ran. The node does not check that an address matches its
derivation, so another writer's record there was shown as the seller's.
Show the card only when the record's storage owner is the expected
seller, and say its signature still has to be checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…otes

- README: name the Standard, SDK and node revisions the verdicts depend on
  and the node's storage-read contract; list ARTIFACT_REJECTED and its retry
  class; describe the binding ceiling as it is since #94 (32 per signer of
  each job and role, no total across signers).
- SPEC_PIN: the did:demos:agent profile is Ed25519, so listings signed with
  ecdsa-secp256k1 or sr1-aggregate are not verified.
- The SDK repository is public; the token is only for a private mirror
  (README, setup-sdk.sh, CI).
- The sessionAnchorName import comments: upstream exported it after the
  pinned SDK_REV.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n sweep left untested

A mutation sweep of the listing verifier and indexer showed most listing
acceptance and revocation rules could be removed without a failing test.

- listing-rules: every DACS-1 identity, offering, pricing, pipeline and
  validity rule and every schema member at its boundary, the per-claim
  presentation, the legacy-sdk-v0.1 envelope, the indexer's publication
  gate (readable, owner-bound, hash-pinned, in its window), and RB-3
  retention scoped to the same listing version.
- revocation-vectors: run the vendored revocation-binding-v0.3 vectors
  through the Directory's verifier with the vector's key, and pin RB-4's
  tuple and envelope checks with markers that are validly signed, so only
  the rule under test can reject them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The current-profile evidence graph and the BB-4..BB-7 binding path had
hundreds of surviving mutants on shape, reference, signature and party
checks.

- evidence-graph-rules: one case per DACS-5 bundle shape, party,
  signature and fault-attribution rule, per top-level and nested reference
  rule, per DACS-3/DACS-5 artifact shape and signature rule, DACS-2 vet
  records, CA-5/CA-7 bindings, payee-bound payout coverage and the §8.5.2
  inputs. Each mutated artifact is signed again so only the rule under
  test can reject it.
- bundle-binding-rules: BB-4 members and envelope, the per-signer stored
  ceiling and its overflow, and BB-5..BB-7 side resolution.
- indexer-binding-rules: the indexer resolves each side only through its
  role holder's bindings, records the deal unverified otherwise, and
  reports full signature standing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… test and pin its strict policy

The legacy-sdk-v0.1 deal path had no positive test, so any of its guards
could be removed silently. The browser verdict uses the same policy.

- An SDK-profile deal verifies end to end through the indexer, and each
  copy must be strict: both copies, the registered roles, both
  signatures, resolvable evidence and no outsider signature.
- The strict ref policy, referenced-artifact signatures, the bundle's
  signature entries, registered deal and copy-role binding, the browser
  verdict's registered owners and the pinned cancellation terms, one case
  per rule.
- A copy carrying the current bundleVersion "1" goes to the current
  verifier and is never retried as legacy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d SIG-6 decoder rules

- identity-rules: a claim counts toward the reported tier only through a
  hash-bound, signed, passing VerifyResult from a trusted signer under a
  live, version-pinned recipe, inside its validity window, with its
  authority attestation; each rule withholds the tier when it fails.
- reputation-rules: eligibility, the windowing basis, SB-2, neutral
  outcomes and rates, bundle references, seller ratings, observed volume
  and ST-10 neutrality.
- signature-value-rules: SIG-6 encoding and decoding at every length, the
  declared legacy import and the legacy-sdk-v0.1 reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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