Skip to content

Add design specs for the provider architecture and permission model epic - #986

Draft
aram356 wants to merge 9 commits into
mainfrom
specs/providers-and-permissions
Draft

Add design specs for the provider architecture and permission model epic#986
aram356 wants to merge 9 commits into
mainfrom
specs/providers-and-permissions

Conversation

@aram356

@aram356 aram356 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What this is

Five design specs for the provider/permission epic (#777#782), written after review of PR #838 so the next implementation pass has a normative behavioral contract instead of discovering design decisions inside a 67-file diff. They live in docs/superpowers/specs/ per the existing convention. Where a spec contradicts PR #838 — or one of the source issues (each spec ends with an explicit divergence table) — the spec is authoritative.

Spec Covers
pluggable-providers-design EC/device/geo provider architecture: full identity lifecycle contract (mint / parse-canonicalize with provider-declared equivalence / canonical graph key / optional cluster prefix / tombstone), declarative namespace descriptors core proves disjoint, version-rotation schema, active-writer + legacy-reader switching with confirmation-by-presentation rewrite, graph-store startup requirement and no-active-until-commit, runtime failure matrix, adapter-capability matrix, four-adapter parity
permission-model-design Policy as [permissions] in trusted-server.toml (not build-time YAML), enforced-vocabulary rule, three-class signal taxonomy with regime-scoped grant evidence, normative normalization matrix preserving current conflict-mode/expiry/proxy semantics, exhaustive withdrawal triggers with a family revocation record, concrete raw-EC egress inventory (path → permission table), stored-provenance S2S authority with legacy fail-closed, regime-based auction dispatch matrix including the raw-TCF arm, fail-closed jurisdiction resolution with an acknowledged static-jurisdiction escape hatch
provider-migration-rollout-design Behavior-preservation matrix (16 rows incl. US grant-signal cases and declared hardenings; silent changes are defects), identity stability via deterministic HMAC-prefix vectors, dual-read config release with strict reader-first ordering, graph-schema expand-contract rollout, per-adapter committed preserving fixtures run through the decision matrix in CI, telemetry with retirement thresholds
client-cycle-ec-resolve-design Threat model and bar for the browser-resolve EC provider type #838 shipped undeclared: exact-Origin/CSRF authorization, session-bound replay handling with stated reservation states, identity-graph invariant, existing-identity semantics, bounded inputs. On hold until its open questions get a dedicated issue
integration-response-header-hook-design #782 sharpened: structured mutation operations core validates and attributes, three-stage ordering ending in an inviolable cache/privacy invariant pass, reserved surface incl. framing/hop-by-hop headers and cookie-name granularity, response-eligibility matrix, panic-is-fatal on wasm, lands only with a real consumer

Key decisions encoded

  • Policy lives in trusted-server.toml, validated at ts config push and startup; absent policy falls back to a compiled-in everything-requires_signal, regime = "gdpr" posture.
  • Opt-out signals always override a consenting TCF string and are honored globally; grant evidence is regime-scoped so US non-opt-out values cannot authorize identity under GDPR rules.
  • EC providers own their identifiers end to end (parse, keys, tombstoning); permission gating covers minting and identity use only — parse and revocation always run. Geo is ungated because it is circular (it feeds resolution); device is ungated by recorded decision (security classification authorized by operator selection, with fingerprint-derived buyer-facing row fields removed pending a vocabulary extension).
  • Every raw-EC egress is inventoried and gated (user.id, derived IDs, page bids, forwarding, identify, sync), not only EIDs; batch sync is authorized by stored provenance recomputed against current policy, and legacy rows fail closed until backfilled.
  • Withdrawal writes a family revocation record first — durable intent, sibling discovery, and fail-closed read marker in one key; the migration story is expand-contract for the graph schema and dual-read for config, with strict reader-first ordering.
  • PR Add pluggable Edge Cookie, device, and geo providers gated by a permission model #838's host-signals and client-fixed providers are deliberately not carried over; configs selecting them fail loudly.

The specs have been through three review rounds (an adversarial fresh-eyes pass plus two maintainer reviews); the commit history records each round's findings and fixes.

aram356 added 2 commits July 31, 2026 00:06
Five specs covering the work proposed in PR #838 (issues #777-#782), written
so the next implementation pass has a normative behavioral contract:

- Pluggable providers: identity lifecycle contract (mint/recognize/hash/
  tombstone), trait minimalism, adapter parity, validation table
- Permission model: signal precedence (opt-out over TCF), fail-closed
  jurisdiction resolution, policy file validation, decision-matrix testing
- Migration and rollout: behavior-preservation matrix, ID stability
  vectors, loud-failure requirements, operator recipes
- Client-cycle EC resolve endpoint: threat model and prerequisites; on
  hold until its open questions get an issue
- Integration response-header hook: #782 contract with ordering and
  collision policy, ships only with a real consumer
Self-review of the five specs (own pass plus an adversarial fresh-eyes
pass) surfaced fixes applied here:

- Policy location reversed per maintainer decision: the permission policy
  is a [permissions] section of trusted-server.toml flowing through the
  config-store pipeline, not a build-time-embedded YAML. Overrides name
  acquisition rules directly instead of +/- sigils, and a rules.default
  entry separates resolved-but-unlisted countries from geo default_country.
- Removed a circular requirement: geo and device providers are inputs to
  permission resolution and cannot be gated on its output; the enforcement
  gate is EC-only and the decorative required_permissions declarations are
  dropped from those traits.
- Fixed the identity-stability guarantee: the EC id has a random per-mint
  suffix, so known-answer vectors pin the deterministic 64-hex prefix,
  recognition of existing cookies, and hash-prefix semantics instead of
  full identifiers.
- Split the KV key contract into the verbatim graph-row key and the
  deliberately-colliding hash prefix that IP-cluster trust depends on.
- Declared previously silent behavior changes in the migration matrix:
  global opt-out honoring (including tombstones) and the fate of
  non-regulated countries, with a preserving recipe for the latter.
- Sequenced the geo neutral-default flip into the permission model PR so
  no intermediate step zeroes EC issuance under the current fail-closed
  gate.
- Resolved smaller contradictions: withdrawal triggers made exhaustive
  (including denied-baseline and policy-edit cases), jurisdiction
  consistency requirement now covers both legacy lists with explicit
  exceptions, ISO rule-key validation made decidable, response-header
  reserved surface defined at cookie-name granularity for Set-Cookie, and
  the host-signals provider's removal made explicit with config rejection.
@aram356
aram356 marked this pull request as draft July 31, 2026 08:02
aram356 added 4 commits July 31, 2026 01:41
…ssion specs

Blocking findings from review of PR #986, all addressed:

- Raw EC egress is now a first-class enforcement point with a mandatory
  egress inventory (user.id, derived request IDs, page bids, proxy/click
  forwarding, identify, pull/batch sync, graph access): bidstream egress
  requires both purposes, first-party identity operations require
  store-on-device, revocation is exempt, and no-provider mode never
  vacuously allows an existing cookie to egress.
- The behavior-preserving recipe carries the complete policy table plus a
  delta; a partial permissive-default-only policy is called out as the
  trap it is, and the exact recipe text becomes a CI fixture run through
  the decision matrix.
- The EC permission gate is split: it covers minting and identity use
  only; parse, canonicalization, and tombstoning always run, with a
  spy-provider test - a blanket gate would block withdrawal in exactly
  the state an opt-out produces.
- Provider switching gets active-writer/legacy-readers semantics
  ([ec] legacy_providers) so old identities keep resolving and stay
  withdrawable; unmatched cookies never egress.
- The lifecycle contract now distinguishes the canonical graph key
  (provider-owned canonicalization, equivalent envelopes collapse) from
  the cluster prefix, which must be a literal byte prefix of the graph
  key because cluster sizing is a KV prefix listing; cluster support is
  an optional capability with an explicit degradation policy.
- Device gating rationale corrected: only geo is circular; device is
  ungated by decision (security classification authorized by operator
  selection), with the boundary stated - uses beyond security
  classification need a vocabulary extension and a gate.
- Withdrawal triggers made consistent (TCF refusal withdraws under
  requires_signal or denied), and a withdrawal-durability contract
  added: tombstones first, cookie expiry only on success, browser-side
  durable signals as the retry queue, fault-injection tests.
- A signal-normalization matrix is now required (dual-TCF conflict
  modes, expiry, proxy mode, KV fallback, exact GPP fields), and
  malformed-but-present records fail closed for acquisition instead of
  degrading to absent.
- Auction jurisdiction class is an explicit per-group regime attribute
  (gdpr / us-privacy / none), never inferred from purpose flags, and a
  first-class enforcement point.
- The no-geo acknowledgment guard now keys on any enabled jurisdiction
  consumer, not only EC-provider selection.
- Policy validation additionally requires rules.default when the section
  is present, rejects empty sections and case-insensitive duplicate
  keys, and canonicalizes default_country.
- Resolve endpoint: exact Origin-allowlist membership or session-bound
  CSRF token (Sec-Fetch-Site demoted to defense-in-depth), real replay
  mitigation (session nonce or one-time consumption), and bounded-input
  requirements with 413 boundary tests.
- Response hook: structured mutation operations that core validates and
  attributes (no raw header-map access), framing/hop-by-hop headers
  reserved, and a normative response-eligibility matrix.
- Each spec now carries an explicit divergence table against its issue
  (#778, #779, #782) so there is one acceptance contract.
- Non-blocking clarifications folded in: geo lookup-failure residual
  declared and metered, deterministic entropy required in conformance
  tests.
…and rollout gaps

Blocking findings from the second review of PR #986:

- Signal taxonomy gains a grant-signal class (TCF consent, explicit GPP
  non-opt-out, US Privacy present-and-not-opted-out including N/A) so a
  requires_signal US rule reproduces today's no-signal-blocks /
  explicit-non-opt-out-grants behavior, which the two-class model could
  not express; migration matrix gains rows 3a-3c and the example US
  group changes to requires_signal.
- Auction dispatch gets a normative regime matrix (gdpr / us-privacy /
  none across consent, opt-out, malformed, expired, absent states), the
  compiled fallback gains regime = gdpr, and blocked dispatch means no
  outbound request at all.
- The normalization matrix now states outcomes instead of subjects:
  restrictive/permissive synthesize per purpose, newest selects whole
  records, expired records are absent entirely, valid-beats-malformed
  within a family, KV fallback is live-wins with TTL-bounded staleness
  and an exempt consent-state lookup, mirror mode loses to request
  records, and GPP fields are enumerated per section.
- The egress inventory is a concrete path -> permission table: proxy /
  click / Testlight forwarding assigned (both purposes, declared as new
  hardening in split row 11a/11b since those paths are ungated today),
  identify and pull/batch sync classified as partner exchange (both
  purposes), and S2S sync authorized by stored provider/version-tagged
  provenance re-validated against current policy.
- Withdrawal drops the false atomicity claim: revocation families are
  idempotent independent writes, readers fail closed on any present
  member, and fault-injection covers the Nth-write failure.
- Equivalence is provider-declared via fixtures (hmac: hex prefix
  case-insensitive, suffix case-preserved) instead of a universal case
  rule; the legacy HMAC grammar is formally reserved as the hmac
  namespace so verbatim row keys and provider namespacing coexist;
  global cookie-safe identifier bounds added; non-cluster providers get
  defined dedupe and redaction.
- Legacy readers get full semantics: first-match parse with
  namespace-overlap validation, recognizing provider's permissions
  govern, provider/version-tagged provenance, transactional linking
  rewrite with dual revocation, and provider = "none" as an explicit
  stateless state that keeps revoke-only legacy readers.
- Graph store required at startup when any provider can mint or read; a
  minted identity is not active until its row commits; a runtime
  failure matrix covers provider, graph, cluster, rewrite, and
  geo/device runtime failures.
- Rollout gains a dual-read release (N+1 accepts both config shapes,
  N+2 rejects loudly) since no config is accepted by both current main
  and a rejecting binary; the preserving recipe becomes one committed
  valid TOML fixture (the prose delta reopened [permissions.rules],
  which is invalid TOML); metrics extended with retirement thresholds.
- Resolve endpoint defines same-identity no-op / different-identity
  rejection and an atomic single-key reservation tying replay
  consumption to graph persistence.
- Hook ordering becomes core -> integrations -> inviolable cache/privacy
  invariant pass (an appended cookie plus replaced public Cache-Control
  can no longer produce a shared-cacheable cookie response); generic
  ops reject Set-Cookie; per-integration operation limits and
  erroring-mutator semantics defined; every eligibility row tested.
- Stale device-circularity wording removed from the permission spec;
  region-form default_country (US/CA) restored; the source-agnostic
  permission-source requirement of #777/#779 explicitly deferred in the
  divergence table; fail-closed and most-protective labels qualified
  with their stated exceptions.
…fecycle repairs

Blocking findings from the third review of PR #986:

- Grant evidence is now regime- and permission-scoped: gdpr rules accept
  only TCF consent for the specific purpose, us-privacy accepts TCF or
  explicit GPP/USP non-opt-out, none accepts any grant class - closing
  the hole where sale_opt_out=false in France would have authorized
  identity and partner egress with no TCF. Opt-outs and refusals stay
  regime-agnostic.
- The graph schema change gets an expand-contract rollout: reader/
  preserver release (unknown fields preserved through read-modify-write),
  fleet-convergence gate, then writer activation, with schema versions,
  lazy backfill, and mixed-version tests.
- S2S batch-sync authority is a full recompute of both permissions from
  stored per-permission, time-bounded evidence (grant basis, timestamp,
  jurisdiction, policy revision, provider/version) - failing closed on
  denied, tightened baselines without acceptable stored evidence,
  expired evidence, or regime-rejected grant sources. Legacy pre-epic
  rows are hmac-v0 with no grant evidence and fail closed until lazily
  backfilled.
- Pull sync split from batch sync: pull is browser-request-scoped and
  keeps using the live P1/P4 decision plus revocation state; only batch
  is provenance-authorized, and its gate is declared hardening (new
  matrix row 11c; row 11a corrected).
- Withdrawal centers on a family revocation record: a stable family ID
  in every member row, one record written first that is simultaneously
  the durable intent, the sibling-discovery mechanism, and the
  fail-closed marker; member tombstones become cleanup; degraded-graph
  mode fails S2S closed while writes fail; the healthy-graph residual is
  declared.
- Legacy rewrite is confirmed by presentation: both linked rows stay
  live until a later request presents the new cookie (the server cannot
  observe Set-Cookie acceptance); linked rows share the revocation
  family.
- The normalization matrix now preserves actual current semantics:
  whole-record selection by combined P1/P4 eligibility for restrictive/
  permissive, LastUpdated with freshness threshold and restrictive
  tie-break for newest, proxy mode skips decoding, one-valid/one-expired
  row added, and GPP section fields enumerated normatively.
- The auction matrix regains the raw-signal arm: a decodable TCF record
  applies the gdpr dispatch rule in every regime, so a P1 refusal on US
  or non-regulated traffic still blocks dispatch.
- Provider namespaces become declarative descriptors core can prove
  pairwise disjoint at startup (opaque parse cannot be); version
  rotation gets a schema (versions entries, mint_version, newest-first
  parse, retirement rules).
- Client resolve reservations get pending/committed/failed states,
  lease takeover, retention through token expiry, deterministic graph
  idempotency, adapter CAS capability - and duplicates never receive
  Set-Cookie unless the reservation is session-bound, closing the
  idempotent-replay fixation hole.
- The hook invariant pass preserves any pre-hook private/no-store
  classification (cookieless personalized HTML cannot be made publicly
  cacheable) and strips CDN directives; panics are declared forbidden
  and fatal on wasm32-wasip1 (panic=abort - recovery was
  unimplementable); a cumulative final-response header budget with
  deterministic rejection order added.
- Rollout ordering corrected to strictly reader-first with a
  convergence gate (the previous either-order claim was false against
  binaries that reject the new shape); mixed old/new config shapes
  rejected; rollback sequencing defined; one preserving fixture per
  adapter since device/geo selections are capability-gated.
- Device-provider authorization reconciled with persisted use: new rows
  stop carrying fingerprint-derived buyer-facing fields (declared
  change); a field-level graph contract table is a required
  implementation deliverable.
- Non-blockers folded in: adapter-capability matrix, assigned-
  subdivision region validation, retirement quiet period no shorter
  than max cookie/row lifetime plus skew, expanded telemetry, and the
  stale recognize/hashed/eligibility terms corrected.
…cs, and distributed contracts

P0: legacy identities can now enter the family-revocation protocol.
Rows lacking a family ID derive one deterministically from (record kind,
provider namespace, canonical graph key), so a first-post-upgrade
withdrawal is discoverable by every future reader even if the writer
crashes before touching the v1 row; random IDs are called out as
recreating the orphan the design exists to eliminate. Withdrawal never
depends on backfill; tests pin the first-request-is-withdrawal and
crash-between-writes cases.

P1 groups:
- US signal fields get a normative field x value x permission x
  destructive table (4.5): sale maps to P1+P4 and is destructive,
  sharing and targeted-advertising map to P4 only and never destroy
  identity, USP carries no targeted field, absent/N-A grants nothing,
  state sections override national, opt-out beats grant across sections.
  The regime evidence table now defers to this mapping.
- Normalization is a six-state machine (valid-grant, valid-refusal,
  opt-out, malformed-present, expired, absent) wired into precedence and
  the decision matrix (malformed blocks the granted baseline); proxy
  mode keeps a syntax pass so malformed is distinguishable, blocks
  record-derived grants, and is declared as a change from today's
  fail-open skip.
- Conflict resolution is deterministic: whole-record selection over the
  (P1, P4) tuple ordered lexicographically P1-first (split-purpose
  records decided), newest uses LastUpdated with the freshness threshold
  and falls back to restrictive; expiry drops sources before conflict
  resolution - a declared change from today's conflict-first ordering.
- The auction raw-signal arm triggers on raw TC-string presence or a
  GPP section-2 hint before decoding, so malformed raw TCF still blocks
  dispatch outside GDPR regions.
- Stored provenance ages: authoritative timestamp + valid_until per
  evidence class, re-presentation does not reset age, and every live
  resolution atomically replaces the full per-permission snapshot so a
  later refusal clears older positive authority; rewrite provenance is
  the fresh live resolution, partner mappings keep original expiries.
- A per-record-class consistency matrix: replay reservations need
  linearizable CAS with fencing (Durable-Object-class on Cloudflare,
  not Workers KV), family revocation records need strong reads plus a
  declared bounded visibility lag with read-failure failing closed,
  identity rows may be eventual; retention outlives every dependent
  lifetime (today's 24h tombstone TTL explicitly does not carry over).
- The US policy enumerates US/<state> rules for configured privacy
  states with country-level US non-regulated, preserving Wyoming-class
  traffic; regionless-geo degradation is declared; the states-list
  consistency test is region-shaped.
- The graph field contract is normative in-spec (providers 6.3): every
  v1 and new field with purpose, source, gating permission, TTL,
  rewrite, and revocation treatment - including discontinuing
  fingerprint-derived buyer-facing fields; releases unified as N/N+1/N+2
  with semantic (not byte) unknown-field preservation, a hard rollback
  floor at N+1 after writer activation, and stated mixed-version
  expectations.
- Identity boundaries are structural: core constructs physical graph
  keys with record-kind/provider/version prefixes (legacy hmac verbatim
  excepted) and an AuthorizedIdentity newtype - constructible only after
  parse, permission, graph, and family checks - is the only type
  outbound serializers accept.
- Legacy rewrite aliases to one canonical row via fenced CAS (no
  dual-write divergence), with confirmation by presentation and a
  finite retirement deadline.
- Client-cycle: session binding is required for production schemes
  (one-time consumption demoted to defense-in-depth; at-most-once only
  as an explicitly recorded posture with orphan cleanup); reservations
  carry owner hash and monotonic lease epochs with fenced transitions;
  owner-hash retry re-emits the cookie so lost responses do not orphan
  rows; resolve checks the family revocation record and loses races to
  revocation.
- The hook snapshots all pre-hook cache restrictions (origin-supplied
  included) and allows only equal-or-stronger privacy; Content-Encoding
  and Content-Range join the reserved surface; the test set covers
  origin-private and core-private cookieless HTML, cache hits, Vary,
  every CDN directive, and body encoding.

P2/P3: mint 'cookie write' clarified as scheduled-on-final-response with
egress eligibility at graph commit; degraded-health is a per-instance
in-memory state machine with hysteresis; HMAC versions resolve from row
provenance (untagged = hmac-v0), not parse; client limits are exact
(65,536-byte body, content-type allowlist, 256-byte identifier, 128-byte
reservation key, per-code statuses); migration matrix gains rows 3d-3g;
fixtures include the graph-store config; every rollout metric ships with
threshold, window, and action; batch-sync's coverage dip is
operationalized with the provenance-coverage metric; FR default relabeled
a protective opt-in fallback; device-selection authorization qualified to
the opt-in fingerprint provider; the illustrative policy example is
labeled as such.
@aram356 aram356 self-assigned this Jul 31, 2026
aram356 added 3 commits July 31, 2026 14:20
…ncy eligibility, and rollout closure

P1 fixes:

- Opt-out signals split into destructive (GPC, sale, USP - withdraw) and
  non-destructive (sharing, targeted-advertising - revoke P4 only, never
  tombstone) subclasses assigned by the 4.5 mapping, resolving the
  4.2-vs-4.5 contradiction.
- Proxy mode performs minimal opt-out extraction (the 4.5-mapped fields
  and USP only) so globally authoritative opt-outs are never suppressed;
  still no record-derived grants; declared as a change from today's
  opt-out-blind skip.
- GPP applicability is an ordered algorithm with a pinned jurisdiction ->
  section-ID map (usnat 7, usca 8, usva 9, usco 10, usut 11, usct 12):
  applicability from resolved jurisdiction, state-over-national per
  field, restrictive aggregation; foreign and non-applicable sections
  contribute nothing; N/A preserved as not-opted-out (declared,
  correcting the earlier contributes-nothing rule).
- TCF conflict selection reverts to today's algorithm - P1-and-P4
  conjunction comparison with standalone winning equal conjunctions
  (including split-purpose) - replacing the invented lexicographic tuple
  and keeping the Preserved label honest.
- S2S freshness is a per-evidence-class contract: TCF ages by
  LastUpdated, GPP/USP by first-seen with an equality digest
  (re-presentation keeps original first-seen), baseline grants re-derive
  from the current policy revision; clock-skew clamping.
- Degraded-mode protection is declared local-only with the cross-instance
  residual quantified (bounded by user return latency, metered), instead
  of implying fleet-wide fail-closed.
- Family revocation records require a strongly consistent primitive;
  Workers KV is explicitly ineligible ('60 seconds or more' is not a
  bound); alias/rewrite records join reservations in the linearizable
  CAS class and rewrite_legacy is rejected without it.
- The trait now really returns graph_key_suffix (a round-4 batch loss),
  core owns the 6.3 physical key grammar (id/, alias/, fam/, rwx/,
  resv/ prefixes + reserved legacy grammar) with wire schemas and TTLs
  per record class.
- HMAC version attribution really resolves from immutable row provenance
  (also a round-4 batch loss); parse identifies namespace only.
- AuthorizedIdentity is scope-parameterized (GraphOps vs PartnerEgress)
  so a P1-only identity cannot reach an ORTB serializer.
- The provider contract gains acquisition modes (ServerMint /
  ClientResolve carrying resolve_from_client and the JS module id); the
  resolve endpoint enforces the provider's full required_permissions.
- Revocation-wins at the resolve endpoint holds because the family check
  runs through the linearizable class client-cycle already requires.
- Rewrite is a persistent fenced transaction: pinned target on retry,
  reconciliation of updates that won the old-row CAS, orphan GC by
  absent transaction, and fenced alias retargeting keeping chains
  single-hop.
- Release protocol: rollback is binaries-first (N+2 -> N+1 keeping the
  new config); N+1 rejects provider/version selections it cannot encode;
  a pre-N+1 graph-store readiness step plus matrix row 12 covers
  graphless HMAC deployments (breaking, declared).
- The abstract capability list becomes a concrete adapter matrix
  (Fastly/Axum/Cloudflare/Spin) with honest cells - including that
  Cloudflare supports platform geo country-only (the migration text
  claiming it rejects platform geo was wrong) and that no CAS-class
  primitive is currently wired anywhere but the dev adapter.
- Integration cookies enter the permission model: registration-declared
  names with purpose and retention, persistent cookies gated on
  store-on-device, session cookies as the narrow exemption.
- Cache monotonicity is a defined lattice (no-store > no-cache > private
  > public, shrink-only ages, snapshot-gated stale directives, protected
  Vary union) in the contract, not the tests.

P2/P3: persisted-KV consent now flows through the full pipeline
(declared change); provenance transition and mid-replacement fault tests;
the recipe renamed minimal-divergence with its unavoidable divergences
enumerated; batch-sync coverage is a gated stage with thresholds and a
pause action; policy-revision activation defined (stamped revisions,
bounded mixing, no tombstone resurrection); representation surface
extended (Content-Type, ETag, Last-Modified, Accept-Ranges, digests);
append restricted to list-valued headers; exact budgets and snapshot
read semantics; reservation namespacing and per-state ownership
conflicts; media-type matching ignores parameters; the pass-through test
wording fixed; and a product-decision sign-off list (9 items) added to
the migration spec for explicit maintainer ratification.
… and storage-protocol coherence

P1 fixes:

- GPP applicability now gates grants only: mapped opt-out fields (either
  subclass) aggregate globally from any section on any request,
  resolving the section-4-vs-4.5 contradiction where a French visitor's
  usnat SaleOptOut was simultaneously mandatory and ignored.
- The section map covers everything current code recognizes (7-23,
  through usmn), not 7-12; Texas section 16 named as what the truncated
  map would have silently lost; states without a state section (MD, IN,
  KY, RI) use the national section.
- Destructive TCF-refusal withdrawal requires the refusal to be carried
  by the live request; persisted-KV records participate in acquisition
  only - closing the path where a years-old stored refusal tombstones on
  the first signal-less request after a policy tightens to denied, and
  repairing the mixed-revision safety claim.
- Negative authority gets its own record: a permission-exempt,
  strongly consistent suppression record (sup/<family-id>) with
  per-permission entries that every S2S recompute and partner-egress
  check consults - resolving the circularity where clearing P1
  provenance required the P1 the refusal just unset, and the
  eventual-row edge where a stale replica restored P4 after a
  targeted-advertising opt-out.
- The consistency requirement has one normative home (the providers
  matrix, strong read-after-write); the permission spec's bounded-lag
  leftover is gone.
- The never-returning-visitor residual is stated as unbounded and
  becomes sign-off item 11, replacing the false bounded-by-return-
  latency claim.
- Aliases live at the source identity key with a kind discriminator in
  the value envelope - a separate alias/ address could neither be found
  by old-cookie lookups nor installed by a single-key CAS.
- Identity keys drop the version segment (id/<provider>/<suffix>);
  version lives in the row envelope, killing the read-the-row-to-learn-
  how-to-read-the-row circularity. All HMAC versions stay on the
  verbatim key scheme, keeping the 64-hex cluster prefix a literal key
  prefix for every HMAC row; rotation-induced cluster splits are
  declared as inherent to rotation.
- Rewrite requires the row store itself to provide per-key CAS with
  read-your-writes (alias installs happen there); adapters with purely
  eventual row stores cannot host rewrite. Chains use bounded traversal
  (4 hops, cycle detection, fail closed) with opportunistic path
  compression instead of an undefined inbound-alias index.
- Revocation-wins at the resolve endpoint is an explicit linearization
  point: family records carry an epoch and the cookie-emitting commit
  is a CAS conditioned on it - linearizable reads alone lose the race.
- resolve_from_client takes a core-built ClientResolveContext (canonical
  audience, verified session owner, clock, bounded payload) and returns
  a verified identity with reservation id and expiry.
- The mutator snapshot is redacted: all Set-Cookie values and reserved
  identity/consent/privacy header values withheld, so the hook cannot
  leak the raw EC around AuthorizedIdentity<PartnerEgress>.
- The cache merge is over independent sticky directives per RFC 9111
  (no-cache and private are orthogonal; the ordered-lattice version
  could make a personalized response shared-storable), and the complete
  origin Vary set is preserved, not only core-required members.
- Hook cookie coupling acknowledged: the persistent-cookie gate is a
  listed enforcement point in the permission spec inventory; cookie
  operations activate only after the permission model lands; a typed
  cookie builder enforces declared lifetime/scope/security attributes;
  deletion cookies work when P1 is denied.
- N+1 is a full semantic reader and enforcer for every N+2 record kind
  (aliases, family revocation, suppression, provenance fail-closed),
  with rollback tests on N+1 against N+2 data; binaries-first rollback
  gains its precondition (converge to an N+1-compatible config first
  after N+2-only adoption, retaining new-provider secrets as legacy
  readers rather than reverting config).
- Adapters without revocation-eligible storage migrate with explicitly
  stateless fixtures (sign-off item 12) instead of invalid HMAC
  fixtures.

P2: explicit NotApplicable rows (grant-class, preserved) separated from
absent; per-permission first-seen digests over only applicable
aggregated fields; consent.us_states.privacy_states path corrected;
provider-switch rollback keeps the new provider as a legacy reader;
rewrite_legacy with a client-resolve writer is a startup error; client
parity redefined as identical startup rejection on ungated adapters; the
capability matrix distinguishes platform availability from wiring (Spin:
available, not wired); row 3e's effects classified in both directions;
and the sign-off list is a ratification table (owner/status per row,
implementation blocked while any row is open) extended with items 10-14.
…gs, add a review ledger

The recurring theme - browser-side acquisition, integration-owned
identifiers, rewrite, and pre-existing state generating blockers while
the core holds - is answered structurally this round:

Descope (sign-off item 15, ratify or veto):
- The client-cycle spec is demoted to a deferred informative draft: no
  production adapter has its CAS-class primitive, it has no consumer,
  and its findings no longer block core ratification. Within it: the
  ownerless first-presenter mode is removed outright (risk acceptance
  does not make a security invariant true, and its orphan cleanup was
  unimplementable - the server cannot observe Set-Cookie acceptance);
  the page leg is permission-gated before the module executes; the
  cross-key commit atomicity gap is recorded as open question 0.
- rewrite_legacy is cut from the epic into a recorded deferral carrying
  its open problems (retention lineage, eventual-store visibility,
  chain stranding, cluster inflation) as the entry bar for a future
  spec; provider switching is served by legacy readers alone; the key
  is rejected as unknown.
- The hook ships headers-only: the write-side cookie gate never modeled
  reading, using, forwarding, or withdrawing an integration cookie
  (or its P4 nature), so cookie operations defer to a follow-up spec
  with that full model as entry bar; sign-off items 9/10 updated.

Core fixes (new P1/P2):
- Recognized rowless legacy cookies (graphless deployments) get a
  permission-gated, race-safe adoption transaction; no egress before
  adoption; withdrawal needs no adoption (derived family ID); matrix
  row 13.
- Unreferenced [ec.providers.*] blocks are startup errors - a dropped
  legacy_providers entry must not silently strand identities.
- Physical key delimiters are backend-safe and validated per adapter
  (Fastly forbids / in prefix queries); cluster eligibility requires a
  queryable physical prefix, checked at startup.
- Cluster size means live identity rows: kind/liveness filtering,
  short-TTL tombstone inflation declared conservative.
- Validation split into structural (push + startup) and deployment
  (startup; optional push pre-check via a machine-readable capability
  profile) - 'same validation at push' was unimplementable.
- The rollback floor is N+2 writer activation itself, recorded as a
  durable schema-floor marker - not an unobservable first-row fact.
- Integration IDs are startup-unique.

Previously-open items closed:
- GPP map completed against the official registry: section 6 (US
  Privacy as GPP section) and 24-27 (MD/IN/KY/RI - the earlier claim
  they had no sections was wrong).
- State-over-national applies to grants only; a national opt-out can
  never be erased by a state field.
- Suppression records completed: full negative-state coverage,
  monotonic per-permission ordering, re-consent clearing, write-failure
  semantics.
- The raw-TCF dispatch arm triggers on TCF-sourced effective records
  including the persisted-KV fallback.
- N+1 writes the safety-critical record kinds (family, suppression) and
  accepts N+2-only providers as legacy readers, making the rollout
  boundary safe in both directions.
- Request-side integration views are identity-redacted; the legacy
  RequestFilterEffects.response_headers channel is folded into the hook.
- must-understand and friends join the sticky directive set; the Axum
  matrix cell is honest (in-process, non-durable, dev-only).

Process: docs/superpowers/specs/pr986-review-ledger.md records the
disposition of every finding from all seven review rounds (fixed /
reapplied-after-batch-loss / partial-refixed / superseded / deferred /
open), so coverage is auditable per finding rather than claimed in
summaries.
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