Companion App — Phase 6: Derived-Artifact Storage & Provenance Enforcement Gate
Status: ✅ IMPLEMENTATION LANDED 2026-06-06 — Design gate ratified by owner 2026-06-06;
implementation shipped on feat/companion-app in the same commit as this doc update. All D6.1–D6.7
decisions are realized. The 7-tier test suite (155 tests, 0 failures across unit/integration/e2e/
stress/data-integrity/performance/security) passes. The full repo suite is clean (3266/3267 pass;
1 pre-existing skip unrelated to Phase 6). See §10 for test obligations honored.
New modules shipped:
lib/companion-provenance-validator.mjs— D6.2 provenance schema + validatorlib/companion-tier-resolver.mjs— D6.1 per-tier storage routinglib/companion-client-encryptor.mjs— D6.4 ClientEncryptor hook (fail-closed default)lib/companion-artifact-writer.mjs— D6.6 single DerivedArtifactWriter (authority group)
Migrated (D6.6.2 — direct write paths deleted):
mcp/tools/index-enrich.mjs— enrichIndexedNotes routes through writerlib/memory-consolidate.mjs— runDiscoverPass routes through writer
Remaining hard prerequisites (unchanged, NOT discharged here): ZK key hierarchy + two-tier vault
registry (privacy_max fails closed); tenancy identity (cross-partition writes blocked). Phase 7
(packaging/signing) remains a separate, later gate.
Branch: feat/companion-app (Muse-canonical; paired with the Phase 1–5 code already on this
branch — not a docs-only PR to main, per the owner's no-docs-only-PR-to-main policy).
Phase table ref: Gate §12, Phase 6 ("Derived-artifact storage + provenance enforcement — per-tier
policy (§5), generated_by/model/version/source_event_id (§6), client-encryption hook for the
privacy-max/ZK tier"). Marked 🔀 Hybrid: design/spec the storage + provenance + encryption seam with a
thinking model (this gate), implement the body against the fixed contract with Sonnet/auto. The seam
is elevated reasoning because a wrong rule here is a privacy breach (host-readable privacy-max
artifact), a multi-tenant policy defect (a member's companion silently enriching an owner's
notes), or a crypto-custody mistake (server-held key masquerading as zero-knowledge).
Depends on (all accepted):
COMPANION-APP-DESIGN-AND-AUTHORIZATION-GATE.md
(§5 derived-artifact storage paradox, §6 provenance, §12 Phase 6, §13 Phase 0 D1/D3, the "DOES NOT
approve" list); Phase 1
(COMPANION-APP-PHASE-1-ADAPTER-SEAM.md,
lib/model-runtime-lane.mjs — enforceConsentPolicy, isManagedLane); Phase 5
(COMPANION-APP-PHASE-5-BIND-GATE.md,
lib/companion-shell.mjs — authority/runtime capability segregation D5.8, companionAvailable D5.7).
Hard prerequisite (carried, NOT discharged here): the ZK key hierarchy (brief §9.4: client-side
Argon2id → IK → DEK-vault/DEK-memory, envelope encryption) and the two-tier vault registry (brief
§9.3) are owned by the ZK tier and remain unimplemented. Phase 6 defines the client-encryption hook
interface and fails closed wherever a privacy-max artifact cannot be client-encrypted — it does
not build the key hierarchy.
Related code (grounding, not assumptions): mcp/tools/index-enrich.mjs (ai_summary →
writeNote frontmatter), lib/tag-suggest.mjs + lib/vector-store.mjs (embeddings),
lib/memory-consolidate.mjs (runDiscoverPass → insight event), lib/memory.mjs /
lib/memory-event.mjs (createMemoryEvent, store), lib/memory-provider-encrypted.mjs
(AES-256-GCM with server-held KNOWTATION_MEMORY_SECRET — encryption-at-rest, not ZK),
hub/gateway/server.mjs (scopesForRole → vault:read/vault:write),
hub/gateway/billing-middleware.mjs (runBillingGate).
Simple summary
When the AI on your computer reads a note and produces by-products — a short summary, the math "fingerprints" used for search (embeddings), little insight cards (connections, open questions), and a label saying who/what made them — those by-products have to be stored somewhere. If the thinking happened privately on your device but the by-products get saved to the cloud in plain text, your private content has quietly leaked anyway. That is the trap this phase closes.
This document is the rulebook for saving AI by-products. It does not write the saving code. It fixes six rules and argues each against an attacker:
- Where each by-product is allowed to live, per privacy level. Convenience users: cloud, as today. Privacy-max users: never something the cloud operator can read — either it stays on the device or it is locked with the user's own key before it is uploaded.
- A stamp on every by-product saying who made it, with which model and version, from which note and which source event, when, on which lane, and at which privacy level — so it can be trusted, audited, deleted, and re-made when the model is upgraded.
- Who is allowed to save a by-product into someone else's notes. A teammate's on-device AI can enrich your notes only if you already let them read those notes and you turned on "let my team enrich my notes" (off by default). A teammate can never quietly downgrade your privacy level.
- A locked door for the privacy-max level. Until the user-held-key system exists, a privacy-max by-product simply cannot be saved rather than being saved in the clear. No "temporary plain-text for now." Ever.
- By-products die with their note. Delete a note and its by-products are deleted too; in privacy-max, destroying the key makes them permanently unreadable (crypto-shred).
- One single saving door, built so the by-products cannot leak through any side path, the model process can never reach your saved data, and no secret ever lands in a log or an error.
Technical summary
Phases 1–5 delivered the lane/consent decision core, the loopback/OAuth/runtime cores, and the Phase 5 bind contract — but every one of them stopped short of writing a derived artifact. Phase 5 §0 explicitly carried "new derived-artifact storage paths / encryption scheme" forward as NOT lifted — remains Phase 6. This document is that gate. It ratifies seven decisions (D6.1–D6.7) over the single new capability Phase 6 introduces — persisting a derived artifact — built strictly inside the Phase 5 authority group (the runtime/inference group, D5.8, never writes; it only produces raw bytes).
The decisions: D6.1 per-tier, per-artifact storage location (binding routing, fail-closed);
D6.2 the provenance schema (required fields, where stored, downgrade-safe, not a lifecycle state);
D6.3 write-back authorization (reuse Phase 1 enforceConsentPolicy; preserve
delegatedEnrichmentAllowed default-OFF; owner's tier governs; no member silently enriching owner
partitions; no tier downgrade); D6.4 the client-encryption hook interface (a ClientEncryptor
seam that fails closed for privacy-max when ZK keys are absent — no plaintext fallback, ever);
D6.5 retention/deletion (artifacts inherit the source note's retention; delete-on-delete;
crypto-shred for privacy-max); D6.6 the single-writer enforcement mechanism (object-capability:
exactly one derived-artifact writer, no bypass path, enforced by architecture tests); D6.7
provenance integrity + re-enrichment on model upgrade. Every decision defaults fail-closed; no
secret (DEK, IK, JWT, KNOWTATION_MEMORY_SECRET, loopback token) appears in any artifact, log, error,
or provenance field. The implementation that follows ships its own 7-tier suite before any merge to
main.
0. What this gate lifts — and what it deliberately does not
The design gate's "DOES NOT approve (no code)" list
named "storing derived artifacts (ai_summary, embeddings, insight events) under any new storage path
or encryption scheme." Phase 5 §0 reaffirmed it as NOT lifted — remains Phase 6. This gate lifts a
bounded subset of that item — and only that subset.
| Item from the design gate's "DOES NOT approve" list | Phase 6 disposition |
|---|---|
| Storing derived artifacts under a new storage path / encryption scheme | LIFTED for the derived-artifact writer specified in D6.1/D6.6, writing only to the existing stores (note frontmatter via writeNote, the vector store, the memory insight store) under the per-tier routing + provenance + consent contract below. No new top-level store or table is created. |
| New derived-artifact encryption scheme | NOT lifted (interface only). D6.4 defines the ClientEncryptor hook interface and the fail-closed contract. The actual ZK key hierarchy / envelope encryption (brief §9.4) is NOT built here — it remains the ZK tier's gate. Privacy-max writes fail closed until that lands. |
| Server-held-key memory encryption masquerading as ZK | EXPLICITLY FORBIDDEN. The existing KNOWTATION_MEMORY_SECRET AES-256-GCM path is encryption-at-rest, not zero-knowledge (brief §9.1, D3 technical summary). It does not satisfy privacy-max (D6.1, D6.4). |
| New canister routes / new Hub REST endpoints / wire-protocol changes | NOT needed / NOT lifted. Write-back uses the existing authenticated vault-write path (vault:write scope, scopesForRole). Phase 6 adds none. |
| Any change to OAuth client registration or scopes | NOT lifted. Write-back consumes the existing vault:write scope ceiling; it changes nothing server-side. |
| Shipping a companion binary / installer / auto-updater | NOT lifted — remains Phase 7. |
| Tenancy implementation (auto-owner provisioning, hosted role store, invites, effective/owner billing identity) | NOT lifted — hard prerequisite (gate §13 D1 outcome). A Phase 6 write to a delegated partition must not enable until the tenancy gate lands the effective/owner identity; until then D6.3 enables only self-partition derived-artifact writes. |
Net: Phase 6 means "a derived artifact may now be persisted, to the existing stores, under the per-tier + provenance + consent + retention contract below, inside the authority group." It does not mean "build the ZK key hierarchy," "create a new store," or "ship a product."
1. Adversarial threat model (the storage/write-back surface)
Phase 5 modelled the bind/I-O surface. Phase 6 adds the persistence surface: the moment a private inference result becomes a stored byte. Each attacker is paired with the exact Phase 6 control that stops it, argued against the attacker — not pattern-matched.
| # | Attacker capability / failure | Exact Phase 6 control | Decision |
|---|---|---|---|
| P6-a | The storage paradox: inference runs privately on-device, but the ai_summary/embedding/insight is written to a host-readable location, so the private content leaves the device anyway. |
Per-tier routing (D6.1): a privacy-max artifact is only ever written local-only or as ciphertext under a user-held key; a host-readable plaintext write for a privacy-max artifact is structurally impossible (single writer, D6.6) and fails closed if encryption is unavailable (D6.4). | D6.1, D6.4, D6.6 |
| P6-b | Fake zero-knowledge: a privacy-max artifact is "encrypted" with the server-held KNOWTATION_MEMORY_SECRET, which the operator can decrypt — privacy claim is false. |
D6.1/D6.4 forbid server-held-key storage from satisfying privacy-max. Privacy-max requires the ClientEncryptor (user-held key); the existing at-rest path is recognized as convenience-tier-only. privacy_tier=privacy_max + server-held key is a rejected combination. |
D6.1, D6.4 |
| P6-c | Silent delegated enrichment: a member's companion enriches an owner's notes and writes the artifact with no owner consent (the gate §12 canonical defect). | Every write-back passes Phase 1 enforceConsentPolicy with enrichesDelegatedPartition=true computed from "actor ≠ partition owner"; local/openrouter lanes are denied unless the owner set delegatedEnrichmentAllowed (default OFF). Fail-closed. |
D6.3 |
| P6-d | Tier downgrade: a member (or a misrouted job) writes an owner's artifact at a weaker tier than the owner's vault (e.g. a ZK owner's summary stored host-readable because the member's device is convenience-tier). | D6.3: the artifact's privacy_tier is resolved from the owner's vault tier, never the actor's; a write that would downgrade is rejected fail-closed (D1.3(4)). |
D6.2, D6.3 |
| P6-e | Provenance forgery / omission: an artifact lands with no/false generated_by/model, defeating audit, trust, and correct re-enrichment. |
D6.2 requires a complete, validated provenance record as a write precondition; the single writer (D6.6) rejects any artifact missing a required field. Provenance is stamped by the authority group from trusted session/lane state, not from model output. | D6.2, D6.6, D6.7 |
| P6-f | Secret leak into an artifact or its provenance (note body fragment that is a secret, a token, the DEK, the memory secret, a JWT). | createMemoryEvent already rejects sensitive-key payloads (hasSensitiveKeys); D6.2 provenance carries no secret-bearing field by construction (no key material, no token); D6.6 logs fixed reason codes only. The note body is never copied verbatim into provenance. |
D6.2, D6.6 |
| P6-g | Orphaned artifact after note deletion: the source note is deleted but its summary/embedding/insight lingers, readable, indefinitely. | D6.5: note-scoped artifacts inherit the source note's retention and are deleted on note delete; vector entries purged; for privacy-max, key destruction crypto-shreds them. Aggregate insights reference source_event_ids for invalidation/re-enrichment. |
D6.5 |
| P6-h | Bypass write path: a code path persists a derived artifact directly to frontmatter/vector store/memory without going through routing, consent, provenance, or encryption. | D6.6: exactly one derived-artifact writer; an architecture/import test asserts no other module writes a derived artifact to any store; the existing enrichIndexedNotes/runDiscoverPass write paths are migrated to route through the writer (no parallel path). |
D6.6 |
| P6-i | Ambient-authority pivot from inference: the runtime/inference group acquires the vault-write capability or the encryption key and writes/exfiltrates. | The runtime group never writes (Phase 5 D5.8): it returns raw bytes to the authority group, which alone holds vault:write and the ClientEncryptor. Re-asserted by the D6.6 architecture test (runtime-adapter module imports no writer/encryptor/canister module). |
D6.6 (extends D5.8) |
| P6-j | Plaintext fallback under failure: encryption fails or keys are missing, and the system "helpfully" stores the privacy-max artifact in the clear so the feature still works. | D6.4: no plaintext fallback, ever. Encryption failure / absent key for a privacy-max artifact → the write fails closed (artifact dropped or queued local-only encrypted), surfaced as a recoverable error, never downgraded to plaintext. | D6.4 |
| P6-k | Re-enrichment staleness / provenance drift: model upgrades, but stale artifacts keep their old content with no record that a newer model exists, or re-enrichment silently rewrites provenance hiding the original generator. | D6.7: model/version in provenance triggers re-enrichment on upgrade; re-enrichment writes a new provenance record (new created_at, model_version) and is a provenance flag, not a lifecycle state (gate §6) — it never forces the note through the proposal pipeline. |
D6.2, D6.7 |
2. Decision D6.1 — Per-tier, per-artifact storage location (binding routing)
Question: for each derived artifact, where may it live under each privacy tier, and how is that enforced rather than merely documented?
Verified state
Gate §5 + Phase 0 D3 already fixed the policy: convenience artifacts may live host-readable in the cloud as today; privacy-max artifacts are local-only or client-encrypted under a user-held key before upload — never host-readable plaintext, never under a server-held key. Grounding on where the artifacts live today:
ai_summary→ note frontmatter viawriteNote(mcp/tools/index-enrich.mjs), so it inherits the note's own storage + sync path.- Embeddings/vectors → the vector store (
lib/vector-store.mjs, vialib/tag-suggest.mjs). - Insight events (
connections/contradictions/open_questions/topic_count) → a single aggregateinsightmemory event (runDiscoverPass→mm.store('insight', …)), persisted by the memory provider — which on the encrypted path uses server-held-key AES-256-GCM (KNOWTATION_MEMORY_SECRET), i.e. encryption-at-rest, not ZK. - Discovery facets (the body-free projections Scooling's Pattern Map consumes, brief §7.4) are a read-projection of the insight event; they store nothing new and inherit the insight event's tier.
Decision — Routing table is binding; privacy-max never resolves to a host-readable or server-held-key location; resolution is fail-closed
| Artifact | Convenience (server holds key) | Privacy-max / ZK (user holds key) |
|---|---|---|
ai_summary |
Note frontmatter, host-readable, as today. | Frontmatter of a client-encrypted note (the whole note, frontmatter included, is ciphertext under the note/vault DEK) or a local-only summary cache. Plaintext ai_summary in a host-readable note is forbidden. |
| Embeddings / vectors | Cloud vector store, server-side index, as today. | Vectors computed client-side and stored server-side only as ciphertext (sync/backup), or kept local-only; plaintext vectors never leave the device; vector search runs client-side (brief §9.5). |
Insight events (runDiscoverPass) |
Memory store, as today — including the server-held-key AES-256-GCM at-rest path, which is acceptable only at convenience tier. | Computed client-side (companion); stored client-encrypted under a user-held DEK-memory; the server-held-key path does NOT satisfy this tier (brief §9.5). |
| Discovery facets (Pattern Map projection) | Read-projection of the insight event; no new storage; convenience tier. | Read-projection produced client-side from the decrypted insight event; never materialized host-readable. |
D6.1.1 — Tier is resolved, not assumed. The effective privacy_tier for a write is resolved from
the owner's vault tier in the two-tier vault registry (brief §9.3), not from the requesting
actor or the device. The registry is owned by the ZK tier (prerequisite). Until the registry
exists, the only resolvable tier is convenience, and any attempt to write a privacy_max
artifact fails closed (D6.4) — Phase 6 does not invent a tier source.
D6.1.2 — Three legal terminal states only. A derived-artifact write resolves to exactly one of: (a) host-readable (convenience only), (b) client-encrypted ciphertext under a user-held key (privacy-max), (c) local-only (either tier). There is no fourth state; in particular, "server-held-key ciphertext" is classified as (a) host-readable for policy purposes (the operator can decrypt) and is therefore convenience-only.
D6.1.3 — Routing is centralized. The mapping above is implemented in one resolver inside the
single writer (D6.6); no caller chooses a storage location directly. A caller supplies the artifact +
provenance; the resolver picks the terminal state from privacy_tier and fails closed on any
unmapped/ambiguous combination.
Fail-closed
Unknown/unresolvable tier → treat as the most restrictive (privacy-max) → require client-encryption
→ if unavailable, refuse the write. A privacy_max artifact that resolves to host-readable or
server-held-key → reject (never write). Convenience is never assumed for an artifact whose owner
tier cannot be confirmed.
3. Decision D6.2 — Provenance schema (required fields, placement, semantics)
Question: what provenance must every derived artifact carry, where is it stored, and what does it mean (and not mean)?
Verified state
Gate §6 + brief §8.4 require generated_by, model, version, date, source_event_id, and that
provenance is a flag, not a lifecycle state (must not force notes through the proposal pipeline,
brief §7.3). D1.3(4) adds source = companion and the owner's tier for delegated writes. Today
ai_summary carries no provenance (it is a bare frontmatter string); insight events carry
id/type/ts/vault_id from createMemoryEvent but no model/lane provenance.
Decision — A single canonical provenance record, validated as a write precondition, stamped by the authority group
D6.2.1 — Required fields (every derived artifact, all tiers). The write is rejected if any is missing or malformed:
| Field | Type | Meaning / source of truth |
|---|---|---|
generated_by |
string (actor id) | The authenticated actor whose session produced the artifact (the delegate's id for a delegated write, D1.3(4)). From the verified session, never from model output. |
source |
enum companion|in_browser|managed|self_hosted|enterprise|openrouter |
Origin of the inference, mapped from the selected lane. |
model |
string | Model identifier (e.g. the runtime model name). |
model_version |
string | Model version/tag. For local runtimes that expose no version, the runtime_version (runtime build) is recorded and model_version set to a pinned digest/tag — one of the two MUST be a concrete value; both-absent is rejected. |
runtime_version |
string | null | Companion/runtime build identifier (null only for non-companion lanes that have no runtime build). |
lane |
enum (RUNTIME_LANES) |
The selectLane result that served the call (local/self_hosted/…). |
privacy_tier |
enum convenience|privacy_max |
The owner's resolved vault tier (D6.1.1). Governs storage routing (D6.1) and encryption (D6.4). |
source_note_path |
string | null | Vault-relative path of the single source note (note-scoped artifacts: ai_summary, that note's embedding). null for aggregate artifacts. |
source_event_id |
string | string[] | The memory source_event_id(s) the artifact derives from. Single id for note-scoped; the set of consolidation event ids for an aggregate insight. |
created_at |
ISO-8601 string | Generation timestamp. (Reconciles gate §6 date → created_at.) |
artifact_type |
enum ai_summary|embedding|insight|discovery_facet |
Which artifact this provenance describes. |
schema_version |
integer | Provenance schema version, for forward-compatible migration. |
D6.2.2 — Field-name reconciliation (binding). This schema supersedes the looser gate §6 list:
date → created_at; version → model_version (+ runtime_version); adds lane, privacy_tier,
source (the prompt's required set). Implementations MUST use these names.
D6.2.3 — No secret-bearing field, ever. Provenance carries no key material, DEK, IK, JWT,
refresh/loopback token, KNOWTATION_MEMORY_SECRET, raw note body, or consent secret. consentId (if a
managed-lane write) is recorded as an opaque reference id only, never the underlying token. The
existing hasSensitiveKeys guard in createMemoryEvent applies to any provenance persisted as a
memory event; the writer applies the same scan to frontmatter/vector-sidecar provenance.
D6.2.4 — Placement (follows the artifact's store, never a new top-level store).
ai_summary: provenance stored adjacent in the note frontmatter (e.g. anai_summary_provenanceobject), so it inherits the note's tier/encryption exactly (host-readable at convenience; ciphertext inside an encrypted note at privacy-max).- Embeddings: provenance stored in the vector store's per-vector metadata sidecar (or its encrypted equivalent at privacy-max).
- Insight events: provenance embedded in the
insightevent payload (already an object viacreateMemoryEvent), inheriting the memory store's tier (convenience: server-held-key at-rest; privacy-max:DEK-memory).
D6.2.5 — Provenance is a flag, not a lifecycle state (gate §6 / brief §7.3, restated). Stamping provenance MUST NOT route a note into the proposal/review pipeline, change its status, or block reads. It is descriptive metadata for trust, audit, deletion, and re-enrichment.
Fail-closed
Any required field missing/malformed → reject the write (no partial artifact). Provenance fails the
secret scan → reject. privacy_tier=privacy_max with source=managed (a cloud lane produced a
privacy-max artifact) → reject as a contradiction (privacy-max never routes private text to a
managed lane, D2.3).
4. Decision D6.3 — Write-back authorization (owner/delegate; default-OFF; no downgrade)
Question: who may write a derived artifact into which partition, and how is the "member silently enriching an owner's notes" defect kept closed at the storage layer?
Verified state
Phase 1 enforceConsentPolicy already returns lane_policy_denied when
isDelegate ∧ enrichesDelegatedPartition ∧ lane ∈ {local, openrouter} ∧ ¬delegatedEnrichmentAllowed
(default OFF), and cloud_consent_required / lane_policy_denied for managed-lane delegate spend
(delegatedManagedAllowed). D1.2 sets the billing principal = owner of the partition written.
Today's enrichment writers (enrichIndexedNotes, runDiscoverPass) perform no authorization check
before writing — they assume a single-tenant local context. Write authority server-side is the
vault:write scope (scopesForRole).
Decision — Every write-back is gated by enforceConsentPolicy; the owner's tier governs; delegated writes stay default-OFF; tenancy is a hard prerequisite for cross-partition writes
D6.3.1 — The artifact write is the gated event (not just the inference). Phase 1 fixed that the
consent gate applies when a call writes a derived artifact to a partition the actor does not own
(enrichesDelegatedPartition=true). Phase 6 makes the writer the enforcement point: before any
derived-artifact persist, the writer calls enforceConsentPolicy({ lane, containsPrivateData, consentId, isDelegate, delegatedManagedAllowed, enrichesDelegatedPartition, delegatedEnrichmentAllowed })
and persists only on 'allow'. 'lane_policy_denied' and 'cloud_consent_required' abort the
write (surfaced as permission / consent messages respectively, per Phase 1 §2.1).
D6.3.2 — enrichesDelegatedPartition is computed, not trusted. It is true iff the resolved
owner of the target partition (resolveEffectiveCanisterUser → effective) differs from the
authenticated actor (generated_by). Self-partition writes (owner == actor) set it false and are
allowed without the delegated-enrichment opt-in (the actor owns the data).
D6.3.3 — delegatedEnrichmentAllowed stays default-OFF (preserved, not weakened). Phase 6 changes
nothing about the Phase 0/Phase 1 default-OFF posture. A member's companion (local) or BYO
(openrouter) lane producing an owner-partition artifact is denied until the owner flips the
workspace opt-in. This is the storage-layer realization of the gate §12 canonical defect closure —
no member companion silently enriches owner partitions, because the write itself is blocked,
not merely the inference.
D6.3.4 — Owner's tier governs; no downgrade (D1.3(4)). The artifact's privacy_tier (D6.2) is the
owner's vault tier, resolved at write time (D6.1.1). A delegated write cannot select a weaker
tier than the owner's: a ZK owner's artifact is stored client-encrypted even though a member
generated it on a convenience-tier device. A write that would downgrade is rejected.
D6.3.5 — Reading is free; producing is gated (D1.4). Members may read an owner's already-stored
derived artifacts subject to their vault:read scope. Phase 6 gates only production/persistence,
consistent with D1.4.
D6.3.6 — Cross-partition writes require the tenancy gate (hard prerequisite). Until the tenancy
implementation lands the effective/owner identity and hosted role store (gate §13.2), the writer
enables only self-partition writes (enrichesDelegatedPartition can only be false). A delegated
derived-artifact write is disabled rather than guessed — matching D1.2's "delegated managed-lane
and metered ops are not enabled" until the owner billing identity exists.
Fail-closed
enforceConsentPolicy returns anything but 'allow' → no write. Owner tier unresolvable →
treat as privacy-max → D6.4 path (fail closed if no encryptor). Delegate status ambiguous → treat as
delegate (isDelegate=true, enrichesDelegatedPartition=true) → require the owner opt-in → deny if
absent. Tenancy identity absent → only self-partition writes permitted.
5. Decision D6.4 — Client-encryption hook interface (privacy-max; fail-closed; no plaintext fallback)
Question: since the ZK key hierarchy is not implemented, what interface does the privacy-max write depend on, and what happens when it cannot encrypt?
Verified state
Brief §9.4 owns the ZK key hierarchy (client-side Argon2id → IK → DEK-vault/DEK-memory, envelope
encryption, hybrid X25519+ML-KEM-768 wrapping) and is explicitly deferred (gate §13.3, brief §9.7);
none of it is implemented. Today the only encryption is EncryptedFileMemoryProvider with a
server-held key — disqualified for privacy-max (D6.1.2). Phase 6 must therefore define an
interface seam and a fail-closed contract, not an implementation.
Decision — Define the ClientEncryptor hook; privacy-max writes require it; absence/failure fails closed; never a plaintext fallback
D6.4.1 — The hook interface (interface only; no key code here). Privacy-max persistence depends on an injected capability with exactly this shape (named, not built):
ClientEncryptor (held ONLY by the authority group; never by the runtime group)
isAvailable(tier, scope) -> boolean
// true only when a USER-HELD key for this vault/memory scope is unlocked in this
// process (client-side). Always false at convenience tier and whenever the ZK
// hierarchy is absent or locked. Fail-closed default: false.
encrypt(plaintextBytes, { scope, aad }) -> { ciphertext, wrappedDekRef, alg }
// Envelope-encrypt under the user-held DEK for `scope` (vault | memory).
// Returns ciphertext + a reference to the wrapped DEK; NEVER returns key material.
// No decrypt() here — reads/decryption are the client/ZK tier's concern, out of scope.
The encryptor is opaque to Phase 6: it does not expose keys, never logs, and is the only thing
that can turn a privacy-max artifact into a storable byte. Phase 6 calls isAvailable then encrypt;
it never inspects key material.
D6.4.2 — Privacy-max write algorithm (binding).
- Resolve
privacy_tier(D6.1.1). Ifconvenience→ store host-readable per D6.1 (no encryptor needed). - If
privacy_max: requireClientEncryptor.isAvailable(tier, scope) === true. If false (no ZK tier, locked key, wrong device) → fail closed: do not store host-readable; either drop the artifact (re-derivable later) or queue it local-only as ciphertext if a local key is present — never plaintext to a host-readable store. - If available:
encrypt(...), then store only the returned ciphertext +wrappedDekRef+ provenance. The host sees ciphertext; it cannot read the artifact (brief §9.1 ZK property).
D6.4.3 — No plaintext fallback, ever (Rule #1 / hard constraint). There is no code path in which a privacy-max artifact is written in the clear "for now," "temporarily," or "until ZK ships." A privacy-max write with no available encryptor is a fail-closed no-op-plus-error, surfaced as a recoverable state ("enable your private-vault key to enrich"), exactly mirroring Phase 5 D5.3's "locked keychain means re-auth, not store insecurely."
D6.4.4 — Algorithm agility / forward-compat. The alg field and schema_version (D6.2) record the
envelope scheme so the ZK tier can introduce hybrid X25519+ML-KEM-768 DEK wrapping (brief §9.5) without
breaking stored artifacts. Phase 6 fixes the seam, not the algorithm.
D6.4.5 — Capability custody. The ClientEncryptor lives in the authority group (Phase 5 D5.8)
alongside the keychain/session controller. The runtime/inference group never holds it — it cannot
encrypt, cannot decrypt, and cannot write. Re-asserted by the D6.6 architecture test.
Fail-closed
No encryptor / isAvailable=false for privacy-max → no host-readable write (drop or local-only
ciphertext). Encrypt throws → abort the write, fixed reason code, artifact not persisted. Convenience
tier never invokes the encryptor (and must never be silently "upgraded" to skip it — tier is the
owner's, D6.3.4).
6. Decision D6.5 — Retention & deletion (inherit source; delete-on-delete; crypto-shred)
Question: how long do derived artifacts live, and what happens when the source note is deleted or the key is destroyed?
Verified state
Gate §5/D3.4 baseline: derived artifacts inherit the source note's retention; deleting a note
deletes its derived artifacts; destroying the key crypto-shreds privacy-max artifacts. Today the memory
provider supports clearEvents/pruneExpired and ttl per event (createMemoryEvent), and
frontmatter ai_summary lives and dies with the note file. Vector entries are keyed by note path
(lib/vector-store.mjs). Detailed minors/classroom lifecycle is deferred (gate §11).
Decision — Note-scoped artifacts inherit the note 1:1 and are deleted on delete; aggregate artifacts are invalidated/re-derivable; privacy-max destruction is crypto-shred
D6.5.1 — Note-scoped artifacts (ai_summary, that note's embedding) inherit the source note. They
carry the note's retention exactly. Deleting the note deletes them: ai_summary vanishes with the
frontmatter; the note's vector entry is purged from the store (keyed by source_note_path). No
orphan survives the note (P6-g).
D6.5.2 — Aggregate artifacts (insight events, discovery facets) are bound to the memory partition
and reference, don't own, their sources. An insight derived from 50 notes is not deleted when one
source note is deleted (it was never that note's property). Instead: deleting a source note marks the
insight's affected source_event_id(s) stale (the existing runVerifyPass stale-reference
mechanism), flagging it for re-enrichment (D6.7) rather than deletion. The insight event follows
the memory partition's retention (ttl/pruneExpired).
D6.5.3 — Crypto-shred for privacy-max (the strong deletion). For privacy-max artifacts, destroying
the user-held key (DEK-vault/DEK-memory or the IK that wraps them) renders all ciphertext
artifacts permanently unreadable — deletion-by-key-destruction (brief §9.4 "hard truth of ZK"). This is
the privacy-max guarantee that plaintext deletion cannot match: even un-garbage-collected ciphertext is
inert. The writer records wrappedDekRef (D6.4) so a per-scope key destruction shreds exactly the
intended artifacts.
D6.5.4 — Deletion is also a single-path operation. Derived-artifact deletion routes through the same single writer/owner (D6.6) so that a delete cannot be partial (frontmatter cleared but vector left behind). One delete call → all stores for that note/scope.
D6.5.5 — Deferred (not Phase 6 blockers). Minors/classroom stricter retention and legal-hold/export semantics remain the deferred consent/data-lifecycle item (gate §11). D6.5.1–D6.5.3 are the safe baseline until that lands.
Fail-closed
Deletion that cannot confirm removal from every store for a note → reported as failed/partial, retried; never silently treated as complete. Crypto-shred where the key cannot be confirmed destroyed → surfaced, not assumed.
7. Decision D6.6 — Single derived-artifact writer (object-capability enforcement)
Question: what mechanism guarantees that every derived-artifact persist (and delete) goes through routing + consent + provenance + encryption — with no bypass?
Verified state
Phase 5 D5.8 established object-capability segregation (authority group holds secrets/handles; runtime
group holds none) and enforced it with an architecture/import test. Today, two independent write
paths exist with no gate: enrichIndexedNotes (frontmatter) and runDiscoverPass (memory). A new
unguarded third path (vectors) would reintroduce P6-h.
Decision — Exactly one DerivedArtifactWriter in the authority group; all stores reached only through it; enforced by architecture tests, not convention
D6.6.1 — One writer, one delete path. A single DerivedArtifactWriter capability (authority group)
is the only module that may persist or delete a derived artifact in any store (frontmatter via
writeNote, vector store, memory insight). Its write(artifact, provenance, context) performs, in
order: (1) provenance validation (D6.2) → (2) tier resolution (D6.1.1) → (3)
enforceConsentPolicy (D6.3) → (4) encryption routing (D6.4) → (5) terminal-state store
(D6.1.2). Any step failing → no write, fixed reason code.
D6.6.2 — Existing writers are migrated, not duplicated. enrichIndexedNotes and runDiscoverPass
are refactored to call the writer for persistence; their direct writeNote/mm.store('insight', …)
calls are removed (no parallel path). Per the MuseHub/Knowtation "delete on sight" posture, the old
direct-write code is deleted, not left as a fallback.
D6.6.3 — Runtime group cannot write (extends D5.8). The DerivedArtifactWriter, the
ClientEncryptor, and the vault:write session live in the authority group. The runtime/inference
group returns raw artifact bytes to the authority group and holds no reference to the writer,
encryptor, canister, or vault client. An architecture/import test asserts the runtime-adapter module
and companion-runtime-manager import none of { derived-artifact writer, client-encryptor, write.mjs, vector-store, memory writer, canister/vault client }.
D6.6.4 — No-bypass test (build-blocking). A repository-wide test asserts that no module other than
the writer calls writeNote with a derived-artifact frontmatter field, mm.store('insight', …), or a
vector-store upsert for a derived artifact. If a refactor introduces a second path, the test fails the
build — the merge is blocked, not shipped with a warning (mirrors D5.8's posture).
Fail-closed
Any store reachable without the writer → architecture test fails the build. Writer step failure → artifact not persisted. Ambiguity about which store an artifact belongs to → reject (the writer owns the mapping, D6.1.3).
8. Decision D6.7 — Provenance integrity & re-enrichment on model upgrade
Question: how does provenance stay trustworthy over time, and how is re-enrichment handled without hiding history or triggering the proposal pipeline?
Verified state
Gate §6 / brief §8.4: record model/version/date; re-enrich when the model upgrades;
provenance is a flag, not a lifecycle state. runVerifyPass already detects stale references.
Decision — Re-enrichment writes a fresh provenance record; never rewrites history silently; never a lifecycle state
D6.7.1 — Upgrade trigger. When the active model/runtime version (D6.2 model_version/
runtime_version) is newer than an artifact's recorded version, the artifact is eligible for
re-enrichment. Eligibility is advisory (a flag), batched per the brief §7.2 lazy/cached strategy —
it does not force immediate recompute.
D6.7.2 — Re-enrichment is a new write through the single writer (D6.6). It produces a new
provenance record (new created_at, new model_version, same source_note_path/source_event_id),
subject to the same consent gate (D6.3) and tier routing (D6.1). It replaces the artifact content
but the new provenance truthfully records the current generator — it never forges the original.
D6.7.3 — Provenance integrity binding. Provenance binds to the artifact it describes (co-located per
D6.2.4) and, at privacy-max, is encrypted with it (so provenance is not a host-readable side channel
about a private artifact). A privacy-max artifact's generated_by/source_note_path are not stored
host-readable.
D6.7.4 — Still a flag, not a lifecycle state (gate §6, restated and locked). Re-enrichment MUST NOT push the note into the proposal/review pipeline, change note status, or gate reads. It is a derived metadata refresh.
Fail-closed
Version unknown/unparseable → treat artifact as eligible for re-enrichment (safe: recompute rather than trust stale) but never block reads. Re-enrichment write fails the consent/tier/encryption gates → the existing artifact is retained unchanged (no destructive half-write).
9. How Phase 6 discharges the prior phases' deferred obligations
| Source obligation | Discharged by |
|---|---|
| Design gate §5 / Phase 0 D3 — per-tier derived-artifact storage policy | D6.1 (binding routing, fail-closed; server-held-key ≠ privacy-max) |
| Design gate §6 / brief §8.4 — provenance fields + "flag not lifecycle state" + re-enrich on upgrade | D6.2 (canonical schema), D6.7 (re-enrichment) |
Phase 0 D1.3(4) — delegated write records generated_by/source/owner-tier; no downgrade |
D6.2, D6.3.4 |
Phase 0 D1.3(2) / Phase 1 enforceConsentPolicy — member companion may not silently enrich owner notes |
D6.3 (the write is the gated event; default-OFF preserved) |
| Phase 5 §0 — "new derived-artifact storage paths / encryption scheme … remains Phase 6" | This gate (bounded subset, §0) |
| Phase 5 D5.8 — object-capability segregation (runtime group holds no authority) | D6.6.3 (runtime group cannot write; encryptor/writer authority-only) |
| Brief §9.4/§9.5 — privacy-max client-encryption (ZK) | D6.4 interface only; ZK key hierarchy remains the ZK tier's gate (hard prerequisite) |
Gate §11 — retention/deletion of ai_summary/embeddings/insight |
D6.5 (safe baseline; minors/classroom still deferred) |
Remaining external dependencies (NOT discharged here): the ZK key hierarchy + two-tier vault registry (brief §9 — required before any real privacy-max write; until then privacy-max fails closed) and the tenancy implementation (effective/owner identity — required before any delegated write; until then only self-partition writes enable). Both are pre-existing hard prerequisites, not new.
10. 7-tier test obligations (Phase 6 derived-artifact storage layer)
Aaron's Rule #0. The prior phases' suites do not absolve the storage/write-back layer of its own
tests. Before any merge to main, the Phase 6 implementation ships all seven tiers:
| Tier | Focus |
|---|---|
| Unit | Provenance schema validation (every required field; reject missing/malformed; model_version-or-runtime_version rule); tier resolver (D6.1) incl. fail-closed unknown→privacy-max; ClientEncryptor.isAvailable=false→no host-readable write; routing table maps each artifact×tier to the correct terminal state; enrichesDelegatedPartition computation (owner≠actor). |
| Integration | Writer pipeline end-to-end: validate→resolve tier→enforceConsentPolicy→encrypt→store, across ai_summary/embedding/insight; migrated enrichIndexedNotes/runDiscoverPass route through the writer; convenience write lands host-readable with provenance; privacy-max write lands ciphertext + wrappedDekRef, never plaintext. |
| End-to-end | Sign in → local inference (Phase 5) produces a summary → writer persists per owner tier; delegated-enrichment denied without delegatedEnrichmentAllowed, allowed with it; convenience vs privacy-max branch; note delete removes summary + vector (+ insight stale-flagged). |
| Stress | High-volume concurrent writes across all three stores; large embedding batches to the writer; many delegated-write authorization checks; deletion fan-out across stores under load; no partial/half-write under contention. |
| Data-integrity | Provenance round-trips intact and co-located with its artifact; no orphan after note delete (P6-g); crypto-shred renders privacy-max ciphertext unreadable; aggregate insight stale-flag + re-enrichment preserves source_event_id history; re-enrichment never destroys the prior artifact on a failed gate. |
| Performance | Writer overhead bound per artifact; tier-resolution + consent-gate cost; encryption hook latency budget; deletion fan-out latency; no event-loop starvation on batch enrichment. |
| Security (centerpiece) | No privacy-max artifact ever written host-readable or under the server-held key (P6-a/P6-b); no plaintext fallback when encryption unavailable (P6-j); delegated write fail-closed without owner opt-in (P6-c); no tier downgrade by a delegate (P6-d); provenance cannot be forged/omitted (P6-e); no secret in any artifact, provenance field, log, or error (P6-f); single-writer no-bypass architecture test build-blocking (P6-h); runtime group imports no writer/encryptor/vault module (P6-i, extends D5.8); convenience never silently masquerades as privacy-max and vice-versa; global fail-closed posture. |
11. Constraints honored
- Decisions only — no storage/writer/encryption code. This document fixes the contract; it writes none.
- Muse-canonical, on
feat/companion-app, paired with the Phase 1–5 code already there — not a docs-only PR tomain. - No host-readable privacy-max derived artifacts (D6.1); server-held-key ≠ ZK (D6.1.2/D6.4).
- No temporary plaintext fallback for privacy-max, ever (D6.4.3 / Rule #1).
delegatedEnrichmentAlloweddefault-OFF preserved; no member companion silently enriching owner partitions (D6.3) — the write is the gated event.- Client-encryption is an interface only (D6.4); the ZK key hierarchy (brief §9) is a hard prerequisite, not built here; privacy-max fails closed until it lands.
- Derived artifacts inherit source-note retention; delete-on-delete; crypto-shred (D6.5).
- No secret in any artifact, provenance field, log, or error (D6.2.3, D6.6); no ambient authority — the runtime/inference group cannot write (D6.6.3, extends Phase 5 D5.8).
- Fail-closed on every ambiguous point (tier, consent, encryption, delegation, deletion).
- No assumptions stated as fact — every cross-reference is anchored to a verified file/section in the gate, the brief, or Phases 1–5 code.
- No new store/table, no new canister/Hub route, no OAuth scope change, no GitHub mirror/deploy; packaging/signing remain Phase 7.
12. Approval table
| Decision | Recommendation | Owner approval |
|---|---|---|
| D6.1 — per-tier/per-artifact storage routing; privacy-max never host-readable / never server-held-key; three terminal states; centralized fail-closed resolver | CONFIRM (locks D3 per-artifact) | ✅ approved 2026-06-06 |
D6.2 — canonical provenance schema (required fields incl. generated_by/model/model_version|runtime_version/lane/privacy_tier/source_note_path/source_event_id/created_at); validated as write precondition; no secret-bearing field; flag-not-lifecycle |
ACCEPT | ✅ approved 2026-06-06 |
D6.3 — write-back authorization via enforceConsentPolicy; delegatedEnrichmentAllowed default-OFF preserved; owner's tier governs, no downgrade; cross-partition writes gated on the tenancy prerequisite |
ACCEPT | ✅ approved 2026-06-06 |
D6.4 — ClientEncryptor hook interface only; privacy-max requires it; no plaintext fallback; fail-closed when ZK keys absent; algorithm-agile |
ACCEPT | ✅ approved 2026-06-06 |
| D6.5 — note-scoped artifacts inherit + delete-on-delete; aggregate insights invalidated/re-derivable; crypto-shred for privacy-max; single-path deletion | ACCEPT | ✅ approved 2026-06-06 |
D6.6 — single DerivedArtifactWriter in the authority group; existing writers migrated (old paths deleted); runtime group cannot write; no-bypass architecture test build-blocking |
ACCEPT | ✅ approved 2026-06-06 |
| D6.7 — re-enrichment writes fresh provenance (no silent rewrite); provenance integrity bound + encrypted at privacy-max; flag-not-lifecycle | ACCEPT | ✅ approved 2026-06-06 |
On owner approval of D6.1–D6.7, the Phase 6 implementation (the DerivedArtifactWriter, provenance
schema, per-tier routing, ClientEncryptor seam, retention/deletion, and the migration of the existing
enrichment/discovery writers) is unblocked — itself gated on the §10 7-tier test obligation before any
merge to main. The ZK key hierarchy (brief §9) and the tenancy implementation (effective/owner
identity) remain hard prerequisites for real privacy-max and delegated writes respectively; Phase 6
fails closed in their absence. Phase 7 (packaging/signing/notarization/auto-update) remains a
separate, later gate and is not approved by this document.