Privacy Promises
<!-- Privacy Promises — architecture + workflow A single source of truth for user-facing privacy claims, each linked to the code that enforces it and the tes...
Privacy Promises
Summary
- Privacy promises are declared once in
shared/docs/privacy_promises.yml. - Backend and frontend tests must contain matching promise markers before a promise can stay active.
- The public privacy policy renders technical privacy measures from the same registry, reducing drift between product claims, code, and tests.
Every user-facing privacy claim OpenMates makes is declared in one registry file, linked to the code that enforces it, and verified by tests. If any of the three pieces drifts — the claim, the code, or the test — the meta-test fails and a Claude Code hook warns the next editor. The public privacy policy’s “Technical Privacy Measures” section is generated from the same registry, so we can’t publish a promise without wiring its enforcement.
Why this exists
Historically, privacy claims lived in three disconnected places: the public policy (shared/docs/privacy_policy.yml), ad-hoc architecture docs (docs/architecture/privacy/*.md), and tests scattered across the backend and frontend. A change to encryption code or a removed test could silently invalidate a public-facing promise — with no automated signal. The Privacy Promises system closes that loop.
The registry — shared/docs/privacy_promises.yml
A single YAML file with one entry per promise. Each entry declares:
| Field | Purpose |
|---|---|
id |
Stable kebab-case identifier used in test markers and hook output |
category |
encryption / pii / logging / deletion / auth / payment / tracking / transparency |
severity |
critical / high / medium |
i18n_key |
Canonical translation key (legal.privacy.promises.<id>.{heading,description}) |
verification |
test (needs linked tests) or documentation (contractual/structural claims) |
gdpr_articles |
List of GDPR articles the promise supports (e.g. ["Art. 32"]) |
enforcement[] |
List of {file, what} pairs — the code that enforces this promise |
tests[] |
List of {kind, path, marker, assertion} — the tests that verify it |
architecture_doc |
Link to the deeper architecture doc (when one exists) |
surfaced_in_policy |
If true, auto-renders into the public privacy policy |
Shape enforcement lives in shared/docs/privacy_promises.schema.json.
Terminology rules (meta-test enforced)
client-side encryptionis our accurate baseline claim.zero-knowledge encryptionMAY be used only when the checklist incore/encryption-architecture.mdis satisfied — and the meta-test asserts the linked architecture doc documents that checklist.end-to-end encryption/E2EE/E2E encryptionis forbidden in any surfaced heading, description, or linked architecture doc. Disclaimers like “this is not end-to-end encryption” are allowed; affirmative claims are not.
The promises (current set)
Fourteen promises ship today — twelve verified by runtime tests, two verified by documentation (contractual / structural). The authoritative list is the registry; this table is a summary.
| # | ID | Category | Verification |
|---|---|---|---|
| 1 | client-side-chat-encryption |
encryption | test |
| 2 | email-encryption-at-rest |
encryption | test |
| 3 | no-third-party-tracking |
tracking | test |
| 4 | pii-placeholder-substitution |
pii | test |
| 5 | telemetry-privacy-filter |
logging | test |
| 6 | cryptographic-erasure |
deletion | test |
| 7 | argon2-password-hashing |
auth | test |
| 8 | payment-data-minimization |
payment | test |
| 9 | logging-redaction |
logging | test |
| 10 | prompt-injection-defense |
pii | test |
| 11 | no-external-resources |
tracking | test |
| 12 | cli-no-credential-prompts |
auth | test |
| 13 | no-training-on-user-data |
transparency | documentation |
| 14 | open-source-transparency |
transparency | documentation |
Verification layers
1. pytest meta-test — backend/tests/test_privacy_promises.py
Thirteen checks validate the registry’s integrity on every CI run:
- YAML parses + matches the JSON Schema
- Promise IDs are unique
- Every
enforcement[].fileexists on disk - Every linked test file exists on disk
- Every
verification: documentationpromise has validdocs[] - Every linked test file contains a matching
@privacy-promise: <id>marker - Every
i18n_keyexists infrontend/packages/ui/src/i18n/sources/legal/privacy.yml - No orphan markers — every
@privacy-promise:found in the repo maps to a registry entry - Forbidden-terminology guard — no affirmative
end-to-end encryption/E2EEin registry strings or linked architecture docs (explicit disclaimers pass) - Zero-knowledge gate — if any promise claims
zero-knowledge, the linked architecture doc must document the checklist - Live tracking audit — scans every frontend
package.jsonfor forbidden analytics SDKs (gtag, segment, mixpanel, posthog, plausible, amplitude, hotjar, fullstory, heapanalytics). Fragment-encoded to avoid tripping theanalytics-sdk-forbiddenhook on itself. - Live logging-redaction check — instantiates
SensitiveDataFilterand asserts an email, bearer token, and password are redacted from a log record - Cryptographic-erasure phase order — static source check that
user_cache_tasks.pydestroys encryption keys before deleting user content
Runs on host (python3 -m pytest backend/tests/test_privacy_promises.py) and in CI. Skips gracefully in the api docker container because docs/ and parts of frontend/ aren’t mounted there — the skip message points developers to the host / CI path.
2. vitest companion — frontend/packages/ui/src/legal/__tests__/privacyPromises.test.ts
Three checks on the frontend side:
- The generated registry module is populated and all IDs use the canonical
legal.privacy.promises.prefix buildPrivacyPolicyContentemits a level-3 heading + description for everysurfaced_in_policy: truepromise- The legacy
legal.privacy.protection.*.descriptionkeys are no longer referenced (prevents regression to the old hard-coded list)
3. Claude Code hook — .claude/hooks/privacy-promise-guard.sh
A PreToolUse(Edit|Write) hook. When an agent edits a file listed in any promise’s enforcement[], the hook emits the affected promise IDs, their headings, and the linked tests. If a linked test file has been removed, the hook escalates with 🚨 LINKED TEST REMOVED. Never blocks (warn-only, exit 0). Registered per-developer in .claude/settings.local.json.
4. Per-test marker — @privacy-promise: <id>
A one-line comment in each linked test. The meta-test scans all .py/.ts/.tsx/.js/.svelte files under backend/ and frontend/ for these markers. Two invariants:
- Every marker maps to a registry ID (no orphans)
- Every listed test in the registry contains the expected marker (language-agnostic regex, same syntax in Python
#and JS/TS//comments)
5. Legal-compliance auditor cross-check
The twice-weekly legal-compliance-auditor agent (.claude/agents/legal-compliance-auditor.md) runs Step 8 — Privacy Promises cross-check — on every full and delta scan. On the Thursday delta scan, if an enforcement file was modified in the commit window but its linked tests were NOT re-run (per test-results/last-run.json), the auditor emits a high-severity code-fix finding titled “Privacy-promise enforcement file changed without test rerun” referencing the affected promise IDs.
Public privacy policy integration
The “Technical Privacy Measures” section of the public privacy policy is derived from the registry, not hand-written.
shared/docs/privacy_promises.yml
│
▼
frontend/packages/ui/scripts/generate-privacy-promises.js (build step)
│
▼
frontend/packages/ui/src/legal/privacyPromises.generated.ts (typed module)
│
▼
buildLegalContent.ts iterates SURFACED_PRIVACY_PROMISES
│
▼
Public privacy policy renders heading + description per promise
The generator runs as part of npm run prepare / prebuild / build in frontend/packages/ui/package.json. The emitted TS module is committed so the browser never parses YAML at runtime.
i18n keys for each promise live under legal.privacy.promises.<id>.heading and .description in frontend/packages/ui/src/i18n/sources/legal/privacy.yml. Editing the registry bumps lastUpdated in privacy-policy.ts per GDPR Art. 13 transparency (enforced by the existing legal-text-lastupdated-bump hook).
Adding a new promise
- Append an entry to
shared/docs/privacy_promises.ymlfollowing the schema. Pick a kebab-caseidand matching snake_casei18n_keysuffix. - Add
legal.privacy.promises.<id>.headingand.descriptionentries (English at minimum) tofrontend/packages/ui/src/i18n/sources/legal/privacy.yml. The auto-rebuild-translations hook fills in other locales. - Add a
@privacy-promise: <id>comment to at least one existing or new test file, and reference that file undertests[]in the registry. - For a
verification: documentationpromise, reference real files underdocs[]and leavetests: []. - Run
python3 -m pytest backend/tests/test_privacy_promises.py— it will fail informatively until steps 1–3 are consistent. - Run
npm run generate-privacy-promisesinfrontend/packages/ui— that emits an updatedprivacyPromises.generated.ts. - Commit all three files in the same change (
sessions.py deployis the normal path).
Removing / deprecating a promise
Remove the entry from the registry and the linked i18n keys in the same commit. The meta-test will catch any orphan markers left behind. If the promise had surfaced_in_policy: true, bump lastUpdated in privacy-policy.ts so the public policy re-renders.
Downgrading a claim
If a promise previously claimed zero-knowledge but the enforcement code no longer satisfies the checklist in core/encryption-architecture.md, the meta-test’s Zero-Knowledge Gate will fail. Resolve by either:
- Strengthening the enforcement code until the checklist is satisfied again, or
- Downgrading the user-facing claim to
client-side encryptionin the i18n description and removing the termzero-knowledgefrom the registry entry.
Never downgrade silently — every change to a public promise belongs in the commit history.
File map
Registry + schema
shared/docs/privacy_promises.yml— source of truthshared/docs/privacy_promises.schema.json— JSON Schema
Verification
backend/tests/test_privacy_promises.py— pytest meta-test (13 checks)frontend/packages/ui/src/legal/__tests__/privacyPromises.test.ts— vitest companion
Guards
.claude/hooks/privacy-promise-guard.sh— PreToolUse warn hook (generic, fires whenever an enforcement file is touched).claude/hooks/cli-credential-prompt-guard.sh— blocks password / email / 2FA prompts in CLI source (pair-auth PIN excepted).claude/hooks/external-resources-guard.sh— reminds whenever a non-allowlisted https URL is loaded by the web app.claude/agents/legal-compliance-auditor.md— twice-weekly cross-check
Policy integration
frontend/packages/ui/scripts/generate-privacy-promises.js— YAML → TS build stepfrontend/packages/ui/src/legal/privacyPromises.generated.ts— generated modulefrontend/packages/ui/src/legal/buildLegalContent.ts— iteratesSURFACED_PRIVACY_PROMISESfrontend/packages/ui/src/i18n/sources/legal/privacy.yml—legal.privacy.promises.*subtreefrontend/packages/ui/src/legal/documents/privacy-policy.ts—lastUpdatedfield