Skip to content

Add the integration seam design specs - #1342

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

jwrosewell wants to merge 179 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 57 files, 9171 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, and each design a pull request of the stack links as its spec ends in its review log.

Test plan

CI on this pull request at a854526 passed all 23 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 a854526

Checklist

  • 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.

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.

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, and the last document has no pull request 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-12-attestation-design.md Attestation #1353 #161
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 through the loading cache #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-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.
… fetch's timeouts

The design said an origin fetch has 15-second timeouts, and now says core asks for them and only the Fastly adapter applies them, the Axum client applying its own limits and the Cloudflare and Spin adapters none of their own.
The registry selects SelectedPrefixModule::default() and the trusted_server() constructor is gone, which ends the two code scanning alerts, so the first open question keeps only whether Prefixes::trusted_server should take the publisher domain alone.
…ines

No debug line for a skipped EID carries the source the reader sent, and the line for a partner's EID names the partner's configured domain, with the test that shows it.
A handler is removed only where no quantifier, class or comment follows its prefix or bound, the region the Cloudflare lookup now reads is reported to check, main's jurisdiction check is described as it was, one document run on two adapters is covered, and the review log records round 2.
The specs copy had stopped at the round 2 review of 15 October. It now carries the x-geo response headers written by the headers.geo serve middleware, the Axum and Spin refusal of the platform lookup, and every adapter calling detect and writing the request filters' response headers, as the branches do, with its review log last.
Every named route in the request cycle is shown to run the filters on Axum, Cloudflare and Spin, the design names what a device module is shown and what an unreadable Cookie value loses, gives a true reason no shipped filter is tested through an adapter, names the two DataDome designs whose scope this lifts, and its review log records round 2.
The wait on Axum when the service cannot be reached and the targets its tests skip are corrected, and the review log records the review of the change that has every adapter run the filter.
The specs copy now carries the 16 October fix, in which the rewriter moves each address once in one pass, with the tests that show it.
The specs copy now carries the 16 October code fixes for the rollout and privacy finding of review round 1, in which a withdrawal on an addressed request also expires a pull-sync marker under the root path and tombstones the row of each Edge Cookie the request carried, and the two open questions they raise.
The specs copy now carries section 2.6, in which core names no hosting platform or platform domain in any line, with the counts, the _vercel prefix, the guard and the tests that moved to the Fastly adapter.
The specs copy now carries the 15 October code fixes and the 16 October section in which cache headers follow the stored item's policy, which closes the open question on the cache headers.
A graph configured later replaces only a server-side module's cookie, and a client-side module's cookie keeps its identifier with no row, at the cost the design states. The change discloses more in three cases, the client-side module on a host with no graph being the third, each adapter's resolve route is shown to set a client-side module's cookie, and the review log records round 2.
…exist

The design said both caches are built with detachable: false, a field no code has. It now says a load runs in the request that started it, as every load of the loading cache does.
The Cloudflare and Spin adapters install a logger of their own, so the warning for a route that enables the Image Optimizer and the error for Spin's refusal of a host_header naming another host reach each host's log, and the design says where. Spin's refusal is marked breaking for the publisher path too, the warning is weighed against the auction refusal, and the review log records round 2.
…stions

The configuration guide says core applies no lifetime of a declared table and when to push one, the integration guide lists .with_cache_features, and the two cache declaration checks are pub(crate), so items 5 to 8 leave the open questions. Section 2.7 and section 3 also say the deploy check refuses a module that offers no loading cache, as it has since the loading cache change.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
A load that fails asking the origin is the answer to every request that ran or waited on it, so the origin is asked once, and the design's failure handling, the cost of a slow origin and the tests say so.
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
The origin redirect design describes the header rewrite merged in main (#1238) with the origin folder that #1378 gives it, in place of the Location rule #1378 no longer has, and records the five deliberate faults its tests catch. The object storage design's sentence on redirects names the mapping it now follows. The same text is on the branch of #1378.
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:22
The design of row 55, #1423, joins the specs
pull request that carries every design of the stack, with the same
text as on that row's branch.
The design now says why the Rust discovery tests run without a file
system, the same text as on the branch of #1423.

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