Files
maka/docs/archive/memory-threat-model-pr-memory-1.md
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

12 KiB

Memory Subsystem Threat Model

Archived on 2026-07-13. This document records the contract-only PR-MEMORY-1 boundary, not the current product implementation. Source and focused tests own the active contract.

PR-MEMORY-1 is a contract-only package. It MUST NOT add IPC handlers, storage repositories, embedding providers, Recall tool runtime, renderer UI, or settings flags. Implementation packets (PR-MEMORY-2+) sit downstream of this contract — none may bypass validateMemoryWriteRequest.

Anchors:

  • notes/reference implementation-memory-reverse-2026-05-25.md (kenji msg 66fd3eab) — reference implementation facts + Maka quasi-memory inventory.
  • notes/pr-memory-0-audit-plan.md — pre-audit plan skeleton.
  • #my-ai:2f91befb thread, msgs 22209a1b (xuan gate review + scope adjust) + fb95a158 (kenji invariant ask) + 68a1bcb5 (xuan review priorities).

Source separation (gate #8 + type system)

Memory is not one surface. The contract types separate them so a quasi-memory observation cannot become a durable memory entry by typing alone.

  • MemorySource (durable-capable): user_authored, chat_extracted. These can produce 'active' entries IF a confirmedAt timestamp is supplied AND the request did not originate from the renderer.
  • MemoryCandidateSource (non-durable): voice_transcript, activity_observation, cu_observation, search_recall, daily_review. These can only produce 'draft' or 'review_required' entries. The normalizer rejects any candidate-source request with persistenceState: 'active' (MemoryBlockReason='candidate_source_no_active').

MemorySource and MemoryCandidateSource are disjoint enums. The DurableMemoryEntry shape's source field is typed MemorySource, and DraftMemoryEntry's source field is typed MemoryCandidateSource. A Voice transcript cannot type-check into a DurableMemoryEntry even before reaching the validator.

Assets

  • Memory entry content (NFC-normalized, control/zero-width-stripped).
  • Memory entry source and the user-visible provenance derived from it.
  • Memory entry scope ('workspace' / 'session').
  • Memory entry confirmedAt (a CLAIM about user action — gate #9 forbids renderer from setting this).
  • Pending/review queue contents.
  • Future embedding vectors (out of scope for v1 contract; embeddingProvider: 'disabled' is hard-coded).
  • Citation index / prompt-injection references when memory is consumed (gate #4).
  • Export bundles (gate #6 reversibility).

Boundaries

  1. Renderer → main: renderer may PROPOSE memory writes but MUST NOT supply confirmedAt. Confirmation event recording is a main-side responsibility.
  2. Main → store: every write passes validateMemoryWriteRequest; the store boundary refuses any request that did not.
  3. Store → embedding/provider: NOT IMPLEMENTED in v1. embeddingProvider: 'disabled' literal in MemoryCapabilitySnapshot prevents accidental wiring of a provider.
  4. Store → prompt-build: only 'active' durable entries are eligible for injection, and only with a visible citation surface (MemoryUsePolicy='cited_only'). The contract does NOT include 'silent'.
  5. Quasi-memory surfaces → memory: forbidden as a direct write path; the only way into memory from quasi-memory is via a draft entry that the user explicitly promotes.

Nine privacy gates (locked by @xuan 22209a1b)

# Gate Enforcement in contract
1 default-off MEMORY_MODES includes 'off'; fresh-install snapshot MUST be 'off'. Step #1 of validator rejects with MemoryBlockReason='mode_off'.
2 manual confirm before durable write Durable 'active' path requires confirmedAt. Validator rejects with MemoryBlockReason='manual_confirm_required' when missing/invalid.
3 reversible delete/export Contract docs require reversible operations exist BEFORE any auto-write capability is added. v1 contract has no auto-write; downstream packets MUST add delete + export shapes before adding any write driver.
4 incognito read+write disable MemoryWriteRequestContext.incognitoActive short-circuits validator at step #2 with MemoryBlockReason='incognito_active'. (Read path is a future contract — the same flag MUST gate it.)
5 no auto sleep consolidation NOT in MemorySource enum; NOT in MemoryCandidateSource. No type exists for an automated consolidation source — adding one is a contract change requiring explicit review.
6 visible citation MemoryUsePolicy only allows 'never' or 'cited_only'. No 'silent' policy. Adding 'silent' requires expanding the enum (i.e. requires explicit review).
7 no hidden activity promotion activity_observation / cu_observation are MemoryCandidateSource only. Validator rejects persistenceState='active' for these via gate candidate_source_no_active.
8 provider+embedding leakage boundary MemoryCapabilitySnapshot.embeddingProvider is the literal 'disabled'. v1 has no provider field on entries. Downstream wiring of any provider requires extending this snapshot type.
9 renderer cannot forge provenance/readiness MemoryWriteRequestContext.originatedFromRenderer=true blocks any durable 'active' write — even with valid confirmedAt — returning MemoryBlockReason='renderer_provenance_forged'.

What NOT to copy from reference implementation (negative reference list)

Per notes/reference implementation-memory-reverse-2026-05-25.md:

  1. Sleep cycle / 4-layer consolidation (22-embeddings.md) — Maka has no sleep stage. No code path automatically aggregates, summarizes, or merges memory entries.
  2. Auto-extract from chat — every chat_extracted write requires explicit confirmation. Even after assistant suggests, user must click confirm; no silent insertion.
  3. Auto-retrieve / Recall tool runtime — out of scope for v1 contract. Adding it requires a separate retrieve-side contract with its own threat model.
  4. LLM-mediated forget/delete — only user-initiated deletes allowed. No "the model decided this is no longer relevant".
  5. Activity-derived memory — Activity recorder feeds Maka memory only via activity_observation candidate source, never as a durable source. Activity audit (separate lane) MUST run before any activity→memory pipeline.
  6. Unauthenticated local route (reference implementation's 05-express-api.md exposes memory over local HTTP) — Maka memory MUST NOT have a local HTTP endpoint; if Local Gateway lane lands, it MUST go through capability scope allowlist + audit log, never default-on.
  7. Cloud embedding fallback — embeddingProvider: 'disabled' lock. Adding a provider requires an explicit contract extension; no silent OpenAI/Cohere/local hybrid.
  8. Soul tree / ~/.config/reference implementation long-term file — out of v1 scope. If durable persistence lands (PR-MEMORY-2+), it sits inside the existing workspace store, not a separate global tree.

Quasi-memory exclusion list (gate #7 + #8)

These existing Maka surfaces contain durable user-correlated data BUT MUST NOT be treated as MemorySource and MUST NOT auto-promote:

  • settings.json — personalization fields, onboarding milestones.
  • skills/ — user-installed skill content.
  • usage_log (telemetryRepo) — historical queries / latency / errors.
  • sessions/*/session.jsonl — session history.
  • workspace/ instruction files — prompt-time injection only.
  • Capability snapshot + runtime probe history (Health Center).
  • Visual-smoke fixtures.
  • Daily Review candidates (post-Daily Review lane).
  • Search index hits (MemorySource='search_recall' is the candidate-source bridge, not durable).
  • Voice transcripts (voice_transcript candidate source).
  • CU / Activity observations (candidate sources).

What the contract catches (gate, per @xuan 0c9c68f9): a request whose source value is the literal name of a quasi-memory surface — source: 'usage_log', source: 'health_probe', source: 'session_summary', etc. — is rejected by validateMemoryWriteRequest with MemoryBlockReason='unknown_source', regardless of other fields. The string is not in MEMORY_SOURCES or MEMORY_CANDIDATE_SOURCES.

What the contract CANNOT catch (source-laundering — downstream concern): if a downstream IPC handler or store boundary reads a usage-log entry, copies its body into a fresh { source: 'chat_extracted', confirmedAt: ..., content: ... } payload, and submits THAT to the validator — the validator accepts it. The validator sees a well-formed durable chat_extracted write; it has no way to know the body originated from a quasi-memory surface. Source-laundering defense is a per-IPC-handler / per-store-boundary responsibility (provenance lock at the input layer), NOT a contract-layer responsibility. Any future implementation packet that introduces a "promote draft to active" or "summarize quasi-surface as chat_extracted" path MUST add a separate provenance gate at that boundary.

Minimum test matrix (locks the gates above)

The companion test file packages/core/src/__tests__/memory.test.ts MUST contain at least these assertions (each labeled G#N for the gate locked):

  • G1: mode='off' → mode_off block (any source, any state).
  • G2: durable user_authored + persistenceState='active' without confirmedAt → manual_confirm_required.
  • G2b: durable chat_extracted + persistenceState='active' without confirmedAt → manual_confirm_required.
  • G3: contract has no delete/export-bypass shape (must be added by downstream packet only).
  • G4: incognitoActive=true + any valid write → incognito_active.
  • G5: no automated-consolidation source exists in MEMORY_SOURCES or MEMORY_CANDIDATE_SOURCES.
  • G6: MEMORY_USE_POLICIES includes only 'never' and 'cited_only'.
  • G7: candidate-source + persistenceState='active' → candidate_source_no_active (one assertion per candidate kind).
  • G8: MemoryCapabilitySnapshot.embeddingProvider is the literal 'disabled'.
  • G9: durable + active + originatedFromRenderer=true → renderer_provenance_forged.

Plus normalizer matrix:

  • normalizeMemoryContent: typeof reject, NFC, control/zero-width strip, trim, empty reject, cap reject.
  • normalizeMemorySource: known durable, known candidate, unknown rejected.
  • normalizeMemoryMode / normalizeMemoryPersistenceState / normalizeMemoryScope: closed-enum reject for non-members.
  • validateMemoryWriteRequest returns canonical DurableMemoryEntry or DraftMemoryEntry (not raw input) on success.
  • validateMemoryWriteRequest rejects mode_only + candidate (mode_disallows_candidate).

Forbidden surfaces in PR-MEMORY-1 source diff

Reviewer source-grep MUST NOT find these in the PR:

  • ipcMain.handle — no IPC handler is wired.
  • BrowserWindow / renderer imports — no renderer code path touches memory.
  • fetch( / XMLHttpRequest — no provider/network call.
  • embedding / vector — no embedding logic (the contract field is the literal 'disabled').
  • Recall / recall_tool — no Recall tool runtime.
  • Storage repo names (telemetryRepo, connectionStore, sessionStore, etc.) — no persistence path.
  • Settings field additions for memory — contract is independent of settings shape until PR-MEMORY-2 adds them.