Skip to content

Add the integration seam design specs - #1342

Open
jwrosewell wants to merge 203 commits into
mainfrom
feature/6-integration-seam
Open

jwrosewell wants to merge 203 commits into
mainfrom
feature/6-integration-seam

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
docs/superpowers 59 files, 9906 lines added, 14 removed
crates/trusted-server-cli/src 1 file, 31 lines added, 0 removed

Closes

Part of #777

Spec

Spec: this pull request carries every design of the stack. Each design that a pull request of the stack links as its spec ends in its review log.

Test plan

CI on this pull request at c075081 passed all 24 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at c075081

Checklist

  • Feature PRs: spec linked above, ending in a review log of two or more review rounds
  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

This pull request is first in the stack, and its branch number is out
of sequence.
The branches were numbered as they were cut, and the design
documents were written down after the first five pull requests were raised,
so this branch is the sixth. It is read first, because every other pull
request builds what these documents describe, so it is the first row of the
stack in #777 and the first to merge.

It also carries the one code change the whole stack needs in place
first.
It is in crates/trusted-server-cli/src/commands/dev/lint/domains.rs
and the linter's spec. The URL host linter's allowlists gain the hosts of
the modules the stack ships (InMobi Choice, the Google ad tag loader, the
Model Terms signal) and the hosts of the integrations already shipped that
the lists lacked (DataDome behind Fastly, APS, Permutive, Sourcepoint's EU
CDN), the reference hosts gain the IETF RFC editor and Google's
documentation, and a module crate's own fixtures under
crates/<type>/<vendor>/src/fixtures/ are exempt as core's publisher
captures are. With this merged, ts dev lint domains --changed-vs main on
each later pull request reports only the placeholder hosts main itself
uses (test-publisher.com, cdn.com and the like) and the security tests
that name an attacker's host on purpose.

This pull request carries the design documents of the stack and the one code change above.
The seven documents in the first table are the specs for the module series
and for the integration seam, reviewed here before the code that implements
them in #1343 to #1348. It targets main directly.

At the 51Degrees PR review on 23 October, IAB Tech Lab asked to review the permission designs first and the developer experience after them. The two sections below list those designs in that order, and the tables after them list every design of the stack.

Read first: permissions

  1. 2026-07-30-permission-model-design.md, the permission model, built by row 4 (Add the permission model with the Privacy Taxonomy vocabulary #1345). A module declares the permissions its use of data needs, and the publisher's policy file decides which are held. The file is selected and supplied when a service is deployed, so no adapter build carries a policy, and a domain without a valid one answers 500 to every request.
  2. 2026-10-14-consent-decoders-design.md, built by row 42 (Move consent decoders into their standards' crates #1383). The TCF, GPP and US Privacy decoders leave core for the crates named for their standards, and core keeps only the interface every permission signal module fills.
  3. 2026-10-12-inspection-endpoints-design.md, built by row 9 (Add inspection endpoints that show what a deployment runs and decides #1350). /_ts/config shows the settings a deployment runs, with secrets masked and the policy's name and SHA-256, and /_ts/permissions shows the permissions resolved for a request and names the policy that resolved them.
  4. 2026-10-12-config-by-host-design.md, built by row 11 (Add one app-config blob for each host #1352). Each host has a settings blob of its own, which names its policy file, and the service stores each policy file once however many hosts name it.
  5. 2026-10-12-module-context-design.md, built by row 8 (Add one module context for every kind of module #1349). Core hands every module one context, and a value whose use needs a permission, being the location, the device signals or the Edge Cookie identifier, reaches only a module that declares the permission and is granted it.
  6. 2026-10-12-inmobi-choice-module-design.md, built by row 18 (Add the InMobi Choice consent prompt as a module #1359). A consent prompt as a module, what it places on a page, and how the reader's answer reaches the permission model only through the permission signal modules.

Developer experience

2026-10-27-developer-experience-design.md follows three people through every step, being a module author adding a module, an operator running a deployment from the one signed release WASM that serves every publisher, and the release manager building and signing that WASM. It adds no code. Two new designs sit beside it, one for a module outside core serving an endpoint of its own (row 12, #1353), and one for folding the module crates into one crate that keeps each <type>/<vendor> folder (for a new row 56).

What changed after the 51Degrees PR review

Agreed at the review on 23 October, point 1. No configuration or permission policy is compiled into the WASM, and a WASM without a policy does not start.

  • The permission designs take the policy file chosen when a service is deployed. The permission model design, and the config by host, inspection endpoints, config migrate, provider migration rollout and response header hook designs, say that a domain's settings name the publisher's policy file and record its SHA-256, that ts config push --permissions <file> supplies the file, and that a domain whose file is missing, unnamed or not the one pushed answers 500 and logs why.
  • Review logs record each round of the permission designs, and three review log entries say what each round read.

Agreed at the review on 23 October, point 2. The 51Degrees loading cache is pluggable through modules, and the publisher asset feature becomes a module.

  • Stock builds link no cache. The loading cache design says that no stock build links a loading cache module, the asset cache design moves the publisher's assets into the module crates/asset-cache/51degrees behind one interface in core, and the module cache declarations design says that a build that links one loading cache module holds a declared cache's answers. Their review logs record each round.

Agreed at the review on 23 October, point 3. The <type>/<vendor> folders stay, in one crate.

  • The integrations crate design describes the change that folds every module crate into one crate, crates/trusted-server-integrations, with a folder for each type and vendor, and its review log records each round. Its code is a later pull request, row 56, on top of this stack.

Agreed at the review on 23 October, point 4. Attestation leaves the stack, with no attestation interface in core.

  • The module endpoints design replaces the attestation design. It says how a module outside core serves an endpoint of its own, as Prove a module outside core can serve its own endpoint #1353 proves on all four adapters. The administration, diagnostic headers, Fastly names and request filters designs no longer describe attestation, and the review logs of these designs record each round.

Agreed at the review on 23 October, point 5. Tech Lab reviews the permission designs first, then the developer experience.

  • The developer experience design follows a module author, an operator and the release manager through every step, and its review log records each round. This description lists the permission designs first and the developer experience design after them.

Fixes found in review

  • Four designs follow the cache module change. The module cache declarations, asset routes, no modules crate and client hints designs no longer say that a stock build links a loading cache module or that core holds the publisher's assets.
  • The loading cache and asset cache designs say their code is built, where they said they were written ahead of it.
  • The integrations crate design now counts the designs that name a path under crates/<type>/<vendor>/, 43 of them, and gives the search that finds them.
  • The loading cache, developer experience and module endpoints designs take one more review round, because the last round each had logged had found blocking issues, and the contributing guide asks for rounds until one finds nothing blocking.
  • The developer experience and loading cache designs take four minor points from the confirmation round.
  • The module scripts design's review log cites no unpublished commit.
  • The designs' headers call no ready pull request a draft, in 41 lines of 38 designs.
  • The permission model and provider migration rollout designs take four wording points from the third review of the permission designs.
  • The config migrate design says only stored pages are fetched again after the upgrade, because core holds no copy of a file once the asset cache is a module.
  • Four designs say what their rows hold. The client hints design says it describes the stack from Run request filters and the device module on all adapters #1410 up, the no modules crate design names the order test of the ts list, the Fastly names design names the commit its count is taken at, and the module scripts design's first review names the branch of Move module browser scripts and Tinybird into their crates #1423.
  • One entry of the config by host design's review log takes US spelling.
  • The permission model design says that a load asks for the rules held for the policy a host's settings pin before it reads the file, so an instance that already holds them reads and hashes nothing for that host, as Add the permission model with the Privacy Taxonomy vocabulary #1345 now does (section 3.7 and item 6 of section 3.1). Its line numbers follow Add the permission model with the Privacy Taxonomy vocabulary #1345's change.
  • The asset cache design says that [cache.51degrees-assets-vary] refuses the lifetimes it never reads, as Hold the publisher's assets in the loading cache #1375 now does.
  • The config by host design says that an instance that already holds the rules of a SHA-256 reads no policy entry for it, in its account of each policy file and of the entry that makes a host fail to load, as Add the permission model with the Privacy Taxonomy vocabulary #1345 now does.
  • Four designs whose last logged review round found a blocking issue take three more rounds, the last finding nothing blocking, and each statement the rounds found false is corrected. The consent decoders design's gate paragraph no longer says what an EU signal that only GPP carries does. The device signals design says its line numbers are those of the commit James Rogers approved, and names Run request filters and the device module on all adapters #1410 among the later rows. The publisher object storage origin design says what Allow a signed object storage folder as the origin #1377 does with a URL in a response header and what Map folder origin URLs in headers, add a serve headers step #1378 adds. The asset routes design says a route keeps copies only where a deployment links and selects the 51Degrees asset cache module and the 51Degrees loading cache module, and names EdgeZero v0.0.10.
  • The review round of 27 October is logged in the five designs it covered, and the rounds of 28 October in the four designs they covered.

Dependency update

Kept current

  • A merge of Tech Lab's main at 7aa5c99.

One more commit dates the three designs this update adds 2026-10-27, the day they are proposed, and gives the same date to the status, last updated and revision lines this update writes.

The documents

Spec What it defines Implemented by
2026-08-27-integration-provider-seam-design.md Opening the integration registry, so a vendor integration ships in its own crate with its browser script, its deploy rules, its page changes as middleware, and its identity, location, device, demand and ad server modules carried on its registration #1348, with page changes as middleware in #1351
2026-07-30-pluggable-providers-design.md Edge Cookie, device and location modules, selected by [ec] module, [device] module and [geo] module, and the configuration convention every type follows #1343 and #1344
2026-07-30-permission-model-design.md The jurisdiction permission model, the rules: tree in the permissions file, and permission signal modules run in a configured order #1345
2026-07-30-client-cycle-ec-resolve-design.md Client-cycle Edge Cookie modules and the hardened resolve endpoint #1346
2026-07-30-provider-migration-rollout-design.md Migration and rollout for the module and permission model, with the series' sign-off ledger #1343 to #1347
2026-07-30-integration-response-header-hook-design.md The response-header hook, kept as the design bar for its first consumer Not implemented
provider-code-registry.md The append-only registry of the four-character codes that namespace Edge Cookie identifiers. The file keeps its name so links keep working #1343

The stack's design documents, one for each pull request

This pull request carries every design document of the stack in one place, the seven above and those in the table below. The table gives the pull request that builds each document and the issue it answers. The developer experience design adds no code, and no pull request builds the settings and consumers design yet. The move to the released caching crate (#1372) completes the loading cache and is recorded in the loading cache's document.

Spec What it defines Pull request Issue
2026-10-12-module-context-design.md One module context #1349 #1267
2026-10-12-inspection-endpoints-design.md Inspection endpoints #1350 #1268
2026-10-12-page-middleware-design.md Page changes as middleware #1351 #1269
2026-10-12-config-by-host-design.md One app-config blob for each host #1352 #1270
2026-10-15-fastly-per-host-selector-design.md The Fastly adapter keeps its own selector for a document per host #1352 #1270
2026-10-27-module-endpoints-design.md A module outside core serves an endpoint of its own #1353 Part of #777
2026-10-12-administration-off-the-publisher-domain-design.md Administration leaves the publisher's domain #1354 #1271
2026-10-12-robots-txt-from-modules-design.md robots.txt from modules #1355 #1272
2026-10-12-diagnostic-headers-design.md Headers that describe the deployment stay off a publisher's pages #1356 #1273
2026-10-12-analytics-module-design.md Auction telemetry is a module [analytics] selects #1357 #1274
2026-10-12-cookie-constant-design.md The Model Terms cookie name is declared by the module that reads it #1358 #1275
2026-10-12-inmobi-choice-module-design.md The InMobi Choice consent prompt as a module #1359 #1276
2026-10-12-middleware-media-types-design.md A middleware may run on a media type other than HTML #1360 #1277
2026-10-12-ads-txt-sellers-design.md The sellers a deployment adds to the publisher's ads.txt #1361 #1278
2026-10-12-consent-prompt-phases-design.md A consent prompt runs in the phase the publisher chooses #1362 #1279
2026-10-12-middleware-creates-a-missing-file-design.md A file the origin does not have is made by the middleware named for its path #1363 #1280
2026-10-12-middleware-prepare-design.md A middleware may prepare, with the request's services, and may refuse #1364 #1281
2026-10-12-loading-cache-design.md The loading cache core declares, and the module that supplies it #1365 #1282
2026-10-12-robots-txt-middleware-design.md robots.txt as a middleware #1366 #1283
2026-10-12-shared-vendor-tables-design.md A vendor's modules share one table #1367 #1284
2026-10-12-prefix-module-design.md A module names the browser's cookies and the shared store keys #1368 #1285
2026-10-12-device-signals-design.md A device module may score a person and describe the device #1369 #1286
2026-10-12-viewport-sizes-design.md A format can name the viewport widths at which the page asks for it #1370 #1287
2026-10-12-google-tag-module-design.md A module writes Google's tags on the page from the settings #1371 #1288
2026-10-15-module-cache-declarations-design.md A module declares the caches it loads through #1373 #1320
2026-10-15-geo-module-request-design.md A geo module is called with the request #1374 #1321
2026-10-15-asset-cache-design.md The publisher's assets held by a module, behind one interface in core #1375 #1324
2026-10-13-request-addressing-design.md A deployment may address a request by its path before its configuration is chosen #1376 #1328
2026-10-13-publisher-object-storage-origin-design.md A publisher's site served from a folder of object storage #1377 #1329
2026-10-13-origin-redirect-design.md An origin's redirect to itself keeps the reader on the host they asked for #1378 #1330
2026-10-13-middleware-settings-design.md A middleware reads its settings from a table of its own #1379 #1331
2026-10-14-source-modules-design.md A source module says where the publisher path and the asset routes fetch from #1380 #1332
2026-10-14-rsc-flight-middleware-design.md The Flight rewriter is a middleware of the Next.js module #1381 #1340
2026-10-14-identifier-modules-design.md A module hands core the identifiers it learned for a request #1382 #1337
2026-10-14-consent-decoders-design.md The consent decoders live in the crates named for their standards #1383 #1335
2026-10-14-no-modules-crate-design.md Each adapter lists the modules it links #1384 #1334
2026-10-14-51degrees-client-hints-design.md A deployment using 51Degrees gets client hints from one middleware #1386 #1333
2026-10-14-auction-protocol-design.md OpenRTB is an auction protocol module, and core sends the request a demand source builds #1388 #1336
2026-10-14-last-names-in-core-design.md Small vendor, standard and platform names leave core #1389 #1341
2026-10-14-backend-naming-design.md Each adapter names its own backends #1390 #1339
2026-10-14-origin-signing-design.md An origin signer is a module a source's auth table names #1391 #1338
2026-10-15-fastly-names-in-core-design.md The last Fastly names leave core #1399 #777
2026-10-15-cookies-by-path-design.md Core keeps a publisher's cookies under that publisher's path when one host serves several #1400 #1328
2026-10-17-config-migrate-design.md ts config migrate rewrites a settings document written for main #1409 Part of #777
2026-10-17-request-filters-on-every-adapter-design.md Request filters and the device module run on every adapter #1410 Part of #777
2026-10-17-edge-cookie-on-every-adapter-design.md The Edge Cookie is created and written on every adapter #1411 Part of #777
2026-10-17-asset-routes-on-every-adapter-design.md Asset routes are served on every adapter #1412 Part of #777
2026-10-19-module-scripts-in-crates-design.md A module's browser script lives in the crate that supplies it #1423 Part of #777
2026-10-27-integrations-crate-design.md One integrations crate holds every module, in a folder for its type and vendor None yet Part of #777
2026-10-27-developer-experience-design.md The developer experience, step by step, for a module author, an operator and the release manager None, it adds no code Part of #777
2026-10-16-settings-and-consumers-design.md Every settings section has a consumer, and every consumer has its settings None yet #1403

The one configuration convention

Every type is a top-level table with a selector key inside it, and
[<type>.<name>] holds that selection's settings only when it has any:

[<type>]
module = "<name>"            # a string where one runs, a list where several run

[<type>.<name>]              # only when that module has settings
setting = "value"

The key is module where a type runs one and modules where it runs
several, for every type. [ec], [geo], [device] and [ad-server] take
module, [permission-signal] and [demand] take modules, and a page
integration's section takes whichever fits. A table's name is the
implementation it configures, unless the table carries an implementation
line, which names the implementation by its module path. A demand table
always carries one, because demand is not the type its implementations are
named under, and that is how two instances of one implementation run side by
side under names a deployment chooses. A name is parts joined by ., each of
lower case letters, digits, _ or -. A module from a crate is named by its
folder below crates/, and a [demand] or [ad-server] label of the
operator's own is snake_case.

The specs also state what this buys, which is the part a reviewer should
weigh. A table the selector does not name stops startup rather than sitting
unused, an implementation this build does not have stops startup with the ones
it does have listed, a setting a module does not know is refused rather than
dropped, and demand and ad server endpoints must be HTTPS, or HTTP to a
loopback host. The operator-facing version of the same rules lands with the
code as
docs/guide/configuration-rules.md
in #1348.

The auction in the seam spec

Section 3.4 opens the three things the compiled auction plan keeps in core on
main, being a fixed list of three OpenRTB profiles, special cases for the
prebid-server and aps profiles by name, and adserver_mock as the only ad
server the plan accepts. The ad server, plain OpenRTB, Prebid Server and APS
become implementations that an integration registers on its builder, named
by module path as ad-server.mock, auction-protocol.openrtb,
auction.prebid-server and auction.aps, and a deployment selects them with
[demand] modules and [ad-server] module.
The compiled plan stays, and what it compiles changes from a fixed profile
list to whatever implementations the registered builders offer, so core names
no vendor. An auction-side vendor needs its own crate and a registration, on
the same terms as an identity, location or device module.

The bid renderer contract is unchanged. It is an open descriptor, a type tag
plus the payload the demand provider supplies, with the same serialized form,
so the response a page receives does not change.

The migration and rollout spec records the cost. The auction keys change with
no deprecation period, which is a deliberate choice for a major release, and a
configuration still carrying [auction.providers] or [auction] mediator is
refused with a message naming where the setting moved to.

Page changes as middleware

Section 3.7 of the seam spec. Today a page change is one of four hook traits
an integration implements, run in the integration's one position in the
pipeline, for every document. The spec replaces the four traits and their four
context types with one middleware contract, run only where an ordered
[[fetch]] or [[serve]] entry names it. An entry covers one media type,
optionally only the requests under a path prefix, and lists the middleware in
order, so a publisher can run a change on part of a site, or place two
changes of one integration apart, without touching the auction's priority.
Entries carry no settings and activate nothing, which stays where it is
decided today.

The two phases make the shared-template rule structural rather than
declared. A fetch middleware is handed nothing about the reader, so what it
leaves is what the shared template stores for every reader. A serve
middleware runs per reader on that reader's copy and nothing it writes is
stored. Each integration converts in a change of its own, checked against the
differential harness, and the traits are deleted when the last one converts.

Governance question this raises

The seam spec's section 2 states a principle for the task force rather than for
the compiler. Tech Lab engineering owns core and reviews vendor crates, and
vendors ship and maintain their own integrations. That is the change that
removes the per-vendor core work the project pays for today, most recently the
LiveRamp module (#1054). Opening the auction side widens it, because a demand
source or an ad server is now a vendor crate too.

Declared interest

51Degrees is a vendor and would use this seam for its own integration (#1072).
The reasoning here applies to every vendor on the same terms, ourselves
included, and our own module follows the same route.

The five-PR series (#1043 to #1047) opens the identity, device and geo
seams. The nine vendor integrations already in core sit behind the
integration registry instead, which is a private table, so none of them
can move out until that table is opened.

This spec defines the one core change that opens it: public registration
builders with a second input on IntegrationRegistry, browser JavaScript
carried on the registration, startup validation as a hook, the same
treatment for auction providers and the bid renderer contract, and
neutral replacements for the two places where a vendor reaches into
core. It then sets out the migration of all nine existing integrations,
one PR each. The change is complete in itself: after it, no vendor move
needs a core change.

Written against the series' tree with the file and line references for
every claim about the current code. Documentation only.
… registration capabilities

Adopt the registration shape asked for in the review of PR 1043 and apply
its rule consistently, so a module declares its identity, geo and device
providers alongside its JavaScript and hooks (new section 3.6). Record the
relationship to the pluggable-providers spec in PR 986, reorder the series
so this spec and its core implementation precede PR 1043, and add the
capabilities round trip to the acceptance list.
A probe integration built outside core and registered through an adapter
exercised every seam end to end. Four things surfaced that reading the code
did not: the operator CLI reaches deploy validation through a type that
supplies no builders, so a vendor's own rules are skipped on the path an
operator uses; a carried browser module's hash literal is hand-maintained
and breaks on a checkout that rewrites line endings; a provider is resolved
more than once per request, which the shared-backend principle needs a
per-request context to fix; and one core reader still reads the APS
renderer payload directly. Also records the construction-time verification
of a carried module's declared hash.
The review of #1043 asked that spec changes land before the code that
implements them, so a divergence is a decision taken in review rather
than a ratification of something already merged. PRs #1043 to #1047
each carried the design document for their own step, and #1043 carried
a 607-line spec describing device providers, geo providers, the
permission model and the browser resolve endpoint, none of which is in
that PR.

Move all six series documents here, so this PR carries the complete
normative set and no code:

- 2026-07-30-pluggable-providers-design.md (from #1043)
- provider-code-registry.md (from #1043)
- 2026-07-30-permission-model-design.md (from #1045)
- 2026-07-30-client-cycle-ec-resolve-design.md (from #1046, later
  revised by #1047)
- 2026-07-30-integration-response-header-hook-design.md (from #1047)
- 2026-07-30-provider-migration-rollout-design.md (from #1047)

Each file is taken verbatim at the tip of the stack, so the later
revisions are preserved: the provider-switching continuity section, the
geo requires-signal floor, and the code-envelope paragraph #1047 added
to the client-cycle spec. The revision-record tables are unchanged. No
document's substance was edited.

The only edits are to this spec's own status line, which said the PR
adds one document and that the series specs land with #1047, and a
revision-record row recording the move.
The rule that every trait method needs a production caller in the change that
introduces it is right for a behavioural trait, where a method nothing calls is
dead weight. It is wrong for an evidence interface, and we applied it there and
should not have.

An evidence interface describes what a request carries, not what today's code
happens to read. Held to the caller rule it grows a method each time a vendor
arrives, so a vendor cannot write against it and it cannot stay stable across a
release. It also puts the boundary in the wrong place. What a provider may see
is not the control, because what a provider may do with what it sees is the
control, and that is the permission model's job.

So RequestInfo carries what the request carries. The rule stands everywhere
else.

Addresses: docs/superpowers/specs/2026-07-30-pluggable-providers-design.md
section 4, a rule applied to an interface it does not suit
"Mint" is jargon for something ordinary, which is making an identifier. It was
defined in the glossary, so it was at least explained, but a reader should not
need a glossary for the word that describes the main operation the specs are
about.

Fifty-one uses across five files, including the glossary row itself, now read
create, creates, created or creating. Nothing else changed, so a reader
comparing against the previous revision sees one word substituted and no
argument altered.

Addresses: plain language in documents others read
…style fixes

Delete the reserved 51dd provider-code registry row so the unpublished
vendor provider is not named in the public spec. Correct section 8's
count from four to seven, replace several with many, and restructure
clause-joining colons into separate sentences across the provider specs.
The 51Degrees contribution attribution lines are kept deliberately.
The four series specs (client-cycle EC resolve, permission model,
pluggable providers, migration and rollout) each carried a Status line
saying they were implemented. The code they describe is only in PRs
#1043 to #1047 and none of those is merged, so the line read as shipped
behavior. Each now says Proposed, names the PR that carries the
implementation and states that it is not yet on main, keeping the
existing revision dates and notes.

The integration provider seam spec carried counts and line references
that do not hold on main at d516a9e. Corrected against that commit:

- Section 4 said migration_guards.rs embeds "the thirteen vendor
  files". The directory holds 23 .rs files (2 infrastructure, 6 in
  nextjs/, 2 in datadome/, 13 top-level integration modules), the guard
  embeds 20 of them and 9 of those 20 belong to the nine vendors, with
  osano.rs and the two datadome/ files absent. builders() registers 13
  integrations, which is a different 13 from the file count.
- Section 3.5 gave no counts for the prepare and finalize calls. There
  are nine production prepare_request call sites across the four
  adapters and a tenth in core, and the single production
  finalize_response call site is in core rather than in any adapter.
- Section 8 item 3 described a proxy resolving geo twice, which does
  not happen on main. The real double resolution is the adapter EC
  context build against handle_auction on POST /auction.
- Section 8 item 5 understated the Spin gap and misdescribed
  Cloudflare. Cloudflare covers every route it registers and has no
  health route, while Spin skips its first-party bindings as well as
  its inline admin stubs.
- Line references: settings.rs:166 to :215, auction/mod.rs:49 to the
  list at :51 to :53, publisher.rs:4361 to :4369.

Section 6 now requires the round trip to be proven on the Fastly
adapter, the primary deployment target, rather than on any adapter,
because Fastly has no library target and the round trip otherwise only
runs on the Axum dev server.
Rewrite the resolve endpoint's origin check as an exact serialized-origin
comparison, defaulting to the single origin https://{publisher.domain}
with an optional operator-configured list, and delete the sibling
subdomain justification that did not hold. Record that the origin check is
defense in depth under the provider's envelope verification, require the
sibling subdomain, wrong scheme and non-default port rejection cases, and
close open question 7.5.

Require core to expire the ts-ecr marker on any request carrying a ts-ec
the selected provider does not own, in both the client-cycle and pluggable
provider specs, so a provider switch restarts the client cycle instead of
leaving a visitor with a marker and no identity. Record the constraints on
carrying resolved permissions to the browser without designing the
mechanism, which belongs with the first vendor module.

Require a nonempty DeviceProvider::required_permissions from a
module-supplied provider to be refused at registry resolution rather than
described as a documented no-op.

Replace the DataDome singleton-header rule that skipped the vendor call on
a repeated field, which would have created an attacker-controlled bot
detection bypass and described no shipped behavior. Core now passes
repeated field lines through as received and leaves interpretation to the
vendor integration, keeping only the protocol-level content-length rule.

Correct the APS migration row and add a seam spec finding for the
pre-existing browser-side APS coupling in core TypeScript.
Sign-off row 6 said no vendor move needs a core change after this design.
Section 8 item 8 now records that TSJS core still imports the APS renderer
directly, so an APS move needs a browser renderer contract that this design
does not provide. Row 6 now claims only the Rust side, which is what the
design actually delivers.
…wording claims

Prettier realigns table columns in six specs that the 1 September review edits widened, which the format-docs workflow checks. The client-cycle spec no longer says the origin check normalizes to an effective port, because origins_match compares the port as the browser wrote it and adds or removes nothing. The response-header-hook spec no longer claims Trusted Server rejects a malformed or body-inconsistent content-length, because no core code does that. It is HTTP framing handled by the host HTTP layer, and the point that matters is that no integration reads the value.
The client-cycle spec described the origin check as a string comparison of the serialized origin. The implementation on the client-resolve branch now applies the RFC 6454 same-origin test, comparing the scheme, host and port triple with a missing port meaning the scheme's default, so the spec names that rule and the RFC rather than describing a comparison of its own.
…s and the page

The pluggable-providers spec said a geo declaration is refused like a device one, but nothing consults PlatformGeo::required_permissions and its own body says so, so the summary now names device only. The permission-model spec records that a module device provider may not declare a permission, because the registry refuses one at startup. The client-cycle spec says the demo page script now declares and checks its required Data Use through tsjs.whenPermissions(), that the head script needs a head to inject into, that a bot-classified visitor receives the state resolved for the request rather than an empty one, and what the promise resolves with when nothing arrives.
The rules section of permissions.yaml becomes one tree, and [geo]
default_country retires. Every node carries a group with optional children,
country codes sit at the first level and region codes beneath them, a node
written as a single string is shorthand for that group with no children, and
matching runs region, then country, then the top node. The top node is the only
one that also requires a jurisdiction, whose values come from the Jurisdiction
type in crates/trusted-server-core/src/consent/jurisdiction.rs, and it states
the baseline and the consent handling for a request whose country cannot be
resolved.

The permission model spec carries the change in its format, validation,
resolution and failure-mode sections, with the canonical example and a new
section 12 recording each before and after row and the date. The providers and
rollout specs follow, where the configuration example, the startup-error rows
and the migrated operator configuration drop default_country and point at the
top node instead. Historical ledger rows keep what earlier pull requests
implemented and gain a note naming the restructure, so the record still reads
true.

The audience for permissions.yaml is a policy owner rather than a developer, so
the nesting shows precedence on the page instead of hiding it in slash-joined
keys. Trusted Server encodes no jurisdiction's law, as the deployer states the
policy and the software only carries it out.
Jurisdiction::from_policy_name in
crates/trusted-server-core/src/consent/jurisdiction.rs reads a US state as
us-state/<CODE>, for example us-state/CA, rather than the shorter form the
format section first listed. The section now names the four accepted values as
the parser spells them and records that any other value is a configuration
error.

Trusted Server encodes no jurisdiction's law, as the deployer states the policy
and the software only carries it out.
The rules tree carries jurisdiction on any node, not only at the top. A node
without one inherits the nearest ancestor that has one, the top node must carry
both group and jurisdiction so the inheritance always terminates, and group and
jurisdiction fall back independently, so the two answers for a visitor need not
come from the same node. The us-state value drops its state code, because the
region node it sits on already names the state, which also makes the value
invalid at the top and on a country node.

Section 3.4, which recorded one source of jurisdiction truth as deferred, is
now adopted. consent.gdpr.applies_in and consent.us_states.privacy_states
retire into the tree, where the shipped file holds the same 31 GDPR countries
inheriting gdpr from the top node and the same 20 states with a comprehensive
privacy law as region children of US carrying jurisdiction: us-state, so the
drift the draft named has nothing left to drift against. The consumer side
stays open, as auction dispatch still reads the consent subsystem's gate.

The format, validation, resolution and failure-mode sections carry the change,
section 12 gains a row for each of the three extensions, and the providers and
rollout specs pick it up where they describe the top node and the migrated
operator configuration.

Trusted Server encodes no jurisdiction's law, as the deployer states the policy
and the software only carries it out.
A geo provider that reports an outright lookup failure resolves every
permission to the requires-signal floor and its jurisdiction to unknown, rather
than reaching the tree at all. Section 5.2 and the failure-mode matrix now say
so, matching what permissions.yaml states for the same case.

Trusted Server encodes no jurisdiction's law, as the deployer states the policy
and the software only carries it out.
…named owners

The 3.6 list loosely named the host geo lookup among the platform services, which contradicted the design's own rule that geo is a provider a deployer selects, with a host lookup only one data source an implementation may consume. The 4 migration rule now also requires each migrated vendor crate to carry a visible maintainers declaration, the way Prebid.js requires of every adapter, and per-crate code ownership, so a crate boundary carries a named owner from its first day. The deployer selects providers, and the software encodes no vendor's or jurisdiction's choice.
Section 3.6 keeps its rule and now records that the client IP and the
TLS, JA4 and HTTP/2 signals are candidates to migrate behind an EdgeZero
evidence contract when one exists, which the review discussion on this
PR proposed. Also replace a clause-joining semicolon in the same
paragraph with a full stop.
Both specs still placed the permissions file at the repository root
after the move to config/permissions/vanilla.yaml on the permissions
branch.
The permission signal providers in #1045 replace the fixed rule, where
an opt-out always beat a consenting TCF record, with the order a
deployment configures in [permission_signal] sources. The spec still
described the retired rule, so it now states what the implementation
does.

Section 4 describes the resolution steps, how the order is configured
and its default, and the decision matrix under that default. The
overview, policy format, validation, withdrawal, normalization, US
field mapping, failure-mode matrix, testing strategy and the
divergence and revision records follow it, and a new section 13
records each change. The sample policy's opt-out revoke list is also
described as it is, a list of specific Data Uses rather than all.
Section 11 said the US opt-out revoke set ships as all, and the example
policy in section 3.2 showed revokes: all. The shipped
config/permissions/vanilla.yaml lists device storage, sale, sharing,
third-party sale, first- and third-party targeted advertising, and
profiling, as section 4.5 already says. The example and the section 11
row now match the file.
The implementation renames `config/permissions/vanilla.yaml` to
`config/permissions/sample.yaml` and says in the file, the guides and on the
constant that the sample is for testing and evaluation only, is not a
production policy and is not legal advice. Both specs that name the file follow
it, and the two places that introduce it now carry the same qualification, so a
reader of the spec is told what the file is before deciding to run it.

The permission model spec also drops the phrase "sample policy" for plain
"sample", because calling it a policy is the reading the rename exists to
prevent.

Tests. Prettier accepts both files under the docs configuration, checked with
the same `prettier --check` the `format-docs` job runs.
PR #1016 replaced the auction provider table that section 3.4 opened with
a compiled auction plan, and the seam implementation follows that plan
rather than adding an auction provider builder. Only the generalized bid
renderer survives from section 3.4.

- Section 3.4 says the change does not open the auction, keeps the
  renderer contract, and records what the plan keeps closed, being the
  fixed profile registry, the profile special cases in the plan compiler,
  and the single mediator the plan accepts.
- Section 1 item 4, section 3.1, section 3.3, the APS row in section 4,
  section 8 item 4 and section 9 row 6 no longer claim an auction provider
  seam, and section 3.1 records that the Prebid and APS ids are reserved
  by the duplicate id check.
Every pluggable thing is a provider, each provider type is one top-level
table, and a `provider` key inside that table selects what runs. The series
specs still described the shapes that convention replaces, so a reader
building against them would have written configuration the stack rejects.

The providers spec now states the convention once, in a new section 2.1, and
the other specs follow it. The seven types are `ec`, `geo`, `device`,
`permission_signal`, `demand`, `adserver` and `integration`, all singular and
snake_case. `provider` takes a string where one runs and a list where several
run. A `[<type>.<name>]` table exists only where that provider has something
to set, the word `providers` appears nowhere in configuration, and the name is
the implementation unless the table carries `implementation`, which is how two
Prebid Servers run side by side under different names. A table its type's
`provider` does not select refuses startup, an implementation the build does
not carry refuses startup with the list of what it does carry, a demand or ad
server endpoint must be HTTPS or HTTP to a loopback host, every provider
rejects settings it does not know, and a secret setting names a key in
`trusted_server_secrets` rather than holding a value.

What changed, by shape:

- `[ec.providers.<key>]` becomes `[ec.<name>]`, and the hyphenated selection
  names become `host_signals`, `client_fixed`, `gpp_sale_opt_out` and
  `us_privacy`. The `client-fixed-demo` cargo feature keeps its name, because
  a cargo feature is not a provider name.
- `[permission_signal] sources` becomes `[permission_signal] provider`, the
  list form of the same key every other type uses.
- `[integrations.<id>]` becomes `[integration.<id>]`, and which vendors run is
  `[integration] provider`.
- `[auction.providers]` and the word mediator are gone. Demand and the ad
  server are provider types in their own right, so the seam spec section 3.4
  now shows `[demand]`, `[demand.<name>]`, `[adserver]` and the
  `[auction.bidders.<code>] provider` mapping that stays. APS stopped being an
  integration, so it is configured under `[demand.<name>]`, carries its
  `rendering_mode` there, and can never be named in `[integration] provider`.
- `[auction]` is not a provider type and keeps `enabled`, `timeout_ms`, the
  creative settings and `allowed_context_keys`.

All three specs that describe the seam now say the same thing about it. Core
names no vendor, an implementation is registered by an integration builder,
and a builder that supplies an implementation does not have to be a page
integration. That is how the HMAC identity provider, the User-Agent-only
device classifier and the four permission signal schemes reach configuration
without any of them shipping browser JavaScript or being named in
`[integration] provider`.

Left as they are. The permission model's record of the draft's unadopted
`regime` class still names `us-privacy`, because that is a class name from a
proposal that was rejected rather than a configuration key. The
`signals.us_opt_out.sources` list is policy inside the compiled
`permissions.yaml`, not a provider selector, so it keeps its name.
`provider-code-registry.md` needed no change, because it allocates
four-character identifier codes and describes no configuration.

Verified. `prettier --check` under the docs configuration, the same check the
`format-docs` job runs, passes on all seven files. Every markdown link,
document reference and section cross-reference in the changed files resolves,
apart from one that was already broken, being `docs/guide/permission-model.md`
in the permission model spec, which exists further down the stack but not on
this branch, because this branch targets main directly.
Two rows of the decisions table joined their clauses with a colon and a
semicolon. They now read as one sentence each, and the table is
realigned to the widened cell.
The loading cache, asset cache, integrations crate and module cache
declarations designs end with a review log entry for each round they
took, as CONTRIBUTING.md asks.
The module cache declarations, asset routes, no modules crate and
client hints designs no longer say that a stock build links a loading
cache module or that core holds the publisher's assets, following the
loading cache and asset cache designs. The ts command's list carries
no loading cache module, the Axum probe test links core's
MemoryLoadingCache beside the probe's module and a fourth case links
none, the asset routes offer a GET to the module [asset-cache]
selects and keep no copy in core, and the 51Degrees cloud answer is
held only where a deployment links a loading cache module.
The loading cache design describes the change to rows 24, 32 and 43
as built, with line numbers still those of the code before it, and
the asset cache design's header says that row 34 now holds the
module, where both said they were written ahead of the code.
43 of the other design documents name a path under
crates/<type>/<vendor>/ at this revision, not 41, because the
developer experience and module endpoints designs name such paths
too. The design gives the search that produces the count.
The developer experience design gives the sample policy as 24,271
bytes, its size at row 4. The loading cache design says that the
section 9 search prints six lines at the head of row 55 and, at the
published commit it takes line numbers from, those six and the ten
stock-list lines the change removes, that the change rewrote the two
sibling designs on this branch, and that row 32's rewritten probe test
has four cases. The round 4 and round 5 entries of their review logs
end with none blocking and no minor point.
The round 1 entry no longer names the first commit of
fixpass11/55-scripts-in-crates, which was rewritten before it was
pushed and is on no branch.
Forty-one Status and header lines in 38 of our designs called a pull
request a draft when it is ready for review. Each now says pull request.
Lines naming a pull request that is still a draft keep the word, being
the fork's jwrosewell#36 and #43, row 55 (#1423), and
Tech Lab's #1194 and #894. Tech Lab's own documents are not touched.
…deployed

No build compiles a permission policy in. A domain's settings name the
publisher's policy file in `[permissions] policy` and record its SHA-256,
`ts config push --permissions <file>` supplies the file, and a domain
whose file is missing, unnamed or a different file from the one pushed
answers 500 and logs the reason.

- The permission model design says how the file travels on each adapter,
  in the blob from #1345 and as one shared entry
  per file on Fastly from #1352, the 60 seconds a live push waits between
  a new entry and the blob from #1354, what a release build carries, and
  what reading the file costs.
- The config by host design adds the shared policy entries, one for each
  file under its SHA-256, each written before the blobs that name it.
- The inspection endpoints design shows the policy's name and digest and
  never its text.
- The config migrate design leaves naming the policy file to do, on every
  adapter, and a document written for main may name it before or after it
  is migrated.
- The provider migration rollout and response header hook designs follow
  the same model.
The permission model design's list of scripts carried whole now names
only those of the crates a release links, and says that at row 55 the
seam probe carries its js/probe.js whole too, in a crate each adapter
lists as a dev-dependency alone, so the script never reaches a release.
The Cloudflare item names the adapter's own test of its manifests,
where the sentence had it read as the README's. The template build
digest names the build-support file the build script includes, as
CRATE_INPUTS lists it. The rollout design names the commit of main at
which it cites the line that carries deny_unknown_fields on Settings.
The permission model, response header hook, inspection endpoints,
config by host and config migrate designs end with an entry for each
of review rounds 3, 4 and 5, and the provider migration rollout design
gains a review log with rounds 1, 2 and 3, as CONTRIBUTING.md asks.
Each entry says what the round found and what changed after it. The
last round approved the permission model and rollout designs with none
blocking, and their entries name the wording points fixed after it. It
found no issues in the other four.
The round 5 entries of the permission model and config migrate designs
say how the text was brought into line with the code before the round,
so their round 3 entries read as records of that round. The response
header hook's round 5 entry names the commit its section 1a states.
Step 7 of the upgrade steps said the publisher's stored assets are also
fetched again because their keys hold a code worked out from the
settings. Core holds no copy of a file once the asset cache is a module,
and the asset cache module's keys hold no such code, so the step names
the stored pages alone.
The client hints design says it describes the stack from row 52, where
every adapter runs request filters, because below that row only the
Fastly adapter runs them. The no modules crate design names the order
test the ts list has. The Fastly names design names the commit its count
of 173 lines is taken at. The module scripts design's first review names
the row's branch rather than a local one.
Row 31 moves fiftyone-caching from 4.5.8 to 4.5.13. In the loading
cache design, the paragraph on the pin, the table of how rows 24 and
31 differ and the source link say so, and the published 4.5.8-49 and
4.5.13 are compared file by file. The integrations crate design's
license sentence names 4.5.13.
The three designs added in this pass are named for 2026-10-27, and
every status line, last updated line and revision row that said
2026-10-26 says 2026-10-27, with the links to the three renamed files.
A load asks for the rules held for a pinned digest before it asks a
source for the text, so an instance that already holds a host's digest
reads and hashes nothing for it. Section 3.7 and item 6 of section 3.1
of the permission model design say so, and the line numbers cited in
permissions.rs and config_payload.rs follow the fix in #1345. The config
by host design says that such an instance reads no policy entry for the
host, in its account of each policy file and of the entry that makes a
host fail to load.
A lifetime set in [cache.51degrees-assets-vary] is refused when the
settings load and by the deploy check, a fix found in review in #1375.
The caches the module declares, failure handling, the rollout of a
document written for row 34, the module's tests and what no test
covers say so.
A review of this update's changes on 27 October found nothing blocking.
Its round is logged in the permission model, config by host, asset cache,
loading cache and integrations crate designs. The config by host design
says that an instance already holding a SHA-256's rules reads no policy
entry for it, where it said such an instance reads nothing.

A round on 28 October read again the four designs whose last logged
round had found a blocking issue, being the consent decoders, device
signals, publisher object storage origin and asset routes designs,
against the code of their rows, and two lighter rounds then read the
fixes, the last finding nothing blocking. The issues they found in
these designs are fixed here as fixes found in review, with revision
rows and the three rounds' log entries in each design.

The consent decoders design's gate paragraph no longer says what an EU
signal that only GPP carries does. The device signals design says its
line numbers are those of the commit James Rogers approved, and names
row 52 among the later rows. The publisher object storage origin design
says what its row does with a URL in a response header and what the
next row adds. The asset routes design says a route keeps copies only
where a deployment links and selects the 51Degrees asset cache module
and the 51Degrees loading cache module, and names EdgeZero v0.0.10.

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.

2 participants