Repository navigation
Add the permission model with the Privacy Taxonomy vocabulary - #1045
Open
jwrosewell wants to merge 160 commits into
Open
jwrosewell wants to merge 160 commits into
jwrosewell wants to merge 160 commits into
Conversation
This was referenced Aug 19, 2026
jwrosewell
force-pushed
the
split/3-permissions
branch
2 times, most recently
from
August 20, 2026 02:22
6d20255 to
760b921
Compare
15 of 17 tasks
jwrosewell
force-pushed
the
split/3-permissions
branch
from
August 25, 2026 10:51
760b921 to
b3a0eae
Compare
This was referenced Aug 25, 2026
jwrosewell
force-pushed
the
split/3-permissions
branch
from
August 25, 2026 13:37
b3a0eae to
5a707bf
Compare
4 tasks
jwrosewell
force-pushed
the
split/3-permissions
branch
6 times, most recently
from
August 31, 2026 12:50
4d591e9 to
35f6ef2
Compare
jwrosewell
added a commit
to jwrosewell/trusted-server
that referenced
this pull request
Aug 31, 2026
The review of IABTechLab#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 IABTechLab#1043 to IABTechLab#1047 each carried the design document for their own step, and IABTechLab#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 IABTechLab#1043) - provider-code-registry.md (from IABTechLab#1043) - 2026-07-30-permission-model-design.md (from IABTechLab#1045) - 2026-07-30-client-cycle-ec-resolve-design.md (from IABTechLab#1046, later revised by IABTechLab#1047) - 2026-07-30-integration-response-header-hook-design.md (from IABTechLab#1047) - 2026-07-30-provider-migration-rollout-design.md (from IABTechLab#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 IABTechLab#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 IABTechLab#1047, and a revision-record row recording the move.
Collaborator
jwrosewell
force-pushed
the
split/3-permissions
branch
from
September 1, 2026 15:34
35f6ef2 to
1c9bb91
Compare
…ider
First of five PRs decomposing the provider and permission epic. The
EdgeCookieProvider trait routes Edge Cookie minting, cookie read-back,
and KV keying through the selected provider, so a vendor identifier
round-trips verbatim instead of being dropped by the built-in shape
check.
- [ec] provider selector with per-provider [ec.providers.<key>] blocks.
The deprecated [ec] passphrase form still starts for one release
cycle: it maps to provider = "hmac" with a deprecation warning, and a
configuration carrying both forms is rejected. provider = "none"
spells explicit statelessness. A configured block that is not the
selected provider is rejected at startup, as is a block with no
selector.
- Global identifier bounds enforced by core at mint, read-back, and
cookie write: the cookie-safe alphabet [A-Za-z0-9._~-] and a 256-byte
cap. An identifier outside the bounds is rejected loudly, never
rewritten, so the cookie value and the identity-graph key can never
silently diverge.
- The identity graph is keyed by the provider's canonical form of the
identifier (normalize_id_for_kv), so equivalent representations of
one identity share one row.
- Request evidence abstraction (crate::evidence) giving providers read
access to the client IP, headers (including cookies), URL path, and
query parameters.
- Adapter injection seam: RuntimeServices carries an optional vendor
provider, so a vendor provider lives in its own crate and core never
names it. A selected provider the adapter does not inject fails the
request loudly rather than silently running stateless.
- Provider generate failures log at error level with the request
proceeding stateless.
Edge Cookie creation and use stay gated by the existing consent context
exactly as on main, including with no provider selected; the permission
model replaces that input in the third PR of this series.
Config migration: move [ec] passphrase to [ec.providers.hmac] and set
[ec] provider = "hmac". The old form keeps working for one release with
a warning. Passphrases shorter than 32 characters are now rejected at
startup; previously they were accepted.
The design spec for this slice and the next lives at
docs/superpowers/specs/2026-07-30-pluggable-providers-design.md, the
2026-07-31 draft revised to match the implementation with a
revision-record table of every divergence.
Every provider carries a mandatory registered four-character code
(provider-code-registry.md): core mints {code}~value, checks the code
at read-back, and keys the identity graph with it, so identifiers from
different providers can never collide and a switch of provider cannot
silently adopt another provider's identities. The built-in hmac
provider mints hmac~<hash>.<suffix> and dual-reads its pre-envelope
bare form for one release cycle.
Since the provider-code envelope, the mint path issues identifiers as
hmac~{64hex}.{6alnum}, and that is the value identify hands to partners.
Pull sync, batch sync and the admin lookup still validated the bare
shape through is_valid_ec_id, so pull sync skipped every freshly minted
identifier, batch sync answered invalid_ec_id for the value partners were
given, and the admin lookup answered 400. CI stayed green because the
lifecycle scenario seeds a bare cookie.
is_valid_ec_id now accepts the hmac envelope as well as the legacy bare
form and rejects any other provider's code, and normalize_ec_id_for_kv
keeps the envelope so the key matches the one written at mint. Tests
cover the validator, the normalizer and each of the three call sites
with a coded identifier.
CodeQL's cleartext-logging query treats a call whose name contains "passphrase" as a sensitive source, and because the method mutates the Settings it belongs to, every later log line that prints anything from Settings (store names, timeouts, header names) is reported as writing a secret to a log. The passphrase itself is a Redacted<String> and none of the flagged lines prints it. The method now describes what it does, migrate_legacy_ec_layout, and its behavior is unchanged.
A reviewer raised a P1 against the pluggable Edge Cookie provider work: three of the four adapters broke the provider contract that an unavailable required service or an uninjected provider stops the request. The Axum, Cloudflare and Spin adapters each read the Edge Cookie context with `EcContext::read_from_request_with_geo(...).unwrap_or_else(...)`, logged a warning and continued with `EcContext::default()`. A deployment whose selected provider could not be built therefore came up and served every request with no identity, silently. The Fastly adapter already kept the report and answered with an error response. `build_ec_context` on the three adapters now returns `Result<EcContext, Report<TrustedServerError>>` and every call site propagates it to that adapter's own `http_error`, the same helper Fastly uses, so all four answer with the same status and shape. The design this implements has the composition root check a selected provider's needs once at startup rather than per request, so `ensure_provider_available` was added to `ec/provider.rs` and is called from `build_state_with_settings` on all four adapters (Fastly included, so the rule is uniform). Building a provider reads no request data, so a selection an adapter can never supply now fails when application state is built, and the three adapters answer every route from their existing `startup_error_router` instead of coming up. Statelessness, meaning no `[ec] provider` selector or the explicit `"none"`, still passes and still serves. The widening question was checked rather than assumed. `read_from_request_with_geo` can only fail from two places: the provider build, and a `Cookie` header that is not valid UTF-8. A malformed cookie value is dropped with a warning by `request_ec_id_if_allowed`, consent parsing returns a value rather than a `Result`, and the geo lookup is already swallowed by the adapter before the call, so no ordinary parse problem reaches the error path and none is turned into a failed request. Tests: each of the three adapters gains a route test proving an uninjected provider fails at startup, and an in-crate test proving `build_ec_context` returns the error rather than a default context. Core gains a test that the startup check rejects an uninjected provider and still allows statelessness both ways. Addresses: Christian Pavilonis review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:317 (P1)
`Settings::finalize_deserialized` runs derive validation before `Ec::migrate_legacy_ec_layout`, and the deprecated `[ec] passphrase` field carries no `#[validate]` attribute of its own, so the advertised 32-byte minimum was only enforced on the new `[ec.providers.hmac]` location. A configuration still on the old form could start with `passphrase = "short"`, or with an empty value, and mint identifiers from keying material the new location rejects. The migration now calls `Ec::validate_passphrase` on the value it is about to move, before it logs the deprecation warning and writes the `[ec.providers.hmac]` block, and reports a configuration error naming the minimum length and the new location. Tests: `a_legacy_passphrase_is_held_to_the_passphrase_rules` drives `Settings::from_toml` with the `[ec]` section rewritten to the deprecated form and proves a short value and an empty value are both rejected, and that a passphrase of adequate length still migrates to `provider = "hmac"` with the passphrase in the hmac block. Removing the new check makes that test fail, so it tests the fix rather than the surrounding code. Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/settings.rs:658 (wrench)
The provider spec (section 6) says `deny_unknown_fields` is set on both built-in provider config structs, but `HmacProviderConfig` carried no such attribute, so `[ec.providers.hmac] typo_key = "x"` was accepted silently. An operator who mistypes a key gets a deployment that starts and quietly uses the default for the setting they meant to change. `HmacProviderConfig` now sets `#[serde(deny_unknown_fields)]`, matching `Ec` itself and the rest of the settings tree. The struct is a plain field of `EcProviders` rather than a flattened one, so the attribute does not collide with the `#[serde(flatten)]` vendor map alongside it. Tests: `an_unknown_key_in_the_hmac_provider_block_is_rejected` adds an unknown key to the block in the crate test configuration and proves `Settings::from_toml` fails and names the key. Removing the attribute makes that test fail. Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/settings.rs:726 (wrench)
`build_provider`'s `"hmac"` arm mapped over `ec.providers.hmac`, so a deployment that selected `provider = "hmac"` with no `[ec.providers.hmac]` block got `Ok(None)` and ran stateless under a selector that says it has an identity provider. Every other unbuildable selection in the same match already errors. The arm now returns `TrustedServerError::EdgeCookie` naming the missing block, which the startup check `ensure_provider_available` turns into a failed application state on every adapter. `Ec::validate_provider_selection` rejects that pair before settings reach the composition root, so nothing routes through the new arm today. It is the drift guard for the case where the two checks stop agreeing, which is exactly the shape of the defect being fixed, so it is worth keeping rather than leaving the silent branch in place. Tests: `selecting_hmac_without_its_block_fails_loudly` builds the `Ec` programmatically, bypassing settings validation to reach the seam, and proves the error names the missing block. The doc comment's `# Errors` section is corrected in the same commit, since it still claimed no built-in construction can fail. Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:304 (refactor)
The error raised when a provider mints an identifier outside the identifier bounds was written across two source lines without the trailing backslash that joins them, so the 22 spaces of source indentation became part of the literal and the logged message read "...bytes, or outside the cookie-safe alphabet". The continuation is restored, so the message reads as one sentence. The whole of ec/mod.rs was scanned for the same fault, matching every string literal and stripping real continuations before looking for runs of more than one space or a newline inside a literal. This message was the only one. Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/mod.rs:444 (nitpick)
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.
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.
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.
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 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.
A module's name is the path of its crate below `crates/`, with `.` between the parts, taken from `CARGO_MANIFEST_DIR` when the crate is built, so the name cannot drift from the folder and no crate carries a hand-written id. `module_name!()` gives a crate its own name, `module_name::resolve` finds the name an operator wrote among the modules offered, as written or with the section's type folder in front, and `module_name::short_form` gives the name back without that folder. Core's own modules keep bare names. The Edge Cookie side is the first to use it. The type folder of every Edge Cookie crate is `edgecookie`, so `[ec] module` may name an injected module in full or with that folder left off, and an `implementation` line may do the same. The rule for a name written in `[ec]`, its blocks and their `implementation` lines is the module name rule, parts joined by `.`, each of lower case letters, digits, `_` or `-`, in place of snake_case.
Brings the naming module, so a module is named by its crate folder, and the Edge Cookie side resolving a written name within its type folder. The host-signal module's key and the type folder sit side by side.
Brings the naming module from the Edge Cookie seam, so a module is named by its crate folder.
Each of the five modules takes its name from its crate folder through `module_name!()`, so `crates/permission-signal/gpp` is `gpp` and `crates/permission-signal/us-privacy` is `us-privacy`, where the two had called themselves `gpp_sale_opt_out` and `us_privacy` by hand. The page is told a signal's source by the short name, without the type folder. The section is `[permission-signal]`, named exactly as its folder, and its key is `modules`, a list, because signals compose where `[ec]`, `[geo]` and `[device]` each name one module. A name in the list may be written in full, as `permission-signal.gpp`, or with the type folder left off, and the refusal of a name this build does not offer lists the short names. A configuration still carrying `[permission_signal]`, a `module` key or the retired `sources` key is refused with directions to the new spelling.
This was referenced Oct 7, 2026
Comments on the adapters and two test notes in core described what the code did before it changed. Each now says what the code does and why. - The resolved Edge Cookie module held on the Cloudflare, Fastly and Spin application state is resolved when the state is built, because resolving reads no request data. - The three tests that a selected module the adapter cannot build fails the request say what a default context would do. - The Spin state builder reads its settings from the config store, and its test says why the failure has to be the absence of a config store. - The test that keeps the internal header list in step with the Edge Cookie response headers, and the test for an unknown key in a module block, say what they guard. No code changes.
- The Cloudflare region test says why the region header has to reach the privacy outcome. - The Fastly geo module's re-export and the geo selector's default are described as they are. - The integration fixture's note on the geo selector no longer promises a setting that a later change does not add. No code changes.
- The integration fixture carried two notes on its geo selector that contradicted each other. The one that stays says that Viceroy maps the loopback client to US/CA, so the platform geo module resolves a real place. - That fixture, the Viceroy template, the template cache harness and the permission model guide no longer mention a default country setting, which no released version had. - Five code comments say what the code does without saying what it did before. No code changes.
Core builds the identity-graph key from the module's code and whatever `normalize_id_for_kv` returns, so the method decides what two visits must have in common to be treated as the same visitor. Its documentation described normalization only and advised a module with a case-sensitive identifier to return the value unchanged. That is right for a stable identifier and wrong for one that carries a signature, a nonce or a timestamp, because each reissue then gets a row of its own and the identity does not survive it. The trait documentation now says that the returned value is the identity two visits must share, and that a module whose identifier has a part reissued each time must return the stable part. It also says that core asks `accepts_id` about the returned string as well as about the identifier as issued, so a module must accept its own canonical form, or no row is read or written for it. `two_issues_of_one_identity_share_one_identity_graph_key` drives a fixture module whose identifier is a stable part and a part that changes on each issue through `AcceptedModules::canonical_kv_key`. Two identifiers that differ only in the reissued part reach one key, and a different stable part reaches another. The test fails for a module that returns the value unchanged. No behavior changes.
…ords `KvNetwork` said a low cluster count "indicates an individual or household". A count of connections is a fact about a network, and reading a low one as an individual describes a device record as a record about a person. It now says a small network, such as a home connection. `KvEntry::ids` now says what it holds, which is each partner's identifier for the same browser on the same device, never an identifier for a person. Comments only, with no change in behavior.
This branch has not been deployed
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 device and location module selection (#1044), and depends on its
[geo] moduleselector, whose default this pull request changes to nogeolocation. Through #1044 this pull request also depends on the Edge Cookie
module seam (#1043) for the
EdgeCookieModuletrait, which it gates onpermissions. The stack has six pull requests
(#1043, #1044, #1045, #1046, #1047, #1094), each targeting
main, with this onethird, and the first five decompose #838 as requested in the #986 review.
Compare
split/2-device-geowithsplit/3-permissionsto see only this pull request's change.
The design specs for the series are carried by the spec pull request (#1084).
The spec for this pull request is
2026-07-30-permission-model-design.md,revised to match this implementation, with revision records listing every
divergence and why, and section 13 covering the permission signal modules.
Reader documentation lands with this pull request at
docs/guide/permission-model.mdand
docs/guide/permission-signals.md.What this pull request does
Permissions become the primitive that gates identity features, and consent is
one of several ways a permission is established, alongside a country baseline,
an opt-out signal and configuration.
permissions.rsresolves a per-request permission state, being the country and region
baseline from
permissions.yamlamended by the signals the request carries.permissions.yamlis compiled into the build, and the repository sample isconfig/permissions/sample.yaml.Permission names follow the IAB Privacy Taxonomy Data Uses.
How the signal modules are selected
Signals are read by permission signal modules, which are crates outside core
under
crates/permission-signal/,being Global Privacy Control, the GPP sale opt-out, the US Privacy string, TCF
v2 and the Model Terms for Marketing, each implementing the
PermissionSignalModuletrait that core defines inpermission_signal/mod.rs.An adapter selects them once at startup from
[permission-signal] modules,refusing an unknown or repeated name there rather than on the first request,
and carries them on the request services. An unknown name is reported with the
names the build does have, so a typo is answered with the list to pick from.
Precedence is the configured order. For each permission, core asks the
modules in the order
[permission-signal] moduleslists them. Each answersgrant, revoke or neutral, a neutral answer leaves the prior value standing, and
the last module with an opinion decides. Leaving the list out runs every
module the adapter links, in the adapter's order, and an empty list runs
none, which leaves every permission at its country and region baseline. Every
adapter offers the five in the same default order, being
gpc,gpp,us-privacy,tcfandmtm. Global Privacy Control, abrowser setting with no interface of its own, is asked first, and the schemes
that carry a choice someone made through an interface are asked after it, which
means an answer given at a prompt amends the header the visitor arrived with. A
deployment that wants an opt-out to stand lists it after
tcf.What the state carries
The page receives the state as
window.tsjs.permissions, with four lists:set, the permissions that are set;awaiting, the permissions whosebaseline requires a signal and for which no configured module has answered,
narrowed to what some configured module could still grant, so a page can hold
what depends on them rather than read "nobody has answered" as "refused";
signals, one entry for each signal a module read and found valid, as it wasreceived, with the module and the scheme, so page code and bid requests rely
on exactly those signals and no other; and
tdls, the terms documents thedata is available under. An empty state renders every list empty, and each
is an answer rather than a missing value.
The rest of the model
expired record means for the permissions is the decision of the module for
that scheme, taken in its own answer. The 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. After assembly the consent context keeps only
the raw strings the valid signals name, so a corrupt record never reaches a
bid request.
gpcoff the list stops that module, but the consent pipelinestill turns the same header into a US Privacy opt-out for a US privacy state
when the consent setting
gpc_implies_optoutis on, which it is by default,and the
us-privacymodule then acts on that record. A deployment thatwants the header to have no effect turns that setting off as well.
crates/trusted-server-adapter-axum/tests/permission_signals.rsassemble the real modules and cover the default order against each opt-out,
the reversed order, a module left off the list, one opt-out removed while
the others stand, and the Model Terms words.
(
crates/permission-signal/tcf/src/mapping.rs),in code with tests, because which purpose grants which Data Use is that
scheme's own meaning rather than a deployment's policy.
permissions.yamlkeeps what a deployment decides, being which opt-out sources revoke which
Data Uses and whether a TCF record answers at all. The purposes are the IAB
TCF Europe purposes and what they grant are IAB Tech Lab Privacy Taxonomy
Data Uses, so the mapping is checked against the industry's own documents
rather than against us. Two purposes have no Data Use yet, so the crate
carries the proposed
necessary.operations.storagekey for purpose 1 and theTCF identifier
select-basic-contentfor purpose 11 until the taxonomy addsthem.
tdls, the terms documents the request'sdata is available under, declared by the modules in the order they are
asked with duplicates removed. A permission says what may be done with the
data and not on what basis, and a recipient offered data needs both, because
it has to decide whether the terms are ones it accepts and whether it may
pass the data on. Each entry addresses a published document that must never
be edited once published, which is why a version belongs in its address, and
the name matches the
tdlmember theData Labels work
puts on an OpenRTB node. Of the five schemes only the Model Terms for
Marketing declares terms, the versioned Model Terms document, whenever an
answer is present.
standard,personalizedandnon-marketingfrom__mtm_pref. Both marketing wordsgrant storage, contextual advertising, measurement, content performance,
market research, product improvement and basic content selection, and only
personalizedgrants the four Data Uses that depend on browsing history orinteractions, which
standardrefuses because the visitor chose againstthem.
non-marketingchanges nothing, so the country and region rulesdecide. No word withdraws. The five schemes are a starting set rather than
the list, and
docs/guide/permission-signals.mdsays how a scheme is added as a crate with no change to core.
through
withdraws, core scopes the answer to the storage baseline, and ofthe five only TCF answers it. Only a TCF record refusing storage in a
jurisdiction whose baseline did not grant storage therefore expires the
cookie, the pull sync marker and writes the identity-graph tombstone.
Opt-outs suppress use, with Edge Cookie headers stripped and nothing sent
beyond the edge, but never destroy an issued identifier, so lifting the
opt-out restores the identity. This differs from
main, wherehas_explicit_ec_withdrawalinconsent/mod.rsalso withdraws in a US privacy state for a Global Privacy Control signal, a
GPP sale opt-out or a US Privacy sale opt-out. Under this pull request those
opt-outs suppress use and keep the cookie, and the withdrawal tests
mainwrote around a California Global Privacy Control opt-out state the
withdrawal on the permission state.
user.id, the identify responseand partner pull sync, requires both storage and personalized-ad selection
(
StoreOnDeviceandSelectPersonalisedAdsinec_sharing_allowed), thesame pair that gates bidstream EIDs, so a storage-only grant keeps
first-party use while withholding partner sharing.
module declares
required_permissions()and core runs it only when everyone is set.
resolves are declared together on the top node of the
rulestree inpermissions.yaml, so there is always a defined baseline and one file statesthe policy for both. A failed geo lookup is distinct from an unmatched
country. It resolves at the requires-signal floor instead of the declared
default and is logged at error level, and the one-time warning for a lookup
that resolves no location says that the declared jurisdiction applies.
Geolocation is off by default, and a deployment that runs an Edge Cookie
module with no location module must set
[geo] assume_single_jurisdiction = true, acknowledging that every requestis treated as the declared jurisdiction.
permissions.yamlrules use an explicit per-permission acquisition map(
granted,requires_signal,denied). Parsing rejects an unknown group,Data Use, acquisition or revoke keyword, a top node missing
grouporjurisdiction, and two place codes that differ only by case, each case inone table of refusals. An EU-27 plus EEA coverage test locks the
gdpr-eumapping.
Changed from an earlier description
An earlier version of this text described precedence as a rule fixed in code,
where an opt-out always beat a consenting TCF record. Precedence is the
configured order described above. Under the default order, a visitor who sends
Sec-GPC: 1and then consents through a consent management platform has theData Uses that the TCF record consents to set, which is the opposite of the old
outcome, and a deployment gets the old outcome back by listing the opt-out
after
tcf. An earlier version also said an unreadable consent record revokedeverything ahead of the modules. It no longer does, for the reason given
above.
How it was verified
The head this pull request shows now is
0afd715c5, which merges #1044 at47551a622and, through it,mainat 182fdf4 into613cd1831. The merge brings thets dev lint domainsandts dev install-hookscommands (#733) and a design document (#930), and changes none of this pull request's own code. No dependency of core or of an adapter changed version, and the one line of core the merge touches gains a comment. On0afd715c5these ran locally on Windows:cargo fmt --all --check, a locked dependency check (cargo tree --locked) and the core suite natively (3,114 tests).613cd1831adds tofd51fe830one commit of comment and guide wording and three merges of #1044, which bring
comment wording, the identity graph's comments, one doc comment and one
test. On
613cd1831these ranlocally on Windows:
cargo fmt --all --check, Clippy with warnings denied onall four adapters and the five permission signal crates, the core suite
natively (3,114 tests), the five permission signal crates' own tests
(55 tests), and the docs lint, Prettier and VitePress build. On
fd51fe830the wider set ran locally, being the core suite under Viceroy (3,350 across
the crate's test binaries), the Axum suite with the five permission signal
crates (123 tests), the Cloudflare (56) and Spin (90) adapter suites and the
cross-adapter parity suite (17), and CI runs all of them on this head. The
CLI tests, the browser integration tests and the Fastly Edge Cookie
lifecycle test run only in CI.
CI on
0afd715c5: every check passes, being Run Tests, Run Format, Integration Tests, Permissions Inspector and CodeQL Advanced, with the Fastly Edge Cookie lifecycle test in the integration run.Framing
Privacy is a spectrum and technology is neutral. This model encodes no
jurisdiction's law. The deployer brings the policy in
permissions.yamlandconfiguration, decides their own baselines, and the code makes those decisions
inspectable and enforced. Trust comes from that flexibility being respected,
not from constraint.
References #777 and #779. Decomposes #838. Spec baseline from #986.