Skip to content

fix(directory): open-world listing shape — tolerate unknown top-level fields (SIG-5 / §11.1.2)#16

Open
cX3po wants to merge 2 commits into
mainfrom
cx3po/dacs-listing-openworld
Open

fix(directory): open-world listing shape — tolerate unknown top-level fields (SIG-5 / §11.1.2)#16
cX3po wants to merge 2 commits into
mainfrom
cx3po/dacs-listing-openworld

Conversation

@cX3po

@cX3po cX3po commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Problem

dacs-directory listingVerification.ts rejected any listing whose top-level keys were not in a frozen 13-key allowlist (closed-world):

[...Object.keys(scope)].some((key) => !CURRENT_LISTING_KEYS.has(key)) ||   // → return null

This contradicts the §11.1.2 additivity contract and the older-reads-newer guarantee (CORE.md): a later minor may add a top-level listing field, and an older reader must still consume the newer-minor artifact. SIG-5 (preserve-unknown) is the mechanism — a verifier MAY ignore the meaning of unknown fields but MUST NOT strip them.

It is latent on v0.2 (today's additions are nested), but the first spec minor that adds a top-level listing field would make every such listing rejected by every directory still running the allowlist.

Fix (validator-only)

Remove the closed-world reject clause (and the now-unused allowlist). Signature integrity is unchanged: verifyListing() already hashes the whole scope via contentHash(scope) (raw minus signature), so unknown fields stay signature-bound and a field injected after signing still fails the hash. The validator reads only the recognised fields, so unknown fields are neither indexed nor interpreted.

Tests

A vector pair in verification.test.ts:

  1. accept — an unknown additive top-level field present at signing is accepted as the dacs-v0.1 profile;
  2. bind — the same field injected after signing is rejected on the whole-scope hash (proves the relaxation did not weaken binding).

Receipts: full suite 35/35, tsc --noEmit clean, and the acceptance vector provably fails without the fix (closed allowlist → null).

Independently surfaced during an external review of the directory; verified against the spec and the RI before filing.

… fields (SIG-5 / §11.1.2)

listingVerification rejected any listing whose top-level keys weren't in a
frozen 13-key allowlist. That contradicts the §11.1.2 additivity contract and
the older-reads-newer guarantee (CORE.md): the first spec minor that adds a
top-level listing field would make every such listing rejected by every
directory running the closed allowlist. SIG-5 (preserve-unknown) is the
mechanism — a verifier MAY ignore the meaning of unknown fields but MUST
preserve their bytes.

Fix is validator-only. verifyListing() already hashes the whole scope
(contentHash over raw-minus-signature), so unknown fields stay signature-bound
and a field injected after signing still fails the hash — signature binding is
unchanged. The validator reads only the recognised fields, so unknown fields
are neither indexed nor interpreted.

Vector pair added: (1) an unknown additive top-level field present at signing
is accepted as the dacs-v0.1 profile; (2) an unknown field injected AFTER
signing is rejected on the whole-scope hash. Full suite 35/35, typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cX3po

cX3po commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Credit for surfacing this belongs to the independent directory review that flagged the closed top-level allowlist — we verified it against the RI + spec and took the fix so it doesn't sit as a duplicate open issue.

How we checked it before filing:

  • Normative anchor — the airtight one is §11.1.2 (additivity) + the older-reads-newer guarantee (CORE.md); SIG-5 (preserve-unknown) is the mechanism. Cited that way because the RI never literally strips — it rejected pre-hash — so the additivity contract is the cleaner citation.
  • Crypto safety — confirmed verifyListing hashes the whole scope (contentHash(scope)), so relaxing the validator leaves signature binding untouched; a field injected after signing still fails the hash. The added vector pair proves both directions.
  • Regression guard — the acceptance vector provably fails against the pre-fix code (closed allowlist → null), so it can't silently rot.

One thing worth a second pair of eyes: identityVerification.ts has an analogous enumerated set (INSTITUTIONAL) for claim types — worth checking whether it has the same closed-world/forward-compat behavior one layer down. Happy to follow up if the steward wants it in scope.

@mj-deving mj-deving left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed this against origin/main, including the identityVerification.ts question from the PR comment.

Validation I ran locally on head 2d3c92b21f46e79dc7e7175c45da2cbdb5b265a5:

  • bun run test -> 35/35 pass
  • bun run typecheck -> pass
  • autoreview --mode branch --base origin/main -> clean, no accepted/actionable findings

My read on the second-pair-of-eyes question: identityVerification.ts is not the same closed-world acceptance bug. The INSTITUTIONAL set is only a tier-elevation gate after a claim has already passed verifiedBy / recipe / authority checks; an unknown scheme does not make the listing invalid, it just cannot silently elevate to institutional unless the Directory policy knows that scheme is institution-grade.

So the PR's open-world listing change looks bounded: unknown listing fields remain whole-scope signature-bound, and the injected-after-signing negative covers the important safety edge.

No findings from me.

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.

2 participants