Every Scope Resolves Now: A Systems Review of the Consent Fabric
A full engineering accounting of PCHP and the fabric that serves it: the registry's growth from 47 to 263 scopes, the resolver that went from 8 hand-mapped bindings to resolution by convention, the pseudonym fix that stopped telling subscribers who you are, the economics of a millicent handshake, and a plain ledger of everything that is still not real.

Ten days ago we published a post called We Published Forty-Seven Scopes and Built Eight. It described the least flattering number in our system: a public registry advertising 47 consent scopes while the server could actually resolve 8 of them, with every status light green the whole time.
This is the follow-up, written the way we want to write all of these: a systems review, end to end, with the numbers we actually measured rather than the numbers we remember. The rule for this document is the same rule that produced the last one. A status is not evidence. Every figure below was verified against the repositories, the deploy history, or the live endpoints in the week this was written, and where a number we used to quote turned out to be wrong or unverifiable, we say so inline.
The problem, stated from first principles
Strip away every acronym and the system answers three questions:
- Where does a fact about a person live?
- Who may read it, on what terms?
- What proves the read happened?
Everything we have built is one of those three answers, and most of our design decisions are consequences of refusing to blur them.
A fact lives in one of two stores, and the boundary between them is the most load-bearing line in the architecture. The Personal World Model (PWM) is the person's own preference document: one JSONB row per verified identity, 202 lines of service code, section-level last-writer-wins merge, capped at 64 top-level keys and 64 KB. It is plaintext by design, and its docstring insists on the point, because a shoe size and an ads opt-out do not need a vault, and pretending they do makes the vault meaningless. The Personal Knowledge Model (PKM) is the opposite commitment: 4,618 lines of service code implementing a bring-your-own-key store under aes-256-gcm, where the server holds ciphertext it cannot read. Its export path goes further: since migration 035, an export key is stored only wrapped to the connector's public key (X25519 plus AES-256-GCM), and a computed is_strict_zero_knowledge flag on every consent export is checked in the PKM service, the KYC service, and the developer API before anything moves.
Who may read a fact is the fabric: about 2,050 lines across eight modules (seven services plus the route layer), 11 API endpoints, 62 dedicated tests. A grant binds a subscriber, a scope list, a purpose, a TTL capped at one year, and optionally a price. It is minted as a signed handle returned exactly once; the server keeps only its SHA-256 fingerprint. Reads are serialized against revocations with a per-owner advisory lock so that a read receipt commits in the same database transaction as the data projection. That lock exists because of a class of bug with a name, time-of-check to time-of-use, and an audit requirement with a number, NIST AU-12, and the comment in the code cites both.
What proves the read happened is the receipt chain: a per-owner hash chain where each entry is the SHA-256 of the previous hash plus the canonical payload, signed with HMAC-SHA256, genesis at sixty-four zeros. Three event types exist: GRANT, READ, REVOKE. There is no way to read granted data without producing a receipt, because the projection and the receipt are one transaction.
And the vocabulary all of this speaks is the registry: a published, versioned JSON document at hushh.ai/pchp/scopes.json that anyone can fetch, currently v0.6.0. It carries 263 scopes across 7 roots, organized into five tiers (3 open, 27 signal, 153 commercial, 62 sensitive, 18 intimate), with 80 scopes marked sensitive, grouped into 15 purpose bundles. It is generated, never hand-edited, and a test pins the published file's hash so a hand edit fails the suite. The stricter check, that the generator and its output agree, exists as a command and is not yet a CI gate; it is in the next-steps list below, because this document does not get to overclaim in the same breath it warns about overclaiming.
What changed, by the numbers
The registry grew in four public steps in four days: 47 scopes on July 26, 58 on July 28, 252 later the same day, 263 on July 29. The last step is instructive about what growth means here: those eleven scopes split one boolean, privacy.ads, into eleven purpose-level advertising decisions (measurement, cross-app, cross-device, audience matching, lookalike modeling, identifiers, clean rooms, and so on), because every major ad platform already treats those as distinct questions and one switch could only ever over-collect or under-serve.
The resolver's story is better told as a deletion. Until August 5, scope resolution was a hand-maintained Python map that bound exactly 8 of the published scopes; a subscriber holding a valid, signed, receipted grant for any of the others got a clean 200 and an empty projection, silently, forever. The fix that merged (2,882 lines added) deleted the map. A scope now binds to the preference-document path of the same name, by convention, and exactly two legacy bindings survive as explicit exceptions. Adding a scope stopped being a Python change and a deploy; it became a JSON edit. Structurally, the class of bug where the vocabulary and the server drift apart is now hard to write, because there is no second list to forget.
The same day, fourteen seconds apart, a second fix merged with a title we mean literally: stop telling the subscriber who the person is. Until then, every subscriber read returned the owner's raw identity key, the same value to every subscriber, which means any two subscribers holding grants from the same person could join their records offline and rebuild exactly the cross-brand shadow profile this system exists to abolish. The replacement is a per-pair pseudonym: HMAC-SHA256 over a length-prefixed (subscriber, owner) pair, so each subscriber sees a stable identifier for the person that no other subscriber shares. Length-prefixing is not decoration; without it, the pairs ("acme", "b:carol") and ("acme:b", "carol") would collide. The subscriber's copy of a receipt also stopped carrying the chain's sequence number and previous hash, because adjacent sequence numbers were themselves a correlation channel. Both fixes are merged and, verified through the deploy history rather than assumed, running in production as of August 6.
Around the fabric sits the rest of the system, briefly: a Model Context Protocol server exposing 5 public tools and 15 entitlement-gated ones, shipped as an npm package and a hosted endpoint; a dynamic scope grammar (attr.{domain}.{key}) generated per user from what that user has actually stored across 13 canonical domains, ranked by a deterministic function with a least-privilege tie-break, deliberately never by a model; and, on the website, the first end-to-end write surface, where a person can now set and clear real registry-backed preference values through the same API the fabric reads from. Two earlier figures we circulated internally did not survive verification and are hereby retired: there is no TypeScript MCP implementation of "14 tools in 2,474 lines" anywhere in the repository, and the CI-run count is not "about 8,791" but 9,366 on the main validation workflow and 12,862 across all workflows, counted the day this was written.
Two bugs worth teaching
We think the two incidents above are worth a section of their own, because both generalize far beyond this codebase.
The silent empty read. The 8-of-47 resolver was not a broken system; it was a correct system whose job had quietly diverged from its claim. The lesson we took is that the dangerous gap is never between working and broken, it is between published and served, and no amount of green checks on either side will find it, because each side is individually correct. The only fix that sticks is structural: make the published artifact the direct input to the serving path (the resolver now consumes a vendored, hash-pinned copy of the registry itself), so the two cannot diverge without a build failing loudly. Where you cannot unify, pin: the vendored copy is locked by SHA-256, and changing the registry means changing the digest in both repositories in the same change. That friction is the feature.
The join key you did not know you were emitting. The identity leak is the more humbling one, because nothing about it looked like a leak. Returning the owner's identifier to an authorized subscriber sounds almost tautologically fine; the subscriber is authorized, after all. The failure is only visible at the level of the whole system: the harm was never in any single read, it was in the sameness of the value across readers. Privacy properties are system properties. You cannot audit them one endpoint at a time, and the receipt metadata lesson (sequence numbers as a correlation channel) is the same lesson one layer down. Our conclusion: every value that crosses a trust boundary needs an answer to the question "what can two recipients compute by comparing notes," and "nothing" must be a design goal, not a hope.
Enforcement is data, not documentation
The tier system is the part of the registry we most expect other builders to borrow, so it is worth stating precisely what makes it real. A tier is not a label; it is an input to code that refuses things.
Tier 4, a person's own words (the story root, 18 scopes), is never available through any purpose, any bundle, or any price. This is not a policy statement; the registry generator throws at build time if any purpose names a tier-4 scope, and a test fails if the frontend's auction predicate ever returns true for one. Tier 3 (62 scopes: health conditions, family situations, accessibility needs) can never ride along in a bundle; purposes must list such scopes in a separate confirm set the person ticks one at a time. And because the encrypted routing for tiers 3 and 4 does not exist yet, the preference API refuses any tier-3-or-above write with an explicit 422 naming the scope, rather than silently accepting sensitive data into a plaintext store. Refusing loudly is the honest alternative to the silent drop that produced the last incident.
Two smaller decisions carry more weight than their size suggests. Scope names are one-way doors: a published name may be deprecated but never renamed, removed, or reused, because grants in the wild bind to those exact strings; a regression test holds the complete list of every name ever published and fails if one vanishes. It has already caught, in review and before anything shipped, a registry rewrite that dropped 21 names in what looked like tidying. And the resolver distinguishes absent from null with a dedicated sentinel, because "I cleared this field" is information a person chose to express and "this was never set" is not, and a consent system that conflates them will eventually misrepresent someone.
Everything fails closed. An unknown scope resolves to nothing. An unrecognized root is refused even if someone publishes a scope under it. An unclassifiable scope is treated as intimate, not commercial. The default answer everywhere is no.
The economics of a handshake, as engineering
The fabric prices access, and the pricing rules turned out to be security decisions wearing accounting clothes.
The first handshake is free and discloses exactly three fields: an age band, a sex, a region. Coarsened deliberately, because the classic re-identification result is that date of birth, sex, and ZIP code uniquely identify most Americans, and a free tier that leaked the sharp triple would be a de-anonymization service with a marketing budget. Every handshake after the first has a floor of one millicent, a thousandth of a cent, and above the floor the number belongs to the owner, not the buyer; an earlier draft let the subscriber name the price at request time, and inverting that (the owner's agent prices, the buyer accepts or walks) was a one-line change that moved the market's center of gravity to the correct side. The free-handshake counter counts grants ever issued, not grants currently active, so revoking and re-granting cannot farm free handshakes. And one function exists purely to keep us honest about settlement: it reports whether a price can settle on card rails at all, because our floor is one fifty-thousandth of the card networks' minimum charge, which means sub-cent handshakes need batching or different rails, and no amount of enthusiasm changes that arithmetic. Today only the card path has real, config-gated code; the stablecoin path is named in copy and not implemented, and we would rather say that here than let an integrator discover it.
hu_ssh: the transport we have specified and have not built
Above the consent layer sits a question the industry is just beginning to face: when a person's agent outgrows the device in their hand mid-task, how does the task move to a machine they own without the person losing custody of it? Our answer is a specification called hu_ssh, the third RFC in the hussh series, and we want to be unambiguous about its status: it is a specification only. No parser, no daemon, no conforming implementation exists in any repository we control, and the spec's own page says so.
What the spec commits to is worth reading even so. A binary frame protocol (two-byte magic, fixed big-endian header, strictly increasing sequence numbers) with seven frame types, opened by a handshake that carries a hardware attestation and derives an ephemeral AES-256-GCM session key. The frame that justifies a binary transport at all is StateSync: chunked transfer of an agent's pre-computed attention state, so that bursting a task to a bigger machine does not pay the context prefill twice. And the frame we consider most important is the humble heartbeat, because its absence triggers a mandatory rule: the remote is an optimization, the person's task is not, and a lost heartbeat means the task falls back to local execution rather than stalling. The honest open problems are listed in the spec itself: attention-state formats do not port across model families, backpressure sizing is unsettled, attestation beyond one vendor's enclave is undesigned, and revocation reaching an in-flight burst is an unsolved race.
The ledger of what is not real
Every review we publish carries this section, and it is the section we most hope other teams copy.
- Sensitive tiers have no write path. The 80 tier-3 and tier-4 scopes are published, enforced against the plaintext store, and routable nowhere. The BYOK path they belong on (the PKM's aes-256-gcm machinery, which exists and is large) is not yet wired to the fabric. Design pending, honestly not started.
- The backend is one registry version behind. The fabric's vendored catalogue pins v0.5.0 (252 scopes) while the published registry is v0.6.0 (263). This is exactly the deliberate two-repo friction the hash pin creates, currently unpaid. The eleven advertising scopes do not resolve until it is.
- No first-party surface consumes the fabric yet. The grant, receipt, and read APIs are served and tested, and nothing a person touches calls them today. The fabric is an API-and-documentation surface until our own products run on it, and eating our own cooking is the next milestone that matters.
- The two scope grammars are not unified. The published registry is a taxonomy of what a person could offer; the per-user dynamic grammar is an inventory of what this person actually stored. Both are correct, they do not reference each other, and a developer must currently learn both.
- hu_ssh is prose. See above.
- The website's cookie-banner replacement is honest about its own tier. Its grants live in localStorage, its receipts are locally generated and unsigned, and clearing site data silently revokes. It is a demonstration of the shape, not the system, and its page says so.
- Sub-cent settlement does not exist. The floor works as protocol arithmetic; making it clear as money requires batching or rails we have not built.
One meta-item belongs in this ledger: an earlier version of our public status page claimed protocol source files that did not exist in the repository. We published a correction notice on July 29 rather than quietly editing, and the correction stays up. The cheapest time to be caught overclaiming is by yourself, in public, first.
What happens next, in order
- Pay the pin. Refresh the vendored catalogue to v0.6.0 and re-pin the digest, so all 263 published scopes resolve, including the advertising decisions partners would integrate against.
- Eat our own cooking. Wire the first first-party consumer onto the fabric: the grants and receipts a person sees in their own app should be served by the same 11 endpoints we ask the world to use, not by a parallel path.
- Route the sensitive tiers. Design and build the fabric-to-PKM path so tier-3 grants resolve from ciphertext with the same receipt discipline, replacing today's 422 refusal with the store those scopes were always meant to live in.
- Cross-reference the grammars. Emit the registry name and tier alongside each dynamically discovered scope, so the taxonomy and the inventory describe each other instead of competing.
- Grow the write surface root by root. Preferences shipped first because it was the smallest real thing; wants and favorites follow the same pattern, each slice shippable alone.
- Close the generator gate. Wire the registry's generator-versus-output check into the CI pipeline (and its mirrored cloud gate), so the overclaim corrected in this very document becomes structurally impossible to repeat.
- Give hu_ssh its first thousand lines. A reference wire parser and a set of golden frame vectors is a bounded, well-specified project, and exactly the kind we intend to hand to the interns and the open-source community to attack.
The bar we grade against
A note on how this document was reviewed, and the standard it aims at. The engineering culture we are building borrows its bar, explicitly and with attribution, from people whose public work taught us: the systems-review depth and numerical honesty of Jeff Dean's engineering writing, the build-it-then-show-the-whole-schematic candor of Steve Wozniak, the adversarial code-level rigor of Sanjay Ghemawat's reviews as Dean has described them, and, on the business side of every technical decision, the working-backwards discipline associated with Jeff Bezos and the clear-eyed platform thinking associated with Bill Gates. To be perfectly plain before the next sentence, as our gratitude pages always are: none of these people has reviewed, endorsed, or ever heard of this document or this company as far as we know. They are the measuring stick we chose, not the judges we hired. Admiration is not affiliation.
Our internal rule for a document like this one is to ask whether it would survive those five readers, and this draft went through independent adversarial review passes, by our own reviewers and tools and nobody famous, against exactly that bar before publishing: every number traced to a measurement, every claim of "live" traced to a deploy, every gap stated in the same register as every win. The review caught three real errors in the draft you are not seeing (an endpoint miscount, a fourteen-day arc that was actually four, and one overclaim about our own CI), which is the process working, and we would rather tell you that than pretend the first draft was clean.
The work continues in the open. The registry is at hushh.ai/pchp/scopes.json, the live status ledger at hushh.ai/research/pchp-status, the integration manifest at hushh.ai/.well-known/pchp, and the honest gaps above are the to-do list. If you find the next 8-of-47 hiding in our system, telling us is the job, and we will write it up with your name on it.
The 🤫 hussh magazine
Written by hussh Engineering, and built to read beautifully here — and to travel to 🤫 One on your phone, your glasses, and visionOS, as one immersive magazine you own.