import type { CortexStore } from '../db/store.js'; /** * Refuse a subagent retiring, editing or deleting memory that belongs to an * earlier session (FR-19, Story 5.3, AC #3). * * ## Why this lives on `PreToolUse` and not in the MCP server * * Cortex's MCP server cannot tell a subagent's call from its parent's. The * twelve `ensureScopedSession` call sites in `src/transports/mcp.ts` pass only * `(store, cwd)`, and MCP carries no caller id — measured, not assumed. The * `PreToolUse` hook payload DOES carry `agent_id` for a subagent's tool call * (probed live: an `mcp__cortex__cortex_note` call from a subagent arrives with * it, while the parent's own calls arrive without), and `PreToolUse` is the one * event that can deny. So enforcement is possible here and nowhere else. * * ## "Its own session" means its own session TREE * * Ruling (a), ShuromiU, 2026-08-06. No note is ever stamped with a subagent's * session id: both MCP write paths resolve through `ensureSession(store, cwd)`, * which carries no identity and lands on the PRIMARY. Comparing a note's * `session_id` against the calling subagent's own child id would therefore deny * every subagent memory operation — including on a note that same subagent * wrote seconds earlier — which is the fail-closed outcome the AC forbids. * `getSessionTreeIds` is the right set: this conversation, primary and * children. * * ## Three routes retire other people's decisions, not one * * `insertNote`'s auto-supersede filters by neither session nor scope, so: * `cortex_resolve` on a named note (the route AC #3 names), `cortex_note` * itself (a `decision` retires every other active `decision` on that subject, * anywhere in the store), and `cortex_resolve` WITH `replacement` (it calls * `insertNote`, so a named-target check alone passes it straight through). * Ruling (b) adds a fourth: the shell reaches the same memory through * `note-resolve`, `edit-memory` and `delete-memory`, and that delete is more * destructive than anything the AC names. * * ## Fail OPEN, everywhere, without exception * * Any inability to establish that a target lies outside the tree allows the * call: no current session, an unknown agent, an unresolvable target, a * malformed payload, a thrown store. A blocking hook that errs toward blocking * stops the user's own work, which is a worse failure than the one it prevents. * The residual is stated rather than wished away — a shell command whose target * is built at runtime (`cortex delete-memory "$ID"`) resolves to nothing here * and is allowed. * * ## What is deliberately NOT guarded * * Contest marking. `insertNote` also flags a prior note `[contested]` when the * incoming note contradicts it, and that flag lands on notes outside the tree * the same way a supersede does. It is excluded because it is not a retirement: * both sides stay active and visible, and the marker is the product working — * FR-1 exists to surface disagreement, and a subagent noticing one is a good * outcome. Denying it would turn contradiction detection off for subagents * entirely. * * AD-7 companion: refunds are scoped to `PostToolUse` substitution and * explicitly not to `PreToolUse` deny. This is a different capability on a * different path and books no refund; the two do not contradict. */ /** * The two memory-writing MCP tools. Nothing else — not the read-only ones. */ export declare const MEMORY_GUARD_MCP_TOOLS: readonly ["mcp__cortex__cortex_note", "mcp__cortex__cortex_resolve"]; /** * Every tool name that can run a shell command. * * `Bash` alone was not enough, and the gap was found by a reviewer that WAS a * subagent on this machine: the host also exposes a `PowerShell` tool, and a * subagent with it can run `cortex delete-memory --yes` entirely outside a * `Bash`-only matcher. Cortex already knew other shell tool names exist — * `postToolUse` accepts `shell_command` and `*.shell_command` alongside `Bash` * — and the guard did not. A route the guard never sees is worse than one it * declines to act on, because nothing reports the difference. */ export declare const SHELL_TOOL_NAMES: readonly ["Bash", "PowerShell", "shell_command"]; /** Every tool this guard inspects. */ export declare const MEMORY_GUARD_TOOLS: readonly ["mcp__cortex__cortex_note", "mcp__cortex__cortex_resolve", "Bash", "PowerShell", "shell_command"]; /** The `PreToolUse` matcher `install` writes and `doctor` checks. */ export declare const MEMORY_GUARD_MATCHER: string; /** Whether a tool name runs a shell command, for the routing switch. */ export declare function isShellTool(toolName: string): boolean; /** * The shell subcommands that reach memory. * * Duplicated as a literal `case` in `hooks/claude/cortex-subagent.sh`, which is * the point: N-4 forbids spawning Node per tool call, and `PreToolUse` on * `Bash` fires for every command the agent runs. The shell pre-filter decides * without Node, and a test asserts the two lists agree so the cheap check and * the real one cannot drift apart. */ export declare const SHELL_MEMORY_COMMANDS: readonly ["note-resolve", "edit-memory", "delete-memory"]; /** The pure-text pre-filter, mirrored in the hook script. */ export declare function shellCommandTargetsMemory(command: string): boolean; export type GuardedAction = 'retire' | 'edit' | 'delete'; export interface GuardedTarget { /** The id the call named, or the note id behind it. */ id: string; /** Plain-language identification for the denial reason. */ label: string; } export interface MemoryGuardDenial { action: GuardedAction; targets: GuardedTarget[]; reason: string; } export interface MemoryGuardRequest { toolName: string; toolInput: Record; /** Present only for a subagent. The parent is untouched by this guard. */ agentId: string; } /** * Decide whether to deny. `undefined` is allow-and-say-nothing, which is both * the ordinary outcome and every failure outcome. */ export declare function evaluateMemoryGuard(store: CortexStore, request: MemoryGuardRequest): MemoryGuardDenial | undefined; //# sourceMappingURL=memory-guard.d.ts.map