/** * The estate-MCP proposal-queue client (DES-MEM-FACETED-001 §5.0). * * # Why this exists * * The proposal queue (`proposal.list` / `proposal.approve` / `proposal.reject`, estate #162) lives * ONLY on the estate MCP server — a JSON-RPC 2.0 process that speaks newline-delimited frames over * stdio. Crew already reaches estate two other ways, and NEITHER carries these tools: the * `wicked-estate` CLI (`projects/graph.ts` — indexing/query subcommands) and the wicked-core napi * addon (the run engine). So the queue needs its own small client, and this is it. * * # The operator posture — NON-`--readonly` * * This is the OPERATOR surface: it must list, approve, AND reject. `proposal.approve` / * `proposal.reject` mutate the active store / decide the queue, which the estate server REFUSES * under `--readonly` (a -32601 backstop — see `wicked-estate-mcp/tests/proposals.rs`). So the * server is spawned with NO `--readonly` flag. This is the deliberate inverse of the governed * WORKER grounding client (DES-GROUNDING-001), which allow-lists the estate MCP `--readonly` * precisely so a worker can never mutate the operator's stores. * * `WICKED_MEMORY_DB` is pinned to the store THIS DAEMON owns — `/mcp/memory.db` for an * isolated state home, and the operator global store (`${WICKED_HOME:-~/.wicked}/memory.db`, * matching the server's own default in `wicked-estate-mcp/src/main.rs`) for the default one. See * {@link resolveMemoryDbPath}: before F5 it was always the global store, so a throwaway daemon on * a free port listed — and could retire — the operator's real memories. * * # Framing (mirrors `wicked-estate-mcp/src/main.rs`) * * The server reads one JSON object per line on stdin and writes one response object per line on * stdout. A NOTIFICATION (no `id`) produces no output. The handshake is * `initialize` (id 1) → `notifications/initialized` → `tools/call` (id 2); the tool result is * wrapped in the MCP envelope `{ content: [{ type:'text', text: '' }], isError }`, and a * failure is a JSON-RPC `error` object. Spawn-per-call is fine for v1: the queue is an operator * action, not a hot path. */ import { type ChildProcess } from 'node:child_process'; /** The MCP protocol version crew declares in the handshake (the version the estate server speaks). */ export declare const ESTATE_MCP_PROTOCOL_VERSION = "2024-11-05"; /** The handshake + call must complete inside this budget or the child is killed and the call fails. * Generous because a cold `wicked-estate-mcp` may open a SQLite store on first use, but finite so * a wedged/absent binary fails loudly instead of hanging the request. */ export declare const DEFAULT_ESTATE_MCP_TIMEOUT_MS = 30000; /** A JSON-RPC error surfaced by the estate MCP (or a transport failure reaching it). `code` is the * JSON-RPC error code when the failure came back as a JSON-RPC `error` (e.g. -32602 invalid * params, -32601 method refused under --readonly, -32603 internal), absent for a transport fault. */ export declare class EstateMcpError extends Error { readonly code?: number; constructor(message: string, code?: number); } /** Injectable IO — tests substitute a fake MCP (a spawned node script) for the real `wicked-estate-mcp`. */ export interface EstateMcpIo { /** Spawn the server process (stdio piped). Defaults to `wicked-estate-mcp`, NON-`--readonly`, * with `WICKED_MEMORY_DB` pinned to this daemon's store ({@link resolveMemoryDbPath}). */ spawn?: () => ChildProcess; /** Overall deadline for the handshake + call round-trip. Defaults to {@link DEFAULT_ESTATE_MCP_TIMEOUT_MS}. */ timeoutMs?: number; } /** `wicked-estate-mcp`, overridable via `WICKED_ESTATE_MCP_EXE` (mirrors `WICKED_ESTATE_EXE` in * `projects/graph.ts` and `WICKED_CORE_EXE` in the adapter). */ export declare function estateMcpExe(env?: NodeJS.ProcessEnv): string; /** * The memory store this daemon reads and writes. * * ## F5 (ship-proof C7) — it FOLLOWS `--db`, like every other durable store * * This resolved from `homedir()` unconditionally, so a brand-new state home on a free port * (`--db /core.db`) showed **596 of the operator's real memories** on Steering → * Memories, and a Retire click there would have erased a row from their store. Every other * crew-side durable store resolves through `crewStateHome()` — the crew#330 / #351 / #353 escape, * a fourth time, and this one is a DATA-ISOLATION defect rather than stray disk use. * * The ladder, most specific first: * * 1. `WICKED_MEMORY_DB` — the explicit instruction wins, as it does at every other store's own * resolution site. * 2. An **isolated** state home (anything but the default) gets its OWN store, under the state * home: `/mcp/memory.db`. This is the fix — a test daemon, a rig, a second port * can no longer see, or retire, a row in the operator's store. `WICKED_HOME` does NOT outrank * it: that variable names the operator's GLOBAL store, not this daemon's. * * WHY UNDER `mcp/` AND NOT `/memory.db`. The top level of the state home is an * EXPLICIT registry (`tests/fixtures/state-home-subtrees.json`, mirrored in * `projects/state-home-registry.ts`) that **wicked-core embeds** to build the worker Read * fence, and `Core::launch_run` REFUSES at intake, naming the entry, when it finds a top-level * entry the registry does not classify. A new top-level `memory.db` would therefore refuse * EVERY governed run on the installed engine until the same entry shipped in a wicked-core * release — strictly worse than the defect, and precisely on the isolated state homes this * fixes. `mcp/` is already a registered, worker-denied subtree owned by crew and is where the * estate MCP's own daemon-side state belongs, so this needs no fence change and is safe on the * engine that is already released. Promoting it to `/memory.db` is a two-repo * change (one fence change per RC) and deliberately not smuggled in here. * 3. The **default** state home (`~/.wicked-crew`) keeps `${WICKED_HOME:-~/.wicked}/memory.db`. * Deliberate: that file is not crew's private store, it is the operator's global one — the * estate MCP's own default (`wicked-estate-mcp/src/main.rs`) and the path garden's `mem` * skills read. Moving the default daemon off it would SPLIT that user's memories from their * own CLI, which is the "must not lose sight of their memories" case. So isolation is keyed on * "is this the default daemon", and the default daemon's store neither moves nor needs copying. * * ## The migration case, and what was chosen (codex review of this PR, MEDIUM) * * Arm 3 covers the DEFAULT state home only. An operator who has always run one daemon on a CUSTOM * `--db` now lands on arm 2 and sees an empty store where 596 memories used to be. Nothing * distinguishes that operator from a throwaway test daemon on a free port — both are `--db * ` — so there is no signal to adopt the global store on, and adopting it is the defect. * * The choice: **isolate, and SAY SO.** {@link memoryStoreIsolationNotice} prints one line, once, * when this daemon's own store does not exist yet while the operator's global store does — naming * both paths and the one-line remedy (`WICKED_MEMORY_DB=`) for an operator who wants the * old behaviour. NOT a silent adoption, and not a read-only fallback either: this client is * deliberately non-`--readonly` because it must approve and reject proposals, so a read-only * degrade would break the queue it exists for. * * ## Recognising the default home * * Both sides are compared by REALPATH where the directory exists, falling back to `resolve`: * `--db ./.wicked-crew/core.db`, a symlinked home, a differently-cased spelling on a * case-insensitive filesystem and a Windows junction all name the default daemon, and a lexical * compare would have called them isolated (codex review, MEDIUM). */ /** Where {@link resolveMemoryDb} got its answer — `explicit` is the operator's own instruction. */ export type MemoryDbSource = 'explicit' | 'state-home' | 'global'; /** * Do these two paths name the SAME directory? By realpath where they exist, lexically where they * do not. Exported because it is the load-bearing half of "is this the default daemon": a lexical * compare called a symlinked home, a differently-cased spelling on a case-insensitive filesystem * and a Windows junction ISOLATED, and handed the daemon an empty store (codex review, MEDIUM). */ export declare function sameDirectory(a: string, b: string): boolean; /** {@link resolveMemoryDbPath} with the reason, for the callers that need to know which arm won. */ export declare function resolveMemoryDb(env?: NodeJS.ProcessEnv): { path: string; source: MemoryDbSource; }; /** * The one line an isolated daemon prints when its OWN memory store does not exist yet while the * operator's global store does: the migration case said out loud instead of silently showing an * empty Memories page (codex review of this PR, MEDIUM). `null` whenever there is nothing to say — * the explicit variable was set, this is the default daemon, this daemon's store already exists, * or there is no global store to have been using. */ export declare function memoryStoreIsolationNotice(env?: NodeJS.ProcessEnv): string | null; /** crew#740: the store this daemon's memory surface reads and writes, on the wire * (`/diagnostics.memoryStore`, `GET /memory`'s `store`) — the isolation notice was stderr-only. * Paths are for the local operator; nothing writes them to GitHub-visible text. */ export declare function memoryStoreInfo(env?: NodeJS.ProcessEnv): { path: string; source: MemoryDbSource; notice: string | null; }; export declare function resolveMemoryDbPath(env?: NodeJS.ProcessEnv): string; /** * Call one estate MCP tool by name and return its unwrapped result. * * Generic over the tool name — the proposal queue (`proposal.*`) and the memory-management surface * (`memory.recall` / `memory.coverage` / `memory.erase`) both go through here — so a single * spawn-per-call `wicked-estate-mcp` client (NON-`--readonly`, `WICKED_MEMORY_DB` pinned) serves * every operator-facing estate tool. * * Spawns the server, performs the handshake, issues the `tools/call`, reads the id-2 response * (line-buffered), unwraps the MCP envelope, then closes stdin and kills the child. Throws * {@link EstateMcpError} on a JSON-RPC error, a malformed response, an early process exit, a spawn * failure, or the timeout. */ export declare function callEstateTool(tool: string, args: Record, io?: EstateMcpIo): Promise; /** * {@link callEstateTool} without the unwrap: the server's whole `id:2` JSON-RPC response, a * `result` (the MCP envelope, every content block kept) or an `error`. The estate relay * (`mcp/estate-relay.ts`) hands a worker the envelope as the server wrote it, diagnostics block * included, the way garden's shim reads it from a local server. Throws {@link EstateMcpError} on a * transport fault, a failed handshake, an early exit or the timeout; a JSON-RPC `error` answer is * returned, not thrown. */ export declare function callEstateToolRaw(tool: string, args: Record, io?: EstateMcpIo): Promise>; //# sourceMappingURL=estate-mcp-client.d.ts.map