Repository navigation
Conversation
What an operator selects to supply a capability is a module, and provider keeps the one meaning it has on main, an auction provider instance. This renames the words this branch adds on top of the Edge Cookie seam, which already says module, and only those words: every provider word was compared with upstream main, and a word that exists there is untouched. - DeviceProvider is DeviceModule, and BuiltinDeviceProvider, FastlyDeviceProvider and the test stand-ins are BuiltinDeviceModule, FastlyDeviceModule and so on. DeviceConfig's provider_key is module_key, and build_device_provider and build_geo_provider are build_device_module and build_geo_module. - HostSignalProvider is HostSignalModule, HostSignalsProviderConfig is HostSignalsModuleConfig, and HOST_SIGNALS_PROVIDER_KEY is HOST_SIGNALS_MODULE_KEY. - The [device] and [geo] selectors read module, as [ec] does, so the environment overrides read TRUSTED_SERVER__device__module and TRUSTED_SERVER__geo__module, and labeled_provider_block_names is labeled_module_block_names. - Test helpers and test names say module, and the plain word says module wherever it meant a device, geo or Edge Cookie module, in the core code, the adapters, the device and geo crates, the example settings, the fixtures and the guides.
Brings the refreshed device and geo branch, and through it the Edge Cookie seam and upstream main at 7a0ecb4, into the permission model branch. The lower branches' names say module, so every line here that used one of those names says module too, and most conflicts settled as that rename applied to this branch's own text. The rest, settled by hand: - Upstream removed the legacy consent store, so the consent pipeline's ec_id and kv_store inputs, its read fallback, its KV pipeline tests and the Fastly test helper that named a missing consent store are gone, and this branch's default jurisdiction input stays. The consent-era tests of allows_ec_creation and gate_eids_by_consent do not return, because the permission model replaced those functions here. - Withdrawal is the permission signal modules' answer on this branch, so the pull sync marker expiry upstream added reads storage_withdrawn, and the consent-derived flag upstream computes above the gate goes. - The generated-cookie gate test is upstream's two-case table, worded for the permission gate. - build_pull_sync_context keeps this branch's sharing gate inside upstream's new signature, which takes the partner registry and returns nothing when no partner is pull-enabled. - The test services keep this branch's geo helper beside the renamed Edge Cookie helper, and the publisher keeps gate_eids_by_permissions with upstream's sharedid cookie import.
The one-time warning told the operator that the jurisdiction would be unknown and Edge Cookie creation would fail closed. On this branch neither is true. With no location the caller supplies the declared jurisdiction, which is the permission policy's top node, so identity and the auction both keep running. The warning described behavior from before the policy tree existed. That matters because it is the line an operator reads when advertising stops. Being told identity is blocked when it is allowed sends them looking in the wrong place, and a deployment that selects no geo module sees this message on every start. It now says what happens rather than only what is missing. The jurisdiction is the declared one rather than one resolved from the request, country and region rules cannot match, and a lookup that fails, as opposed to resolving nothing, is the case that does fail closed.
A permission whose baseline requires a signal, and for which every configured module answered neutral, was simply not in the resolved set, so a page could not tell "nobody has answered yet" from "refused". A prompt that never loaded then looked like a visitor who said no. PermissionState gains awaiting, the permissions in exactly that position, recorded by resolution alongside the set. It reaches the page as a third key of window.tsjs.permissions beside set and tdls, and the page core normalises it like the other two. Nothing yet acts on it. That is the next step, the page core holding what depends on an awaited permission.
The awaiting list said every permission that requires a signal and got none. Some of those no module in the deployment could ever grant, the marketing channel opt-ins in the shipped sample policy for one, and a page told to wait for them would wait for ever, which is the defect the list exists to remove. Each module now declares, through grants on the trait, which permissions it can answer Grant for under the policy it is given. An opt-out declares nothing. TCF declares every Data Use a purpose maps to, and nothing when the policy silences the record. The assembly narrows awaiting to the union of those declarations, so a permission nothing here could grant is simply unset. Start-up compares the policy against the declarations and warns, naming every permission the policy requires a signal for somewhere that no configured module can grant. It warns rather than refuses, because the shipped policy itself carries two such permissions and a deployment that never uses those channels is not misconfigured. PermissionSet gains intersect, union and without. PermissionMaps gains requires_signal_anywhere, the union over the whole tree. The guide and the seam's README describe awaiting.
…alid ones Core no longer answers for any scheme. The override that revoked everything ahead of the modules when a record arrived unreadable is gone, and with it ConsentContext::has_malformed_record. What an absent, unreadable or expired signal means for the permissions is the decision of the module for that scheme, taken in its own answer and taken silently. The four shipped modules read their own unreadable record as the refusal it may have carried, on the permissions their scheme covers, and say nothing about any other scheme, so the configured order decides a readable record from one scheme beside an unreadable one from another, as it decides everything else. The permission state gains signals, one ValidSignal for each signal a configured module read and found valid, as received, with the module and the scheme. The page reads it as the fourth key of window.tsjs.permissions. A signal nobody vouched for goes no further, because after assembly the consent context keeps only the raw strings the valid signals name, so a corrupt or expired record never reaches a bid request. The start-up comparison of the policy against the modules, added in the previous commit, is removed. Nothing is logged about a signal.
…ookie A fifth permission signal module, crates/permission-signal/mtm, identifier mtm. It reads the answer a preference platform recorded, one of the three words standard, personalized and non-marketing, from the first party cookie __mtm_pref. The name carries no vendor in it, so any platform that asked the visitor may set it, and Trusted Server reads the cookie and never writes it. With no word present the module says nothing, so the country and region rules stand, which is clause 5.3 of the Model Terms. standard and personalized both grant storage, contextual advertising, measurement, content performance, market research, product improvement and basic content selection. Only personalized grants the four Data Uses that depend on browsing history or interactions, and standard refuses them rather than leaving them alone, because the visitor chose against them. non-marketing changes nothing: the visitor named neither kind of marketing, so the module has no marketing choice to act on and the country and region rules decide. Under a group where every marketing Data Use requires a signal nothing is granted, and under one where they are granted the baseline applies. A deployment that wants a decline to bind there sets that in its own permissions policy. No word withdraws. The word is the valid signal, as received, and whenever one is present the module declares the versioned Model Terms document as the terms the data is available under. Any other value is treated as absent. Every adapter offers it last, after the four IAB schemes. The TCF module's declaration of what it can grant is computed once rather than on every request. A test over a platform's real Set-Cookie strings pins that the cookie is never HttpOnly, because a browser lets a script neither read nor replace such a cookie and the platform that owns the question would stop seeing its own answer.
What an operator selects to supply a capability is a module, and provider keeps the one meaning it has on main, an auction provider instance. This renames the words this branch adds on top of the device and geo seams, which already say module, and only those words: every provider word was compared with upstream main, and a word that exists there is untouched, as is the icu_provider crate the permissions inspector's lock file names. - PermissionSignalProvider is PermissionSignalModule, and GpcProvider, GppSaleOptOutProvider, UsPrivacyProvider, TcfProvider and MtmProvider are GpcModule, GppSaleOptOutModule, UsPrivacyModule, TcfModule and MtmModule. - build_permission_signal_providers is build_permission_signal_modules, the adapters' shipped_signal_providers is shipped_signal_modules, and the permission_signal_providers field and arguments say modules. - The [permission_signal] selector reads module, as the seams' selectors do, so the refusal of the removed sources key names module. - Test doubles, test helpers and test names say module, and the plain word says module wherever it meant a permission signal, geo, device or Edge Cookie module, in the core code, the five module crates, the adapters, the sample policy, the fixtures and the guides.
The five permission signal crates carry their own unit tests, and no CI job ran them, because the test-axum alias covered the adapter crate alone. The test-axum and clippy-axum aliases now name the five crates as well, so the Axum job tests and lints them.
Comments say what the code does. The "today" qualifiers go, along with the claim that no shipped geo module reports a lookup failure and the counts of shipped schemes, which change as schemes are added. Device storage gates every shipped Edge Cookie module, not only the built-in one. The fifteen tests of how from_yaml refuses a malformed policy are one table, each case named with the refusal it must report, so a new refusal is one row rather than a new test shaped like the last one.
Brings the refreshed permission model branch, and through it the device and geo seams, the Edge Cookie seam and upstream main at 7a0ecb4, into the client-resolve branch. The lower branches' names say module, so every line here that used one of those names says module too, and most conflicts settled as that rename applied to this branch's own text. The rest, settled by hand: - The Fastly entry point keeps this branch's resolve route import beside upstream's EcContext and EidSyncSource imports, and constants keeps the resolved-cookie marker beside upstream's pull-complete cookie. - The seam keeps this branch's client_fixed implementation id and its retired spelling, with the built-in list naming all three ids, under the trimmed docs the seam now carries. - The settings keep this branch's refusal of the client-fixed spelling and its startup compilation check, with the host-signals refusal gone as the device and geo branch removed it. This branch's own names still say provider, and are renamed in a commit of their own.
The sandbox tests load settings that select the HMAC Edge Cookie module through the deprecated passphrase form and no geo module. On this branch that pair needs the single-jurisdiction acknowledgment, as every other test settings document in the adapter carries, so without it the settings refuse to load and the test traps under Viceroy before it starts.
An [ec] resolve_allowed_origins entry such as "https://www.example.com/" loaded cleanly, then matched no request, because origins_match treats any value that is not a serialized origin as matching nothing. Every POST to the resolve endpoint from that origin answered 403, with only a warn line saying why. The parse inside origins_match is now parse_serialized_origin in ec/resolve.rs, and it returns the reason a value is not a serialized origin. origins_match reads the request's Origin and each entry through it, and Ec::validate_resolve_allowed_origins refuses an entry it rejects, naming the entry and the reason. The check runs in Settings::finalize_deserialized beside the module selection check, which covers from_toml, from_json_value and the runtime config store load, and in validate_settings_for_deploy_with, so ts config validate and ts config push refuse the entry before it can stop an instance loading its settings. parse_serialized_origin is stricter than the old closure on both sides. It requires http:// or https:// and then nothing from a slash, a backslash, a question mark, a hash, an at sign, whitespace or a control character before handing the value to the url crate for the host and port. The old closure let through userinfo, the ftp, ws and wss schemes, a path that normalizes to a slash, a backslash, a missing double slash and surrounding spaces, each of which matched itself. A browser sends only http or https as the Origin of a page's POST, so no real request that matched before stops matching. The field doc, docs/guide/configuration.md and trusted-server.example.toml say each entry must be a bare origin and is refused otherwise.
handle_ec_resolve rebuilt its module with build_module, passing the module the composition root had threaded into the request as the injected one. On Fastly that is the built-in module itself when hmac is selected, so build_module found the built-in name claimed twice and the endpoint answered 500 for a valid selection, where it should answer 204 for a module that creates nothing from a client post. The same happened to client_fixed in a build that compiles it, which is the one built-in module whose page script posts here. The endpoint now uses EcContext::selected_module, the module the request's own context resolved. The context's host signal and injected module fields existed only for the rebuild, so they and their accessors are removed. The resolve tests built contexts with no module and relied on the rebuild, so their helper now resolves the selected module the way a request's context does. Tests resolve_answers_204_for_a_threaded_module_that_creates_nothing and resolve_sets_the_cookie_for_a_threaded_client_fixed_module read a context with the module threaded through the services, as Fastly does, and fail without the change.
The Edge Cookie settings refused the old client-fixed spelling with a message naming the spelling to write. No configuration in this repository uses it, so the constant, the refusal in validate_module_selection, names_retired_provider, the test of the refusal and the doc paragraphs describing it are gone. An old spelling now fails as any unknown implementation does when the module is built at startup, with a message listing the implementations the deployment has, which an_unknown_implementation_fails_naming_the_known_ones covers.
set_provider_ec_cookie validated and wrote the same cookie as set_ec_cookie, which no longer checks the HMAC identifier shape either, so the resolve endpoint had a second copy of one function. The copy is deleted, handle_ec_resolve calls set_ec_cookie, and the client_fixed module's doc links to it. The three tests of the copy fold into the set_ec_cookie tests, as two tables. One writes an HMAC identifier, an opaque value and a base64url value verbatim with the security attributes, and the other refuses a semicolon, a carriage return and line feed, a line feed and a space with no cookie at all. The second fails when set_ec_cookie stops validating.
Both pages failed the docs prettier check in CI. This is prettier's own output, with no change to the wording.
The inspector called ConsentContext::has_malformed_record, which went when each permission signal module took over the meaning of its own unreadable signal, so the inspector's wasm build failed in CI. The field is removed rather than replaced, because nothing at this level can answer it and reporting false would read as none having been found.
Ten resolve tests built the same request and context and differed only in a value and the answer they expected. They become two tables that name the case in every assertion and keep every case. resolve_answers_204_with_no_cookie_when_it_creates_nothing covers the closed permission gate, no module, no identity graph and a value that fails verification. resolve_refuses_a_request_it_cannot_take covers no Origin, a foreign origin, a sibling subdomain, plain http, a port, a form body and a body over the limit, and now also asserts that each sets no cookie. Both fail when the origin and gate checks are taken out.
The workspace pins the edgezero adapters to a revision rather than the v0.0.8 tag, and the inspector resolves against the same crates, so its own lock follows.
The header double's mint flag becomes the identifier it returns, so a test can post an identifier the cookie cannot carry, and a second helper takes a request the test builds itself. Three tests are added, each failing with the behavior it covers broken: an identifier outside the cookie bounds is refused with 400 and leaves no cookie and no identity-graph row, a failed identity-graph write answers 503 with no cookie, and a Content-Length over the limit is refused before the body is read. The reserved-effect tests say created where they said minted, and the comments say what the code does rather than what it once did.
The docs format job also runs prettier over the Markdown outside docs/, which rejected both files. This is prettier's own output, with no change to the wording.
Three tests built the same ClientResolveInput and differed only in the module, the payload and the identifier expected. They become one table naming each case: client_fixed with the known word creates the identifier, client_fixed with another word creates nothing, and hmac creates nothing from any payload because a server-side module inherits the no-op resolve_from_client default.
Brings the four commits that fixed the permission branch's CI: the inspector no longer calls a removed method, its lock follows the workspace's edgezero revision, and prettier's formatting of the two permission guide pages, the agent guide and the permission signal README. # Conflicts: # AGENTS.md
What an operator selects to supply a capability is a module, and provider keeps the one meaning it has on main, an auction provider instance. This renames the words this branch adds on top of the permission model, which already says module, and only those words: every provider word was compared with upstream main, and a word that exists there is untouched, as is the icu_provider crate the permissions inspector's lock file names. - ClientFixedProvider is ClientFixedModule, CLIENT_FIXED_PROVIDER_KEY is CLIENT_FIXED_MODULE_KEY, and check_named_provider_configuration is check_named_module_configuration. - The test doubles ResolveHeaderProvider and TestIdProvider are ResolveHeaderModule and TestIdModule, HEADER_PROVIDER_ID is HEADER_MODULE_ID, and the test helpers and test names say module. - The plain word says module wherever it meant an Edge Cookie module, in the resolve endpoint, the cookie and finalize code, the integration registry, the demonstration page script, the cargo feature's description and the API reference.
resolve_conflicts_when_a_different_identity_already_exists built its context by hand with no module, so once the endpoint stopped rebuilding one it answered 204 for no module where the test expects the 409 conflict. The context now carries the module the settings select, as the other resolve tests' contexts do.
Brings main at 7a0ecb4 and the refreshed lower branches into the documentation set. Nine of the eleven conflict blocks differed only by the module rename and are settled as this branch's text with the rename applied. By hand: the evidence module keeps the HostSignals trait where this branch moved it, the setup guide keeps this branch's description of the ec_id value ahead of main's text on grouped mappings, and the Edge Cookie guide keeps the client_fixed paragraph and takes main's completion-marker text for the ec_identity_store paragraph where this branch moved it.
What an operator selects to supply a capability is a module, and provider keeps the one meaning it has on main, an auction provider instance. This renames the words this branch adds on top of the client resolve path, which already says module, and only those words: every provider word was compared with upstream main, and a word that exists there is untouched, as is the icu_provider crate the permissions inspector's lock file names. - The environment overrides the configuration guide and the error reference show read TRUSTED_SERVER__EC__MODULE, TRUSTED_SERVER__DEVICE__MODULE and TRUSTED_SERVER__GEO__MODULE, and the selectors in every example read module. - The configuration, Edge Cookie, setup, error reference and Fastly guides, the example settings, the evidence module's documentation and the no-client-IP test's message say module wherever they meant an Edge Cookie, device or geo module.
Brings the fix that threads the selected module into the resolve conflict test's context.
…seam-impl Brings main at 7a0ecb4 and the refreshed chain below this branch into the seam implementation. The seam made the Edge Cookie, device and geo traits async with a runtime services argument, so the Edge Cookie files the chain refreshed in their synchronous form (the resolve endpoint, the context, the seam trait file, finalization, the edge cookie helpers, pull sync, batch sync and the admin lookup) are taken as the chain has them and put back in the seam's shape, with the seam's own tests that read the config store through the services restored. Main's changes that the seam had moved out of core are carried into the seam's implementations: a Prebid Server stored request is sent only when the slot's intent allows the fallback, an implementation marks an impression it has no demand for as omitted, the driver leaves those impressions out and makes no request when none is left, and the parse state keeps only the slots that were sent. Main's split between the reader's and the request's origin requirement is carried into the seam's publisher, which keys personalization rather than one vendor's tag. The build script keeps both jobs, the migration guard's generated source list and the manifest's default config store id. The names the lower branches gave their identifiers are applied to the seam's lines, so the seam compiles against the refreshed chain. The seam's own names keep their spelling for its rename commit. Main's new tests that name demand sources with hyphens take the snake_case names the seam requires, and those written against main's plan shape are rewritten onto the seam's demand table.
main's rule that partner EIDs are written to the identity graph only with consent for personalised ads is stated in the guides as the storage and personalised-ad permissions, the pair the permission model already uses for sharing.
…d run the Cloudflare auction test with no demand modules The build script's TRUSTED_SERVER__GIT_VERSION reaches test processes and the strict loader refused it as an unknown field. Cloudflare runs one demand source at a time, so the fixture's two demand modules stopped worker startup.
The wasm clock lint reaches core's build script, which runs on the host.
Core's version_header module is removed, with its call from the finalize step of the Axum, Cloudflare and Spin adapters and from the Fastly adapter's health probe and its settings fallback, so no response carries x-ts-version unless a [[serve]] entry names the service_version middleware. That middleware now writes the git version the build compiled in from TRUSTED_SERVER__GIT_VERSION or local git as X-TS-Version, when the build knows one, and the version the adapter reports for the service as X-TS-Service-Version, when it reports one. HEADER_X_TS_FASTLY_VERSION, which nothing wrote, goes with the module. The health tests of the three adapters prove the probe carries no version, the middleware's own tests prove each header is written when its version is known and nothing when neither is, and the publisher tests prove both headers reach a buffered page and neither reaches a page no entry names the middleware for. The configuration guide, the getting started guide, the EdgeZero guide and the changelog say the same.
jwrosewell
marked this pull request as ready for review
October 14, 2026 12:35
This was referenced Oct 14, 2026
Collaborator
|
Moved into this repository as #1356, one branch of the stack, so the review continues there. This pull request is closed in its favor and its comments stay here for the record. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on robots.txt from modules (#1301). The additions that follow the seven pull requests open here are listed in #777.
Compare
split/14-robots-txtwithsplit/15-diagnostic-headersto see only this pull request's change.
What changes
A response no longer carries
X-Served-By,X-CacheorX-Cache-Hits, unless this server answered a request for a path beneath/_ts.X-Served-ByX-CacheX-Cache-HitsNo adapter writes a version on a response. What the build was made from is in the attestation document (#1297), under
build, and the platform's version number of the service underplatform.serviceVersion, which is where a deployment is asked about itself. A deployment that wants the versions on its pages names core'sservice_versionmiddleware in a[[serve]]entry. The middleware writesX-TS-Version, the git version the build compiled in fromTRUSTED_SERVER__GIT_VERSIONor local git, when the build knows one, andX-TS-Service-Version, the version the adapter reports for the service, when it reports one, on every page the entry covers and on nothing else.How it works
/_ts. The publisher fallback removes the three from whatever the origin sends, on every path. The finalize middleware of each of the four adapters then removes them from every response outside the namespace. So a request for an unrouted/_ts/anything, which the fallback sends to the origin, does not get the origin's headers back./_tsand/_ts/permissionsare./_tsxand/_ts-archiveare a publisher's pages, and are stripped like any other.[response_headers]among them. An operator who wantsX-Cacheon every page can still set it there.service_versionis core's own middleware. No section selects it, an entry names it, and it writesX-TS-Version, the git version the build compiled in, andX-TS-Service-Version, the platform's version number of the service, each only when its version is known.Why
A response to a reader carries what the page needs and nothing about the deployment, so that the response stays as clean and free of clutter as it can be. The headers above describe how the site is built, to anyone who asks for any page, without that being a decision anyone made. Three arrive from the publisher's origin or a cache on the way to it, and the version was written by the adapter itself.
A question about a deployment has addresses of its own. The inspection pages (#1294) answer beneath
/_ts, and so does attestation (#1297) unless its endpoint is moved, and what each shows is the publisher's decision. The rule here is the same one. What describes the deployment is answered where a deployment is asked about itself, and is left off everything else. A deployment that has a reason to put the service version on its pages says so in its settings, with the middleware, and the decision is then its own.What breaks
X-CacheorX-Served-Byfrom a page fetched through this server, to see whether the origin's cache answered, no longer sees them. The origin still sends them, and they can be read at the origin.X-TS-Versionfrom a page no longer finds it, whichever adapter served the page and whichever version it carried. It reads the attestation document, wherebuildsays what the build was made from andplatform.serviceVersionis the platform's number, or the deployment namesservice_versionin a[[serve]]entry.How it was verified
CI passed on
a43a05a33on the fork. Not exercised: a response the Fastly entry point finalizes outside the middleware, being the router's own405and the two debug endpoints, is not stripped. It carries nothing from an origin.References #1273.