Skip to content

spec: consent-governance family — opt-in, explained, revocable, self-sovereign - #290

Open
mdheller wants to merge 1 commit into
mainfrom
spec/consent-governance-plane
Open

spec: consent-governance family — opt-in, explained, revocable, self-sovereign#290
mdheller wants to merge 1 commit into
mainfrom
spec/consent-governance-plane

Conversation

@mdheller

@mdheller mdheller commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Two contracts that record how consent gates observation and action, and a gate that refuses to certify a weakened default.

Schema URN prefix Governs
ConsentSurfaceRegistry urn:srcos:consent-registry: What may be observed — telemetry surfaces: category, sensitivity, pii, projectionMode, residency, purpose, consent state
CapabilityConsentPolicy urn:srcos:consent-policy: What may be done on the subject's behalf — camera, microphone, screen capture, computer control, remote compute, file writes, network egress, acting as the user

No capture mechanics

Both are classification-and-consent contracts: no collector, no transport, no payload shape, no invocation path. A surface can be fully registered and permanently unobserved — which is exactly how a sensitive surface is meant to appear here: as a default-deny row that makes the non-collection auditable rather than merely absent.

The inversion is structural, not aspirational. deploymentScope is const: "self-sovereign" and collectorPrincipal must equal subjectPrincipal, so a document describing one party collecting from another is not expressible in this family.

The deliberate asymmetry

Passive telemetry ships standing-persistent; active capabilities ship per-use. That gap is the design. A passive counter you opted into once should not re-interrupt you forever, but a camera that asks once and remembers the answer has stopped being consent. microphone is the single exception (standing-session) — a per-utterance prompt makes continuous listening unusable, so the honest mitigation is a session bound rather than a prompt nobody reads.

Capability riskClass defaultStandard defaultState oneShot
camera sensor-capture per-use disabled
screen_capture sensor-capture per-use disabled
microphone sensor-capture standing-session disabled
control_my_computer device-control per-use disabled
skycomputer remote-compute per-use disabled
file_write data-write per-use disabled
network_egress data-egress per-use disabled
send_on_behalf act-as-user per-use disabled

The matrix is pinned in tools/validate_consent_plane_examples.py, not only in the example. An example is a file anyone can edit; a gate is a thing you have to argue with.

Five invariants — make validate-consent-plane-examples

  • (a) self-sovereigncollectorPrincipal == subjectPrincipal.
  • (b) default-deny — the schema must declare consent.state default denied; a granted surface must carry grantedAt + grantRef and a revoked one revokedAt + grantRef (a state cannot be edited into existence — it must point at something that happened); every per-use capability ships defaultState: disabled.
  • (c) canonical defaults — a canonical capability may not be absent (absence is not a safe default, it is ungoverned); defaultStandard is pinned to the matrix; effectiveMode may diverge only under an attributed userOverride. Applied to granted surfaces too — a denied/revoked surface is off by consequence of consent, not by override, and is exempt.
  • (d) one-shot — a per-use capability may never carry a persistable/standing grant, not even behind userOverride. The subject may always tighten; nothing may loosen. This closes the hole through which one "allow" becomes a permanent permission.
  • (e) explanation — a non-trivial plain sentence on every governed thing. Consent to something unexplained is not consent, so the sentence is a structural precondition of registering the thing at all. Length alone does not satisfy it: placeholder and single-word-repeated filler are rejected.

16 negative controls, all verified biting, built from synthetic in-memory documents that never read or mutate examples/ — the validate_schema_references.py self-exclusion discipline, so the proof cannot pollute the thing being proven. If a control fails to trip, the gate exits non-zero and certifies nothing.

Examples

examples/consent_surface_registry.json grants benign surfaces only (model:tokens_used, policy:gate_verdict — the latter demonstrating an attributed userOverride divergence). device:hardware_id (sensitive) is present purely as a default-deny classification row with OPAQUE_HANDLE_ONLY projection and nothing reading it; app:session_start (personal) is revoked, showing that a withdrawal stays legible rather than decaying back into an indistinguishable "denied".

examples/capability_consent_policy.json ships the full canonical set at defaults, all disabled, with microphone tightened to off under an attributed override — the only direction an override may move.

Verification

Gate Result
make validate-consent-plane-examples ✅ 16/16 negative controls trip; 7/7 checks pass
tools/validate_schema_references.py (referential integrity) ✅ 348 schemas valid, all 138 $refs resolve
scripts/check_duplicate_schema_ids.py ✅ 366 unique $id
ajv compile (CI step 1, both new schemas w/ full -r set)
CI example→schema type mapping ✅ both examples resolve and validate
pytest tools/ -q (tools-tests.yml) ✅ 24 passed
pytest tools/test_reconcile_contracts.py -q ✅ 7 passed
registry/contract-registry.json regenerated ✅ 367 canonical contracts

Wiring

validate-consent-plane-examples added to .PHONY and to the aggregate validate target; schemas/consent-plane.README.md added; CHANGELOG entry under [Unreleased] → Added.

Reviewer notes — two judgment calls

  1. CapabilityConsentPolicy carries id/type/specVersion beyond the fields specified. Required by convention: CI's example validator maps examples/*.jsonschemas/<type>.json by top-level type, so without it the example is silently skipped rather than validated.
  2. Rule (c)'s override clause is applied to surfaces as well as capabilities, with an exemption when consent is not granted. Surfaces carry the identical defaultStandard/effectiveMode/userOverride triple; leaving it unenforced there would be a declared-not-enforced hole of exactly the kind this repo exists to close. The exemption is the honest one: a denied surface is off because consent was refused, not because anyone overrode a default.

Not merging — flagged for review.

…sovereign

Two contracts that record how consent GATES observation and action, and a gate
that refuses to certify a weakened default.

ConsentSurfaceRegistry governs what may be OBSERVED (telemetry surfaces:
category, sensitivity, pii, projectionMode, residency, purpose, consent state).
CapabilityConsentPolicy governs what may be DONE on the subject's behalf
(camera, microphone, screen capture, computer control, remote compute, file
writes, network egress, acting as the user).

Both are classification-and-consent contracts with NO CAPTURE MECHANICS — no
collector, no transport, no payload shape, no invocation path. A surface can be
fully registered and permanently unobserved, which is how a sensitive surface is
meant to appear here: a default-deny row that makes the non-collection
auditable rather than merely absent.

The inversion is structural, not aspirational: deploymentScope is
const "self-sovereign" and collectorPrincipal must equal subjectPrincipal, so a
document describing one party collecting from another is not expressible.

The asymmetry is deliberate — passive telemetry ships standing-persistent,
active capabilities ship per-use (microphone alone at standing-session, since a
per-utterance prompt makes continuous listening unusable and the honest
mitigation is a session bound). The matrix is pinned in the gate, not only in
the example: an example is a file anyone can edit, a gate is a thing you have to
argue with.

validate-consent-plane-examples enforces five invariants:
  (a) self-sovereign  — collectorPrincipal == subjectPrincipal
  (b) default-deny    — the schema must DECLARE consent.state default 'denied';
                        a granted/revoked surface must carry the evidence of
                        that transition, so a state cannot be edited into
                        existence; every per-use capability ships disabled
  (c) canonical       — a canonical capability may not be ABSENT (absence is not
                        a safe default, it is ungoverned); defaultStandard is
                        pinned; effectiveMode may diverge only under an
                        attributed userOverride (granted surfaces too; a
                        denied/revoked surface is off by consequence of consent)
  (d) one-shot        — a per-use capability may never carry a persistable or
                        standing grant, not even behind userOverride. The
                        subject may tighten; nothing may loosen. This closes the
                        hole through which one "allow" becomes a permanent
                        permission
  (e) explanation     — a non-trivial plain sentence on every governed thing;
                        consent to something unexplained is not consent

16 negative controls, all verified biting, built from synthetic in-memory
documents that never read or mutate examples/ — the validate_schema_references
self-exclusion discipline, so the proof cannot pollute the thing being proven.
If a control fails to trip, the gate exits non-zero and certifies nothing.

Example set grants BENIGN surfaces only (model:tokens_used, policy:gate_verdict);
sensitive and personal surfaces appear as off, classification-only rows.

Registry regenerated (367 canonical contracts). Referential-integrity gate,
duplicate-$id guardrail, ajv compile, CI example type-mapping, and the
reconciliation suites all pass.

See schemas/consent-plane.README.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant