# Memory-Extraction Threat Model

Ground truth for the hardening work tracked in issue #565. Describes the threat
Remnic's memory surface faces from adaptive data-extraction attacks, what we
protect today, and what we have not yet measured.

This document is the ground truth for PRs 2–5 (attack harness, baseline
measurement, query-budget mitigation, anomaly-detection mitigation). It
deliberately does not propose implementations.

## 1. Scope

### In scope
- Adaptive extraction attacks that reach Remnic through the **MCP surface**
  (`packages/remnic-core/src/access-mcp.ts`), the **HTTP surface**
  (`packages/remnic-core/src/access-http.ts`), or the **CLI surface**
  (`packages/remnic-core/src/access-cli.ts`).
- An attacker who has a valid bearer token — or who shares a host process with
  a connector that has one — and is attempting to read memory they should not
  read, including memory in another namespace or memory about topics they did
  not contribute.
- Read-path extraction in particular: `remnic.recall`, `remnic.memory_search`,
  `remnic.lcm_search`, `remnic.memory_get`, `remnic.memory_timeline`,
  `remnic.memory_entities_list`, `remnic.entity_get`, `remnic.memory_profile`,
  `remnic.briefing`, `remnic.review_queue_list`.
- Passive leakage through debug / introspection tools:
  `remnic.memory_last_recall`, `remnic.memory_qmd_debug`,
  `remnic.memory_graph_explain`, `remnic.memory_intent_debug`.

### Out of scope
- An attacker with filesystem access to `memoryDir`. If they can read
  `namespaces/<ns>/facts/*.md`, they have already won; encryption-at-rest is a
  separate initiative and does not belong to this threat model.
- An attacker with shell access to the gateway process (can read env vars,
  attach ptrace, etc.).
- Supply-chain attacks against `@remnic/core` itself, its transitive npm deps,
  or the QMD binary.
- Prompt-injection that flows into memory via the `observe` path with the goal
  of influencing future behavior (this is a separate concern tracked as memory
  poisoning; see `trust-zones.ts`).
- Cryptographic attacks against the bearer-token format.

## 2. Assets

Listed in rough order of sensitivity.

| Asset | Location | Why it matters |
|---|---|---|
| Raw memory content (facts, corrections, decisions, preferences) | `memoryDir/namespaces/<ns>/facts/**/*.md`, `corrections/`, `decisions/` | Contains names, emails, preferences, private facts about real people and projects. Primary target. |
| Entity graph + relationships | `memoryDir/namespaces/<ns>/entities/*.md` | Discloses who the user talks to, works with, relates to. Graph-shape leaks useful even without content. |
| LCM conversation archive | `memoryDir/namespaces/<ns>/lcm/**` | Near-verbatim conversation turns. Richer than extracted facts. |
| `IDENTITY.md` + `profile.md` | `workspace/IDENTITY.md`, `profile.md` | Behavioral profile; personal by design. |
| Trust-zone records | `memoryDir/namespaces/<ns>/trust-zones/` | Discloses what the system *believes* about provenance. Signal for a poisoning follow-up. |
| Recall-audit trail | `<pluginStateDir>/transcripts/<YYYY-MM-DD>/<sessionKey>.jsonl` | Past queries, injected content sizes, candidate memory IDs. Disclosure reveals what else the user has been asked. |
| Extraction-judge cache | in-memory in `extraction-judge.ts` | Borderline; leaks which candidate facts were judged un-worthy. |
| Work-layer tasks/projects | `memoryDir/namespaces/<ns>/work/**` | User's task list, deadlines, collaborators. |
| Bearer tokens | `~/.remnic/tokens.json` (new) / `~/.engram/tokens.json` (legacy fallback) — see `defaultTokensPath` / `legacyTokensPath` in `packages/remnic-core/src/tokens.ts` | A leaked token gives T2 capability to an attacker. |
| Shared context / feedback inbox | `memoryDir/shared-context/**` | Cross-agent coordination notes. |

## 3. Attacker capability tiers

Referenced throughout the rest of this document.

### T1 — Unauthenticated external
No valid token; reaches the HTTP listener or MCP stdio surface from outside.
Every HTTP route is gated by `isAuthorized()` in `access-http.ts`
(`Authorization: Bearer …`, constant-time compared). MCP-over-stdio is only
reachable by a process the host already trusts to spawn, so T1 against stdio
requires T4 first. T1 against HTTP is blocked by the token check.

### T2 — Valid token, acting inside its own namespace
The common case. A legitimate connector (Codex, Replit, Openclaw host) holds a
valid token and uses it for the namespace it is entitled to.

A T2 attacker in this sense is a **compromised or adversarial agent** on the
user's machine: malware running in a user shell, a malicious VS Code
extension, an MCP connector that was installed from an untrusted source. It
holds a real token, talks to its real namespace, and *does not* try to cross
namespaces — instead it tries to extract **everything** in the namespace it
legitimately has access to via adaptive querying.

This is the primary tier ADAM-style attacks target, and the primary target of
PR 4's query-rate budget and PR 5's anomaly detection.

### T3 — Valid token, attempting cross-namespace retrieval
A connector that has a token but attempts to read memory belonging to a
different principal / namespace, either by:

- passing a forged `namespace` parameter in a tool call,
- passing a forged `X-Engram-Principal` header when `trustPrincipalHeader` is
  enabled,
- crafting a `sessionKey` whose prefix rule maps to another principal.

Namespace ACLs (`canReadNamespace` / `canWriteNamespace` in
`packages/remnic-core/src/namespaces/principal.ts`) are the primary defense,
augmented by `resolveReadableNamespace` in `access-service.ts` which refuses
unauthenticated reads when `namespacesEnabled`. T3 is largely addressed by
existing code; the work in #565 is to *measure* the residual leakage through
shared-namespace promotion and recall snapshots.

### T4 — Co-resident process / filesystem access
Can read `memoryDir`, tokens file, or attach to the gateway process. Out of
scope. Note: on today's default install, `memoryDir` is a plain directory with
user-readable permissions; there is no cryptographic protection and this is
intentional given the out-of-scope statement in the issue.

## 4. Attack surfaces

### 4.1 MCP tools
Enumerated in `access-mcp.ts:98-817`. Every read-path tool that touches memory
is reachable by any client that successfully completes the MCP `initialize`
handshake and passes `tools/call`. The surface is broad — **47 tool names**
including legacy-alias pairs — so attackers have many phrasings to try.

Read-path tools that return memory content:

- `recall`, `recall_explain`, `recall_tier_explain`
- `memory_get`, `memory_timeline`, `memory_search`, `lcm_search`
- `memory_entities_list`, `entity_get`
- `memory_profile`, `memory_identity`, `identity_anchor_get`
- `memory_questions`, `memory_last_recall`
- `memory_intent_debug`, `memory_qmd_debug`, `memory_graph_explain`
- `review_queue_list`, `review_list`
- `day_summary`, `briefing`
- `continuity_incident_list`, `continuity_audit_generate`

Write-path tools are not the extraction attack surface but are listed for
completeness: `memory_store`, `suggestion_submit`, `observe`,
`context_checkpoint`, `memory_promote`, `memory_feedback`,
`memory_governance_run`, `procedure_mining_run`, plus the continuity/work/shared
families.

### 4.2 HTTP surface
`access-http.ts` exposes REST routes at `/engram/v1/*` and a single MCP
transport at `POST /mcp`. Authentication is a bearer token checked with
`timingSafeEqual`. Principal is resolved in this order (see
`docs/namespaces.md`):

1. `X-Engram-Principal` header, if `trustPrincipalHeader` is enabled.
2. Adapter-derived identity from adapter headers (`adapter-id`, etc.).
3. Server default principal (`--principal`).
4. Session-key prefix rules.
5. `"default"`.

A global write rate-limit exists (`WRITE_RATE_LIMIT_WINDOW_MS = 60_000`,
`WRITE_RATE_LIMIT_MAX_REQUESTS = 30` at `access-http.ts:59-60`). It applies
only to write routes and is global (not per-principal). Read routes —
including `/engram/v1/recall`, `GET /engram/v1/memories` (list/browse),
`GET /engram/v1/entities`, `POST /engram/v1/lcm/search`, and
`GET /engram/v1/review-queue` — have no rate limit. Note: there is no
`/engram/v1/memory/search` HTTP route; the `memory_search` capability is only
exposed via the MCP tool surface (`access-mcp.ts`).

### 4.3 CLI access
`remnic recall`, `remnic memory search`, `remnic memory get`, and related
commands run in-process with the same permissions as the invoking user. Out
of the MCP threat model but in scope for the harness because the same
orchestrator code paths are exercised — PR 2's in-process fixture will drive
the orchestrator directly rather than going through transports.

## 5. Known attack pattern: ADAM-style adaptive extraction

From the ADAM paper (Apr 2026, arXiv 2604.09747): the attacker issues a
sequence of queries to a memory-backed agent, uses **entropy** of the response
distribution to pick the next query, and iterates. Published results show up
to 100% ASR (attack success rate) on the systems tested.

Against Remnic, the concrete shape is:

1. Attacker completes `initialize` on MCP and gets `tools/list`.
2. Attacker issues a seed `remnic.recall` query ("what do you know about X?")
   or `remnic.memory_search` query.
3. Response contains summaries and memory IDs. Attacker feeds these back into
   the loop to pick the next query with highest expected information gain.
4. Parallel channels — `remnic.memory_entities_list` to enumerate the entity
   graph, `remnic.memory_qmd_debug` to read index statistics,
   `remnic.memory_last_recall` to read the *previous* recall snapshot — give
   the attacker side-information the paper's systems did not have.

Within T2 (own namespace), none of these calls today fail a policy check. The
attacker gets a full response every time, constrained only by `topK` defaults
and whatever the direct-answer tier decides to inline. ASR is **not measured**
today; PR 3 will produce the baseline number.

For T3 (cross-namespace), `canReadNamespace` rejects the request early when
the requested namespace does not include the principal in its `readPrincipals`
list, so direct cross-namespace queries fail. Residual T3 leakage paths are:

- `shared` namespace: by design, all principals can read. Auto-promotion
  (`autoPromoteToSharedEnabled`) copies facts matching
  `autoPromoteToSharedCategories` into `shared`. An attacker with T2 access to
  any namespace can query `shared` and get facts promoted from other
  principals. The harness in PR 2 must include this path.
- Debug snapshots (`memory_last_recall`, `memory_graph_explain`) currently
  return the *last* snapshot without a namespace ACL check in every code path
  — `access-service.ts` does check via `resolveReadableNamespace` when the
  snapshot has a namespace, but the "no snapshot namespace" branch deserves
  audit under PR 2's harness.

## 6. Existing mitigations

### 6.1 Namespace ACLs
`canReadNamespace` / `canWriteNamespace` in `namespaces/principal.ts` check the
requested namespace against `namespacePolicies[*].readPrincipals` /
`writePrincipals`. Wildcards (`"*"`) are explicit. `resolveReadableNamespace`
in `access-service.ts` adds a hard gate: when `namespacesEnabled` is true and
the principal is absent, the call is rejected with
`authentication required: namespaces are enabled and no principal was supplied`.

Status: **Effective for direct T3 namespace forgery.** Not a defense against
T2-within-own-namespace extraction.

### 6.2 Bearer-token authentication + rotation
`access-http.ts:907-924` enforces `Authorization: Bearer …`, with a dynamic
loader (`authTokensGetter`) so rotation takes effect without restart. Constant-
time comparison via `timingSafeStringEqual` (`access-http.ts:916`).

Status: **Effective for T1.** Does not bound T2 behavior once the token is
valid.

### 6.3 Write rate-limit
Global 30-writes-per-60s on HTTP writes (`access-http.ts:59-60`). Protects
against write-path abuse and memory poisoning; does not apply to reads.

Status: **Not a read-path defense.** Noted here so PR 4 does not re-litigate it.

### 6.4 Recall-audit trail (partial)
`packages/remnic-core/src/recall-audit.ts` defines `appendRecallAuditEntry`,
and the Openclaw host plugin (`src/index.ts:1690,1860,1902`) writes one
JSONL entry per `before_prompt_build` recall when
`cfg.recallTranscriptsEnabled` is true. Pruned daily by
`pruneRecallAuditEntries` (`src/index.ts:793`).

**Gap discovered while writing this document:** the MCP / HTTP / CLI access
layers (`access-mcp.ts`, `access-http.ts`, `access-service.recall()`) do **not**
call `appendRecallAuditEntry`. Recall invocations made directly through those
surfaces are not audited. That means an ADAM attacker driving the MCP surface
leaves no entry in `transcripts/…/*.jsonl`, only whatever coarse logging the
transport itself emits. PR 5 must wire the audit into the access layer before
it can do useful anomaly detection, or the data it operates on will be a
subset of the traffic that actually matters.

Status: **Partial — host-only.** Logged here as a concrete gap for PR 5.

### 6.5 Trust zones
`trust-zones.ts` tags memory by provenance class (`user_input`, `tool_output`,
`web_content`, `subagent_trace`, `system_memory`, `manual`) and placement
zone (`quarantine`, `working`, `trusted`). Primarily a poisoning defense, not
an extraction defense. Listed because the hardening surface overlaps and we
should not re-invent a parallel tagging scheme in PR 5.

### 6.6 MECE taxonomy
MECE (see `taxonomy/`) is a categorization/deduplication mechanism. It does
not reduce the information available through recall. **Evaluated and rejected
as an extraction-mitigation.** Mentioned here only because the issue explicitly
asked us to evaluate it.

### 6.7 Idempotency + schema validation
`access-schema.ts` validates inbound requests, and `access-idempotency.ts`
dedups repeated writes. Neither affects read-path extraction.

## 7. Gaps to measure in PR 2-5

Each gap is stated as something the harness should *probe* so PR 4/5 have a
concrete target.

1. **No per-connector, per-namespace read-query budget.** An MCP client can
   issue arbitrary numbers of `recall` / `memory_search` / `lcm_search` /
   `memory_entities_list` calls with no quota enforcement. PR 4 owns this.
2. **No entropy-pattern anomaly detection.** Nothing flags an attacker who
   spreads probes across many semantically similar queries to avoid QMD
   caches. PR 5 owns this. Requires fix for §6.4 first.
3. **No cumulative-disclosure cap per session.** Today a session can retrieve
   every memory it has read access to if it keeps asking. The threshold for
   "this session has now seen N% of the namespace" is un-enforced.
4. **Debug / introspection tools are not rate-limited and not ACL-gated by
   default.** `memory_qmd_debug`, `memory_graph_explain`, and
   `memory_last_recall` can be called freely by any authenticated client.
   These are side-channels for an ADAM attacker.
5. **Recall-audit is not written from the access layer.** See §6.4.
6. **`shared` namespace auto-promotion is an unbudgeted data-copy mechanism.**
   Any fact the attacker's own principal is allowed to produce in *any*
   namespace can, if eligible, be copied into `shared` and then read by a
   principal that should not have seen it. Threat model for this interaction
   needs explicit ASR measurement in PR 3.
7. **Response size is not capped.** `topK` defaults exist but can be
   overridden by the caller in `remnic.recall`. The harness should probe
   whether `topK: 10000` is honored and produce the current observed cap.

## 8. Success metrics

Proposed thresholds for the hardening milestones. Baseline (§PR 3) replaces
"unknown" in the left column once measured.

| Scenario | Baseline ASR | Target ASR after PR 4 | Target ASR after PR 5 |
|---|---|---|---|
| T2 same-namespace, 1000-query budget | unknown | < 50% | < 20% |
| T3 cross-namespace via forged `namespace` | expected ~0% (ACL blocks) | ~0% | ~0% |
| T3 cross-namespace via `shared` auto-promotion | unknown | unknown | < 10% |
| T1 unauthenticated | ~0% | ~0% | ~0% |

"ASR" = fraction of ground-truth facts in the fixture that the attacker
correctly recovers within the query budget. Exact definition finalized in
PR 2.

Secondary metrics:

- **Audit coverage**: fraction of access-layer recall calls that produce a
  `recall-audit` JSONL entry. Target: 100% by end of PR 5.
- **False-positive rate for anomaly detection**: flags raised on a benign
  workload (the existing eval harness fixtures in `@remnic/bench`). Target:
  < 1% per-session flag rate.
- **Query-budget configurability**: the budget introduced in PR 4 must be
  settable per-namespace and per-adapter, with a documented way to disable it
  for a principal marked as trusted.

## 9. Non-goals for this hardening work

- Encryption-at-rest of `memoryDir`. Separate initiative.
- Differential-privacy noise injection into recall responses. Out of scope for
  #565; potentially a future PR if PR 5's anomaly detection proves
  insufficient.
- Defending against a T2 attacker who exfiltrates memory by *writing* it back
  out via `observe` to a namespace they do control. This is an adjacent
  concern and will be tracked separately if/when it materializes in the
  harness.

## 10. Mitigation wiring status (PRs #649–#652)

PRs #638 and #639 introduced the `CrossNamespaceBudget` and
`AccessAuditAdapter` classes, but they were not wired into the actual
recall paths. PRs #649–#652 close this gap:

| PR | Slice | Change | Status |
|---|---|---|---|
| #649 | 6 | Wire `CrossNamespaceBudget` into `EngramAccessService.recall()` | **Open** |
| #650 | 7 | Wire `AccessAuditAdapter` into `EngramAccessService.recall()` | **Merged** |
| #651 | 8 | Add `security_mitigations` check to `remnic doctor` | **Merged** |
| #652 | 9 | Mitigation-aware ADAM target + mitigated baseline | **Open** |

As of this writing, slices 7 and 8 (PRs #650, #651) are merged to
`main`. Slices 6 and 9 (PRs #649, #652) are still open. Once #649
lands, `CrossNamespaceBudget` will be invoked inside
`EngramAccessService.recall()` alongside the already-wired
`AccessAuditAdapter`.

Both mitigations ship **disabled by default** (rule 48):
- `recallCrossNamespaceBudgetEnabled: false`
- `recallAuditAnomalyDetectionEnabled: false`

Operators enable them explicitly in config. The `remnic doctor` command
(wired in PR #651 via `summarizeSecurityMitigations` in
`operator-toolkit.ts`) warns when both are disabled.

### Mitigated baseline ASR (PR #652, pending merge)

The mitigated baseline (PR #652, still open as of this writing) re-runs
the T3 scenario with a cross-namespace budget of 30 queries per 60-second
window:

| Scenario | Queries | Budget limit | ASR |
|---|---:|---:|---|
| T3 unmitigated (baseline) | 200 | none | 0.0% (ACL enforced) |
| T3 mitigated (budget=30/60s) | 200 | 30/60s | 0.0% (ACL + budget) |

The T3 ASR was already 0.0% in the baseline because the synthetic
target enforces namespace ACLs. The budget mitigation provides defense
in depth — it would throttle a regression that accidentally disabled
the ACL check.

## 11. References

- Issue #565 (this work).
- ADAM — *Adaptive Data Extraction Attack*, arXiv:2604.09747 (Apr 2026).
- `docs/namespaces.md` — principal / namespace resolution model.
- `packages/remnic-core/src/namespaces/principal.ts` — ACL implementation.
- `packages/remnic-core/src/access-mcp.ts` — MCP surface enumeration.
- `packages/remnic-core/src/access-http.ts` — HTTP surface, token check, write
  rate-limit.
- `packages/remnic-core/src/access-service.ts` — read-path namespace gating
  (`resolveReadableNamespace`, `resolveRecallNamespace`).
- `packages/remnic-core/src/recall-audit.ts` — recall audit (host-wired only;
  see §6.4).
- `packages/remnic-core/src/trust-zones.ts` — provenance tagging (poisoning
  defense; not an extraction defense).
