@byok-sdk/client 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/daemon/control-protocol.ts","../../src/bin/control-client.ts","../../src/bin/approval-mcp-server.ts","../../src/bin/byok-approval-mcp.ts"],"names":["fs","fsConstants","errorMessage"],"mappings":"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","file":"byok-approval-mcp.js","sourcesContent":["import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport path from 'node:path';\nimport type { TaskState } from '@byok-sdk/protocol';\nimport type { ApprovalDecision, PendingApproval } from './approvals';\nimport type { StorageCategory } from './journal/journal';\nimport type { StoragePressureState } from './journal/storage-policy';\nimport type { OperationalHealthSnapshot } from './operational-health';\nimport type { LocalAgentReleaseIdentity } from '../release-identity';\nimport type { McpToolsetConfig, McpToolsetRegistryStatus } from '../types';\n\n/**\n * M4 Phase 2: shared local-IPC contract between the daemon's control server\n * (`control-server.ts`) and the CLI's control client (`bin/control-client.ts`)\n * — frame shapes, endpoint path/pipe-name derivation, and the HMAC handshake\n * math. Both sides import from here so the two can never independently drift\n * (e.g. a mismatched HMAC label string, or a socket path computed two\n * slightly different ways).\n *\n * Transport: NDJSON (one JSON object per line) over a Unix domain socket\n * (darwin/linux) or a Windows named pipe — both addressed by the same\n * path-like string via Node's `net` module, so neither `control-server.ts`\n * nor `bin/control-client.ts` needs to special-case the transport itself,\n * only the path/pipe-name derivation below.\n */\n\nexport const CONTROL_PROTOCOL_VERSION = 1;\n\n/** Handshake must complete within this long, on both sides — see each side's own timer. */\nexport const HANDSHAKE_TIMEOUT_MS = 3000;\n\n// ---------------------------------------------------------------------------\n// Endpoint path / pipe name derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Conservative soft limit for a Unix domain socket path, in UTF-8 bytes.\n * macOS's `sockaddr_un.sun_path` is 104 bytes total (including the NUL\n * terminator and any prefix the kernel reserves), so anything comfortably\n * under 104 avoids `ENAMETOOLONG` at `bind()`/`connect()` time on the\n * tightest common platform.\n */\nconst UNIX_SOCKET_PATH_SOFT_LIMIT = 100;\n\n/**\n * The one fixed root the long-path fallback below binds under. Deliberately\n * NOT `os.tmpdir()`, which reads `TMPDIR`/`TMP`/`TEMP` — an endpoint address\n * that varies with the environment is not an endpoint both sides can find,\n * and a caller may have pointed `TMPDIR` INSIDE the very tree that made the\n * natural path too long, where `os.tmpdir()` yields an address LONGER than\n * the one being escaped (`bind()` then fails `EINVAL` and the daemon degrades\n * to no control socket at all). A literal `/tmp` is POSIX-guaranteed,\n * environment-independent, and short enough that this candidate always fits\n * the budget above. Same fix, same reasons as `daemon-owner.ts`'s\n * `STORE_MUTEX_FALLBACK_ROOT`; kept as its own constant because the two\n * derivations differ in name and ownership contract and must stay\n * independently readable.\n */\nconst CONTROL_SOCKET_FALLBACK_ROOT = '/tmp';\n\nfunction shortHash(input: string): string {\n return createHash('sha256').update(input, 'utf8').digest('hex').slice(0, 16);\n}\n\n/**\n * The Unix domain socket path for a daemon rooted at `storeDir`. Prefers\n * `<storeDir>/control.sock` (keeps every one of this daemon's local state\n * files under one directory, which is already created+chmod'd 0700 by the\n * time this matters — see `control-server.ts`'s `startControlServer`);\n * falls back, whenever the natural path would risk exceeding {@link\n * UNIX_SOCKET_PATH_SOFT_LIMIT}, to a short, deterministic path nested under\n * a PER-DAEMON PRIVATE subdirectory of {@link CONTROL_SOCKET_FALLBACK_ROOT}\n * — derived from a hash of `storeDir` alone, so both the daemon and any CLI\n * invocation pointed at the same `storeDir` independently compute the\n * identical fallback path.\n *\n * That root was `os.tmpdir()` until it was proven to break both halves of\n * that sentence: it reads `TMPDIR`, so the daemon (under a service manager)\n * and the CLI (in an operator shell) derived DIFFERENT addresses for one\n * store, and under a `TMPDIR` nested in the same long tree the fallback came\n * out LONGER than the path it escaped — `bind()` `EINVAL`, and the daemon\n * ran on with no control socket at all.\n *\n * Nested one level deep (rather than a bare `<hash>.sock` file directly in\n * that shared, world-traversable root) specifically so\n * `control-server.ts`'s `bindControlEndpoint` can create+chmod that\n * subdirectory 0700 BEFORE ever binding inside it — the directory's own\n * mode gates traversal into it regardless of the socket file's own\n * (briefly default-permissioned, until the post-bind `chmod`) mode, closing\n * what would otherwise be a real window for another user on the same\n * machine to reach a socket living directly in a shared tmpdir.\n */\nexport function controlSocketPath(storeDir: string): string {\n const candidate = path.join(storeDir, 'control.sock');\n if (Buffer.byteLength(candidate, 'utf8') <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;\n return path.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, 'sock');\n}\n\n/**\n * The Windows named pipe name for a daemon identified by `productId` +\n * (`path.resolve`-normalized) `storeDir`. Named pipes have no filesystem\n * path (no stale-file cleanup concern the way Unix sockets have — see\n * `control-server.ts`), but DO share one flat namespace across the whole\n * machine, so the name must be scoped to this exact daemon instance: two\n * different products, or two different store directories (e.g. two agents\n * of the same product — see `templates/service/README.md`'s \"running\n * multiple agents\" section), must never collide. `storeDir` is resolved\n * before hashing so a trivial path-form difference (trailing slash, etc.)\n * between the two sides can't split the name.\n *\n * NOT keyed by the OS user: a WinSW-installed service runs the daemon under\n * the Windows service account (e.g. `SYSTEM`) while the operator CLI runs\n * as the interactive user, so both sides must derive the identical name\n * from the same `storeDir` alone. Impostor servers are defeated by the\n * mutual HMAC handshake below, not by pipe-name secrecy — keying by user\n * was security theater that broke the service-account topology.\n */\nexport function controlPipeName(productId: string, storeDir: string): string {\n const id = shortHash(`${productId}|${path.resolve(storeDir)}`);\n return `\\\\\\\\.\\\\pipe\\\\byok-${id}`;\n}\n\n/**\n * Dispatches to {@link controlPipeName} on `win32`, {@link controlSocketPath}\n * everywhere else. `platform` defaults to `process.platform`; overridable\n * for tests exercising a specific platform's branch on any host (mirrors\n * `lifecycle/create-service-lifecycle.ts`'s identical `platform` override —\n * the REAL win32 named-pipe semantics can only be proven on actual Windows,\n * which CI's `ipc-smoke` job does; this override just makes the PATH-CHOICE\n * logic itself testable everywhere).\n */\nexport function controlEndpointPath(productId: string, storeDir: string, platform: NodeJS.Platform = process.platform): string {\n return platform === 'win32' ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);\n}\n\n/** Where the daemon writes its per-session control-auth token (see the handshake section below). Always a real file, even on Windows (pipes have no path of their own to piggyback secrets on). */\nexport function controlTokenPath(storeDir: string): string {\n return path.join(storeDir, 'control.token');\n}\n\n// ---------------------------------------------------------------------------\n// Handshake: mutual HMAC proof, token never sent over the wire\n// ---------------------------------------------------------------------------\n\nconst SERVER_PROOF_LABEL = 'byok-control-server|';\nconst CLIENT_AUTH_LABEL = 'byok-control-client|';\n\nexport function randomNonceHex(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction hmacHex(token: string, message: string): string {\n return createHmac('sha256', token).update(message, 'utf8').digest('hex');\n}\n\n/** What the server proves to the client: it holds `token`, bound to the client's own nonce so a captured proof can't be replayed against a different handshake. */\nexport function computeServerProof(token: string, clientNonce: string): string {\n return hmacHex(token, SERVER_PROOF_LABEL + clientNonce);\n}\n\n/** What the client proves to the server, symmetrically, bound to the server's nonce. */\nexport function computeClientAuth(token: string, serverNonce: string): string {\n return hmacHex(token, CLIENT_AUTH_LABEL + serverNonce);\n}\n\n/** Constant-time hex-string comparison (`crypto.timingSafeEqual` requires equal-length buffers; a length mismatch is itself a safe, immediate \"not equal\" — no early-exit on content). */\nexport function timingSafeEqualHex(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'hex');\n const bufB = Buffer.from(b, 'hex');\n if (bufA.length !== bufB.length) return false;\n return timingSafeEqual(bufA, bufB);\n}\n\n// ---------------------------------------------------------------------------\n// Handshake frame shapes + parsers\n// ---------------------------------------------------------------------------\n\nexport interface ClientHello {\n v: 1;\n hello: 'client';\n nonce: string;\n}\nexport interface ServerHello {\n v: 1;\n hello: 'server';\n proof: string;\n nonce: string;\n}\nexport interface ClientAuth {\n v: 1;\n auth: string;\n}\nexport interface ServerReady {\n v: 1;\n ready: true;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport function parseClientHello(value: unknown): ClientHello | undefined {\n if (!isRecord(value)) return undefined;\n if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== 'client' || typeof value.nonce !== 'string') return undefined;\n return { v: CONTROL_PROTOCOL_VERSION, hello: 'client', nonce: value.nonce };\n}\n\nexport function parseServerHello(value: unknown): ServerHello | undefined {\n if (!isRecord(value)) return undefined;\n if (\n value.v !== CONTROL_PROTOCOL_VERSION ||\n value.hello !== 'server' ||\n typeof value.proof !== 'string' ||\n typeof value.nonce !== 'string'\n ) {\n return undefined;\n }\n return { v: CONTROL_PROTOCOL_VERSION, hello: 'server', proof: value.proof, nonce: value.nonce };\n}\n\nexport function parseClientAuth(value: unknown): ClientAuth | undefined {\n if (!isRecord(value)) return undefined;\n if (value.v !== CONTROL_PROTOCOL_VERSION || typeof value.auth !== 'string') return undefined;\n return { v: CONTROL_PROTOCOL_VERSION, auth: value.auth };\n}\n\nexport function parseServerReady(value: unknown): ServerReady | undefined {\n if (!isRecord(value)) return undefined;\n if (value.v !== CONTROL_PROTOCOL_VERSION || value.ready !== true) return undefined;\n return { v: CONTROL_PROTOCOL_VERSION, ready: true };\n}\n\n// ---------------------------------------------------------------------------\n// RPC frame shapes (post-handshake)\n// ---------------------------------------------------------------------------\n\nexport interface RawControlRequest {\n /** Not narrowed to `1` here on purpose — an unexpected value is a `bad_version` RESPONSE, not a parse failure; see `control-server.ts`. */\n v: unknown;\n id: string;\n method: string;\n params?: unknown;\n}\n\n/** Loose shape check for an incoming request line: only `id`/`method` need to be well-formed for the server to be able to respond at all (including a `bad_version`/`unknown_method` response) — `v` is deliberately passed through unvalidated. */\nexport function parseRawControlRequest(value: unknown): RawControlRequest | undefined {\n if (!isRecord(value)) return undefined;\n if (typeof value.id !== 'string' || typeof value.method !== 'string') return undefined;\n return { v: value.v, id: value.id, method: value.method, params: value.params };\n}\n\nexport interface ControlErrorShape {\n code: string;\n message: string;\n}\n\nexport interface ControlResponseOk {\n v: 1;\n id: string;\n ok: true;\n result?: unknown;\n /** Present (and `true`) only on the final frame of a streaming method — see `control-server.ts`'s dispatch. */\n done?: true;\n}\n\nexport interface ControlResponseErr {\n v: 1;\n id: string;\n ok: false;\n error: ControlErrorShape;\n}\n\nexport type ControlResponse = ControlResponseOk | ControlResponseErr;\n\nexport interface ControlEventFrame {\n v: 1;\n id: string;\n event: unknown;\n}\n\nexport function encodeFrame(frame: unknown): string {\n return `${JSON.stringify(frame)}\\n`;\n}\n\n/** Thrown by a method handler to control the wire error `{code, message}` a caller sees — anything else thrown surfaces as a generic `internal_error`. See `control-server.ts`'s dispatch and `bin/control-client.ts`'s `request()` (which re-throws this same class on the client side). */\nexport class ControlError extends Error {\n constructor(\n public readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = 'ControlError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// NDJSON line framing\n// ---------------------------------------------------------------------------\n\n/**\n * Bound on a single NDJSON line's byte length. Every real frame this\n * protocol ever sends (handshake frames, requests/responses/events) is well\n * under this — it exists purely as a defensive cap against a misbehaving or\n * hostile peer streaming an unterminated line forever to grow `pending`\n * without bound. Exceeding it is a fail-closed condition: {@link\n * NdjsonLineReader.push} throws, and every caller (`control-server.ts`,\n * `bin/control-client.ts`) destroys the connection on that throw.\n */\nexport const MAX_LINE_BYTES = 64 * 1024;\n\n/**\n * Buffers raw socket bytes and yields complete lines. Splits on the raw byte\n * `0x0a` BEFORE any UTF-8 decoding (mirrors `bin/audit-log.ts`'s\n * `followAuditLog`) — `0x0A` can only ever appear as an actual newline in\n * valid UTF-8, so this never risks decoding a multi-byte character that\n * happened to straddle a chunk boundary.\n */\nexport class NdjsonLineReader {\n private pending: Buffer = Buffer.alloc(0);\n\n /** @throws if the still-unterminated remainder exceeds {@link MAX_LINE_BYTES} — see that constant's own doc comment. */\n push(chunk: Buffer): string[] {\n this.pending = this.pending.length > 0 ? Buffer.concat([this.pending, chunk]) : chunk;\n const lines: string[] = [];\n let newlineIndex: number;\n // eslint-disable-next-line no-cond-assign\n while ((newlineIndex = this.pending.indexOf(0x0a)) !== -1) {\n const line = this.pending.subarray(0, newlineIndex).toString('utf8');\n this.pending = this.pending.subarray(newlineIndex + 1);\n if (line.length > 0) lines.push(line);\n }\n if (this.pending.length > MAX_LINE_BYTES) {\n throw new Error(`NDJSON line exceeded ${MAX_LINE_BYTES} bytes without a terminating newline`);\n }\n return lines;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Method contracts (Phase 2 surface) — shared by create-daemon.ts's control\n// method registry and the CLI commands that call them.\n// ---------------------------------------------------------------------------\n\nexport interface ControlActiveTask {\n taskId: string;\n state: TaskState;\n}\n\n/**\n * M4 Phase 4 (part B.3, observability): a cheap per-active-task queue-depth\n * watermark for the `status` result. The IDEAL metric here would be each\n * runtime adapter's own event-queue depth (`util/async-queue.ts`'s\n * `AsyncQueue`) — but that queue lives inside each adapter's concrete\n * `Session` implementation, and `Session.events` (`types.ts`) is typed only\n * as a plain `AsyncIterable<AgentEvent>`, which has no queryable backlog\n * size; reaching it would mean adding a new method to the `Session`\n * interface AND implementing it in all three bundled adapters\n * (pi/claude/codex), which is out of scope for this pass. This instead\n * reflects two things `TaskRunner` already cheaply knows about the SAME\n * task without any new plumbing: how much progress is buffered locally\n * (not yet flushed as a `task.progress` batch), and how many out-of-band\n * approval requests are currently in flight for it. See\n * `task-runner.ts`'s `getQueueWatermarks` for how each field is computed.\n */\nexport interface TaskQueueWatermark {\n taskId: string;\n /** Events buffered in this task's `ProgressBatcher`, not yet flushed as a `task.progress` batch. */\n progressBatcherPending: number;\n /** Approval requests currently in flight for this task: 1 if one is actively dispatched (registered + `task.await_approval` sent) plus however many more are queued behind it (M4 Phase 4 fold-in — see `TaskRunner.requestApproval`). */\n pendingApprovals: number;\n}\n\n/**\n * S3b (L-003): local storage usage and pressure, as the `status` method\n * reports them (architecture §12.7.2.1).\n *\n * Named `storage*` throughout, NOT `watermark*`: {@link TaskQueueWatermark}\n * above is a per-task progress-buffer depth and has nothing to do with disk.\n * Two unrelated concepts sharing a name on one status result is how an\n * operator reads the wrong number during an incident.\n *\n * Present only when a daemon actually runs a storage policy\n * (`DaemonConfig.hostedJournal.storagePolicy`). Absent means \"not measured\",\n * which is a different statement from \"measured, and fine\" — so it is an\n * absent field rather than a zeroed one.\n */\nexport interface ControlStorageStatus {\n /** §12.7.2.1's four states. `hard-pressure` declines new offers; `emergency` refuses to ack at all. */\n pressureState: StoragePressureState;\n /** `maxStoreBytes` — the budget `usedBytes` is measured against. */\n budgetBytes: number;\n /** Total across every category below, as of `measuredAt`. */\n usedBytes: number;\n /** Bytes available to this daemon on the store's filesystem — the free-space axis of the watermark, independent of the budget. */\n freeBytes: number;\n measuredAt: string;\n /** The five §12.7.2.1 categories, always reported separately — a single total cannot drive a category-scoped cleanup order or a category-scoped never-delete list. */\n categories: ControlStorageCategoryUsage[];\n /** The most recent bounded WAL checkpoint + incremental vacuum, if one has run in this daemon's lifetime. */\n lastCompaction?: ControlStorageCompaction;\n}\n\nexport interface ControlStorageCategoryUsage {\n category: StorageCategory;\n bytes: number;\n /** `true` when this is a host-reported or sampled figure rather than one measured off the filesystem. */\n approximate: boolean;\n}\n\nexport interface ControlStorageCompaction {\n checkpointed: boolean;\n walFramesRemaining: number;\n pagesVacuumed: number;\n durationMs: number;\n at: string;\n}\n\n/** Result shape for the `status` method — see `create-daemon.ts`'s control-method wiring for how each field is sourced, and `bin/format.ts`'s `formatLiveStatusLines` for how the CLI renders it. */\nexport interface ControlStatusResult {\n /** Process-immutable Local Agent application release; absent only for an older control peer. */\n localAgentRelease?: Readonly<LocalAgentReleaseIdentity>;\n pid: number;\n uptimeMs: number;\n paired: boolean;\n deviceId?: string;\n /** The connection state machine's own current value (`ws-transport.ts`'s `ConnectionState`) — e.g. `'open'`, `'degraded'` (long-poll fallback), `'revoked'`, `'closed'`, `'connecting'`. */\n transport: string;\n activeTasks: ControlActiveTask[];\n runtimeIds: string[];\n /** M4 Phase 4 (part B.3): per-active-task queue watermarks — see {@link TaskQueueWatermark}. */\n queueWatermarks: TaskQueueWatermark[];\n /**\n * Finding F4 (cross-model adversarial review): the actual pending\n * approvals currently dispatched — the SAME entries `approvals.list`\n * returns (`ApprovalRegistry.list()`), surfaced here too so a single\n * `status` call can show an operator every `approvalId` they'd need to\n * `approve`/`reject`, without a second control-socket round trip. This is\n * `approvalsPending`'s own source list (`approvalsPending ===\n * approvals.length`, always).\n */\n approvals: PendingApproval[];\n /** M4 Phase 4 (part B.3): total approvals currently DISPATCHED (registered) across the whole daemon — the same count `approvals.list` returns, surfaced here too for a one-call status view. */\n approvalsPending: number;\n /** S3b (L-003): local storage usage + pressure — see {@link ControlStorageStatus}. Absent unless a storage policy is configured. */\n storage?: ControlStorageStatus;\n /** Local lifecycle/retry budget. This is not the transport state above. */\n operationalHealth: OperationalHealthSnapshot;\n /** Redacted content-addressed status from the daemon's single local registry. */\n toolsets: McpToolsetRegistryStatus;\n}\n\nexport interface ToolsetsReloadParams {\n expectedRevision: string;\n mcpToolsets: Record<string, McpToolsetConfig>;\n}\n\n/** Maximum opaque pairing-code payload accepted over local control IPC. */\nexport const ENROLLMENT_PAIRING_CODE_MAX_BYTES = 1024;\n\nexport interface EnrollmentPairParams {\n pairingCode: string;\n}\n\n/**\n * Exact shape gate for service-identity pairing. The code is opaque server\n * authority: this only bounds transport bytes and rejects unknown fields; it\n * never parses, normalizes or logs the code.\n */\nexport function parseEnrollmentPairParams(value: unknown): EnrollmentPairParams | undefined {\n if (!isRecord(value) || Object.keys(value).some((key) => key !== 'pairingCode')) return undefined;\n if (\n typeof value.pairingCode !== 'string' ||\n value.pairingCode.length === 0 ||\n Buffer.byteLength(value.pairingCode, 'utf8') > ENROLLMENT_PAIRING_CODE_MAX_BYTES\n ) {\n return undefined;\n }\n return { pairingCode: value.pairingCode };\n}\n\n/** Shape-only parser; executable definition validation remains registry-owned. */\nexport function parseToolsetsReloadParams(value: unknown): ToolsetsReloadParams | undefined {\n if (!isRecord(value) || Object.keys(value).some((key) => key !== 'expectedRevision' && key !== 'mcpToolsets')) {\n return undefined;\n }\n if (typeof value.expectedRevision !== 'string' || !isRecord(value.mcpToolsets) || Array.isArray(value.mcpToolsets)) {\n return undefined;\n }\n return {\n expectedRevision: value.expectedRevision,\n mcpToolsets: value.mcpToolsets as Record<string, McpToolsetConfig>,\n };\n}\n\nexport interface ApprovalsListResult {\n approvals: PendingApproval[];\n}\n\nexport type { ApprovalDecision, PendingApproval } from './approvals';\n\nexport interface ApprovalsResolveParams {\n approvalId: string;\n decision: ApprovalDecision;\n reason?: string;\n}\n\nexport function parseApprovalsResolveParams(value: unknown): ApprovalsResolveParams | undefined {\n if (!isRecord(value)) return undefined;\n if (typeof value.approvalId !== 'string') return undefined;\n if (value.decision !== 'approve' && value.decision !== 'reject') return undefined;\n if (value.reason !== undefined && typeof value.reason !== 'string') return undefined;\n return { approvalId: value.approvalId, decision: value.decision, reason: value.reason };\n}\n\n/**\n * M4 Phase 3: the control method `byok-approval-mcp` (`bin/byok-approval-mcp.ts`)\n * calls FROM a claude-spawned MCP-server child process — a genuinely\n * different OS process from the daemon, reachable only over this same\n * control socket (see `../types.ts`'s `ApprovalChannel` doc comment for the\n * full why). `taskId` correlates the request to an active task;\n * `summary` is a short, human-readable description of the gated action\n * (carried verbatim into the wire `task.await_approval.summary`).\n */\nexport interface ApprovalsRequestParams {\n taskId: string;\n summary: string;\n}\n\nexport function parseApprovalsRequestParams(value: unknown): ApprovalsRequestParams | undefined {\n if (!isRecord(value)) return undefined;\n if (typeof value.taskId !== 'string' || value.taskId.length === 0) return undefined;\n if (typeof value.summary !== 'string') return undefined;\n return { taskId: value.taskId, summary: value.summary };\n}\n\n/** Result of `approvals.request` — the outcome `byok-approval-mcp` translates into its own MCP `allow`/`deny` answer. */\nexport interface ApprovalsRequestResult {\n approved: boolean;\n reason?: string;\n}\n\n// ---------------------------------------------------------------------------\n// `assertion.issue` (plan device-assertion-broker)\n// ---------------------------------------------------------------------------\n\n/**\n * Params for `assertion.issue`: a sibling local process (the host's own CLI,\n * installed alongside this daemon) asking the daemon to mint one short-lived,\n * audience-scoped device assertion with the paired device key. See\n * `@byok-sdk/core`'s `device-assertion.ts` for the envelope, and\n * `create-daemon.ts`'s handler for the six fail-closed gates every call passes\n * through in a fixed order.\n *\n * One field, and nothing else. In particular there is deliberately no caller\n * identity, no requested TTL, and no requested claim set: every process running\n * as this UID can reach the control socket, so anything a caller \"tells\" the\n * daemon about itself is decoration, and a caller-chosen lifetime is just the\n * TTL ceiling handed to whoever asks.\n */\nexport interface AssertionIssueParams {\n audience: string;\n}\n\n/**\n * Bound on the `audience` a caller may send, in UTF-8 bytes — mirrors\n * `@byok-sdk/core`'s `DEVICE_ASSERTION_AUDIENCE_MAX_BYTES`. Restated here rather\n * than imported so the WIRE bound is checked before anything reaches the claim\n * schema: this is the frame-level shape gate, and it must reject an oversized\n * value without that value ever reaching a signer or an audit line.\n */\nexport const ASSERTION_AUDIENCE_MAX_BYTES = 256;\n\n/**\n * Strict shape check. `undefined` means `bad_request` — a distinct gate from\n * `audience_denied` (see `create-daemon.ts`): \"you sent something that is not a\n * request\" and \"you asked for an audience you may not have\" are different\n * facts, and collapsing them would let a caller probe the allowlist by\n * malforming requests.\n *\n * Rejects an unknown key outright rather than ignoring it. A tolerated extra\n * field is how a future caller comes to believe it can influence the claim set.\n */\nexport function parseAssertionIssueParams(value: unknown): AssertionIssueParams | undefined {\n if (!isRecord(value)) return undefined;\n const keys = Object.keys(value);\n if (keys.length !== 1 || keys[0] !== 'audience') return undefined;\n const { audience } = value;\n if (typeof audience !== 'string' || audience.length === 0) return undefined;\n if (Buffer.byteLength(audience, 'utf8') > ASSERTION_AUDIENCE_MAX_BYTES) return undefined;\n return { audience };\n}\n\n/**\n * Result of `assertion.issue`. `assertion` is the full signing envelope\n * (`DeviceAssertionEnvelopeV1`), carried as an opaque JSON value on this wire —\n * the caller hands it to the host's cloud, which parses and verifies it with\n * core's own `verifyDeviceAssertion`. `expiresAt` is repeated outside the\n * envelope purely so a caller can schedule a refresh without parsing claims it\n * has no business interpreting.\n */\nexport interface AssertionIssueResult {\n assertion: unknown;\n expiresAt: string;\n}\n\n/**\n * The six `ControlError` codes `assertion.issue` can answer with, in the exact\n * order the handler checks them (`create-daemon.ts`). Each one is a distinct\n * refusal with a distinct cause; none of them ever signs anything.\n *\n * - `assertion_disabled` — this daemon has no `deviceAssertion` config, or an\n * empty audience allowlist. The feature is OFF by default.\n * - `bad_request` — params were not `{audience: string}` within the byte bound.\n * - `audience_denied` — the audience is not in the configured allowlist. The\n * message never echoes the allowlist: a refusal must not be an enumeration\n * oracle.\n * - `shutting_down` — a shutdown has been requested. Closes the window between\n * the shutdown RPC being acknowledged and the control socket actually\n * closing, during which a device that is being unpaired could otherwise still\n * mint credentials.\n * - `revoked` — the server has revoked this device.\n * - `not_paired` — there is no device record on disk (never paired, or already\n * cleared).\n */\nexport const ASSERTION_ISSUE_ERROR_CODES = [\n 'assertion_disabled',\n 'bad_request',\n 'audience_denied',\n 'shutting_down',\n 'revoked',\n 'not_paired',\n] as const;\n\nexport type AssertionIssueErrorCode = (typeof ASSERTION_ISSUE_ERROR_CODES)[number];\n\nexport type ShutdownReason = 'unpair' | 'operator';\n\nexport interface ShutdownParams {\n reason?: ShutdownReason;\n}\n\nexport function parseShutdownParams(value: unknown): ShutdownParams {\n if (!isRecord(value)) return {};\n return value.reason === 'unpair' || value.reason === 'operator' ? { reason: value.reason } : {};\n}\n","import { constants as fsConstants, promises as fs } from 'node:fs';\nimport net from 'node:net';\nimport {\n CONTROL_PROTOCOL_VERSION,\n ControlError,\n HANDSHAKE_TIMEOUT_MS,\n NdjsonLineReader,\n computeClientAuth,\n computeServerProof,\n controlEndpointPath,\n controlTokenPath,\n encodeFrame,\n isRecord,\n parseServerHello,\n parseServerReady,\n randomNonceHex,\n timingSafeEqualHex,\n} from '../daemon/control-protocol';\n\n/**\n * M4 Phase 2: the CLI-side half of the control socket — connects, performs\n * the mutual HMAC handshake (`../daemon/control-protocol.ts`), and exposes a\n * small `request()`/`subscribe()` surface every rewired command\n * (`status`/`tasks --follow`/`unpair`/`approve`/`reject`) builds on.\n *\n * `connectControlClient` never throws for the ordinary \"daemon isn't\n * running\" case — it returns a typed `{ok:false, reason}` result instead\n * (see {@link ConnectControlResult}), so a caller can render a clean\n * fallback message rather than catching an exception. Missing\n * `control.token` (the daemon was never started, or was stopped) is by far\n * the most common reason; any other connect/handshake failure collapses\n * into the same shape — a CLI user doesn't need to know WHY the control\n * socket isn't reachable, only that it isn't and a fallback is being used.\n */\n\nexport interface ControlClientOptions {\n storeDir: string;\n productId: string;\n /** Default: `HANDSHAKE_TIMEOUT_MS` (3000ms) — matches the server's own handshake timeout. */\n handshakeTimeoutMs?: number;\n /** Default 10000ms — applied per `request()` call; never applied to `subscribe()`, which is expected to stay open indefinitely. */\n requestTimeoutMs?: number;\n}\n\nexport interface ControlClient {\n /** Sends `{method, params}`, resolves with the server's `result`, or rejects with a {@link ControlError} (or a plain `Error` for a connection-level failure/timeout). */\n request<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Sends a streaming request; `onEvent` fires for each `event` frame. Returns a handle whose `close()` ends the WHOLE connection (per the protocol: \"client may just close the connection to unsubscribe\") — don't share a client between a `subscribe()` and other concurrent `request()` calls if you need them to outlive each other. */\n subscribe(method: string, params: unknown, onEvent: (event: unknown) => void): { close: () => void };\n /** Closes the underlying connection. Safe to call more than once. */\n close(): void;\n}\n\nexport type ConnectControlResult = { ok: true; client: ControlClient } | { ok: false; reason: string };\n\nconst MAX_CONTROL_TOKEN_BYTES = 256;\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction sameFileState(left: import('node:fs').BigIntStats, right: import('node:fs').BigIntStats): boolean {\n return (\n left.dev === right.dev &&\n left.ino === right.ino &&\n left.size === right.size &&\n left.mtimeNs === right.mtimeNs &&\n left.ctimeNs === right.ctimeNs\n );\n}\n\n/**\n * Read the authentication token through a bounded pathname-bound handle.\n * Diagnostics calls this client even when no daemon is expected, so a FIFO,\n * symlink swap, or oversized local file must become an ordinary offline result\n * rather than blocking the CLI or consuming unbounded memory.\n */\nasync function readControlToken(tokenPath: string): Promise<string | undefined> {\n let namedBefore: import('node:fs').BigIntStats;\n try {\n namedBefore = await fs.lstat(tokenPath, { bigint: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw err;\n }\n if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {\n throw new Error('control token is not a real regular file');\n }\n const handle = await fs.open(\n tokenPath,\n fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0) | (fsConstants.O_NOFOLLOW ?? 0),\n );\n try {\n const opened = await handle.stat({ bigint: true });\n const namedAfterOpen = await fs.lstat(tokenPath, { bigint: true });\n if (\n !opened.isFile() ||\n !namedAfterOpen.isFile() ||\n namedAfterOpen.isSymbolicLink() ||\n !sameFileState(namedBefore, opened) ||\n !sameFileState(opened, namedAfterOpen)\n ) {\n throw new Error('control token pathname changed before safe open');\n }\n if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {\n throw new Error('control token exceeds the bounded read limit');\n }\n const size = Number(opened.size);\n const bytes = Buffer.alloc(size);\n const { bytesRead } = await handle.read(bytes, 0, size, 0);\n const afterRead = await handle.stat({ bigint: true });\n const namedAfterRead = await fs.lstat(tokenPath, { bigint: true });\n if (\n bytesRead !== size ||\n namedAfterRead.isSymbolicLink() ||\n !sameFileState(opened, afterRead) ||\n !sameFileState(afterRead, namedAfterRead)\n ) {\n throw new Error('control token changed during bounded read');\n }\n return bytes.toString('utf8').trim();\n } finally {\n await handle.close();\n }\n}\n\n/** Reads the control token, connects, and performs the handshake — see the module doc comment for why failures here collapse into `{ok:false, reason}` rather than throwing. */\nexport async function connectControlClient(opts: ControlClientOptions): Promise<ConnectControlResult> {\n const tokenPath = controlTokenPath(opts.storeDir);\n let token: string;\n try {\n const read = await readControlToken(tokenPath);\n if (read === undefined) {\n return { ok: false, reason: 'daemon is not running (no control.token found)' };\n }\n token = read;\n } catch (err) {\n return { ok: false, reason: `could not read the control token: ${errorMessage(err)}` };\n }\n if (!token) {\n return { ok: false, reason: 'control token file is empty' };\n }\n\n const endpoint = controlEndpointPath(opts.productId, opts.storeDir);\n try {\n const client = await connectAndHandshake(endpoint, token, opts);\n return { ok: true, client };\n } catch (err) {\n return { ok: false, reason: `daemon control socket not reachable: ${errorMessage(err)}` };\n }\n}\n\nfunction connectAndHandshake(endpoint: string, token: string, opts: ControlClientOptions): Promise<ControlClient> {\n return new Promise((resolve, reject) => {\n const socket = net.createConnection(endpoint);\n const reader = new NdjsonLineReader();\n let phase: 'server-hello' | 'ready' = 'server-hello';\n let settled = false;\n const clientNonce = randomNonceHex();\n\n const timer = setTimeout(() => {\n fail(new Error('handshake timed out'));\n }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);\n timer.unref?.();\n\n function fail(err: unknown): void {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n socket.removeAllListeners();\n socket.destroy();\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n\n function succeed(): void {\n settled = true;\n clearTimeout(timer);\n socket.removeListener('error', onError);\n socket.removeListener('data', onData);\n resolve(createControlClient(socket, reader, opts));\n }\n\n function onData(chunk: Buffer): void {\n // Hardening finding (P2 re-gate): `reader.push` throws once the\n // still-unterminated remainder exceeds MAX_LINE_BYTES\n // (control-protocol.ts) — a hostile/broken peer sending a >64KiB line\n // with no newline. Uncaught, this would propagate out of the 'data'\n // listener and crash the whole CLI process (Node has no default\n // recovery for an exception thrown inside an EventEmitter callback).\n // Mirrors control-server.ts's own identical guard around its matching\n // `reader.push` call — route into this same handshake's `fail()` path\n // instead, exactly like every other handshake failure here.\n let lines: string[];\n try {\n lines = reader.push(chunk);\n } catch (err) {\n fail(err);\n return;\n }\n for (const line of lines) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n fail(new Error('malformed handshake frame'));\n return;\n }\n if (phase === 'server-hello') {\n const hello = parseServerHello(parsed);\n if (!hello) {\n fail(new Error('malformed or unexpected server hello'));\n return;\n }\n if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {\n fail(new Error('server failed to prove it holds the control token'));\n return;\n }\n socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));\n phase = 'ready';\n continue;\n }\n if (!parseServerReady(parsed)) {\n fail(new Error('server did not confirm readiness'));\n return;\n }\n succeed();\n return;\n }\n }\n\n function onError(err: unknown): void {\n fail(err);\n }\n\n socket.once('error', onError);\n socket.once('connect', () => {\n socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: 'client', nonce: clientNonce }));\n socket.on('data', onData);\n });\n });\n}\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (err: unknown) => void;\n onEvent?: (event: unknown) => void;\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(message)), ms);\n timer.unref?.();\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err: unknown) => {\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n\nfunction createControlClient(socket: net.Socket, reader: NdjsonLineReader, opts: ControlClientOptions): ControlClient {\n const pending = new Map<string, PendingCall>();\n let idSeq = 0;\n let closed = false;\n\n function handleFrame(parsed: unknown): void {\n if (!isRecord(parsed) || typeof parsed.id !== 'string') return;\n const entry = pending.get(parsed.id);\n if (!entry) return;\n if ('event' in parsed) {\n entry.onEvent?.(parsed.event);\n return;\n }\n if (parsed.ok === true) {\n pending.delete(parsed.id);\n entry.resolve(parsed.done === true ? undefined : (parsed as { result?: unknown }).result);\n return;\n }\n pending.delete(parsed.id);\n const shape = (parsed as { error?: { code?: unknown; message?: unknown } }).error;\n entry.reject(\n new ControlError(\n typeof shape?.code === 'string' ? shape.code : 'internal_error',\n typeof shape?.message === 'string' ? shape.message : 'unknown control error',\n ),\n );\n }\n\n socket.on('data', (chunk: Buffer) => {\n // Same hardening as connectAndHandshake's own onData above: `reader.push`\n // throws on a >64KiB unterminated line (MAX_LINE_BYTES,\n // control-protocol.ts) — uncaught, that would crash this CLI process\n // from inside an EventEmitter 'data' callback. This connection is\n // already past the handshake (there is no `fail()` closure here), so\n // fail closed the same way a malformed/unexpected frame elsewhere in\n // this function already does: destroy the connection (which rejects\n // every pending call via the 'close' handler below) rather than risk\n // continuing to read from a peer that just proved it doesn't speak this\n // protocol.\n let lines: string[];\n try {\n lines = reader.push(chunk);\n } catch {\n socket.destroy();\n return;\n }\n for (const line of lines) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue; // ignore a malformed line rather than tearing down an otherwise-healthy connection\n }\n handleFrame(parsed);\n }\n });\n socket.on('close', () => {\n closed = true;\n for (const entry of pending.values()) entry.reject(new Error('control connection closed'));\n pending.clear();\n });\n socket.on('error', () => {\n // 'close' always follows and rejects every pending call above — swallow\n // here so a peer reset never becomes an unhandled 'error' crash.\n });\n\n function send(method: string, params: unknown, onEvent?: (event: unknown) => void): { id: string; promise: Promise<unknown> } {\n const id = `c${++idSeq}`;\n const promise = new Promise<unknown>((resolve, reject) => {\n pending.set(id, { resolve, reject, onEvent });\n });\n socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));\n return { id, promise };\n }\n\n return {\n async request<T>(method: string, params?: unknown): Promise<T> {\n if (closed) throw new Error('control connection is closed');\n const { promise } = send(method, params);\n const result = await withTimeout(promise, opts.requestTimeoutMs ?? 10_000, `control request \"${method}\" timed out`);\n return result as T;\n },\n subscribe(method, params, onEvent) {\n const { id, promise } = send(method, params, onEvent);\n promise.catch(() => {\n // subscribe()'s lifetime is managed via close()/onEvent, not this\n // promise — swallow so destroying the connection later (which\n // rejects every pending call, this one included) never surfaces as\n // an unhandled rejection.\n });\n return {\n close: (): void => {\n pending.delete(id);\n socket.destroy();\n },\n };\n },\n close(): void {\n socket.destroy();\n },\n };\n}\n\n/**\n * Unpair's own poll for \"has the daemon actually exited yet\" (see\n * `bin/commands/unpair.ts`): both the control token file being gone AND a\n * fresh connect attempt being refused, checked directly rather than through\n * a full `connectControlClient` handshake — cheaper per poll, and matches\n * the exact two-condition check the M4 design calls for.\n */\nexport async function isControlDaemonGone(storeDir: string, productId: string): Promise<boolean> {\n const tokenGone = await fs.stat(controlTokenPath(storeDir)).then(\n () => false,\n (err) => (err as NodeJS.ErrnoException).code === 'ENOENT',\n );\n if (!tokenGone) return false;\n\n const endpoint = controlEndpointPath(productId, storeDir);\n return new Promise<boolean>((resolve) => {\n const socket = net.createConnection(endpoint);\n const finish = (gone: boolean): void => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(gone);\n };\n socket.once('connect', () => finish(false));\n socket.once('error', (err: NodeJS.ErrnoException) => finish(err.code === 'ECONNREFUSED' || err.code === 'ENOENT'));\n });\n}\n","import { createInterface } from 'node:readline';\n\n/**\n * M4 Phase 3: the testable core of `byok-approval-mcp` (`byok-approval-mcp.ts`\n * is the thin stdio-wiring entry point — mirrors this repo's existing\n * `bin/commands/*.ts` split: real logic lives in a plain module, the bin\n * script itself is glue no test ever imports directly).\n *\n * `byok-approval-mcp` is the MCP stdio server `claude`'s own\n * `--permission-prompt-tool` spawns AS ITS OWN CHILD PROCESS when the claude\n * adapter runs a task under `PermissionPolicy.mode: 'confirm'` (see\n * `../adapters/claude/permission-mapping.ts`'s `confirm`-mode doc comment).\n * It implements just enough of the MCP stdio transport (JSON-RPC 2.0,\n * newline-delimited, per the spec) to expose ONE tool — empirically\n * confirmed end-to-end against the real installed claude 2.1.216 binary\n * (M4 Phase 3 STEP 0): `initialize` -> `notifications/initialized` ->\n * `tools/list` -> `tools/call`, with the tool's arguments shaped exactly\n * `{tool_name, input, tool_use_id}` (claude's own real wire shape,\n * live-captured) and its expected response shaped\n * `{content:[{type:'text', text: JSON.stringify({behavior:'allow',\n * updatedInput} | {behavior:'deny', message})}]}` — the SAME shape the\n * Claude Agent SDK's in-process `canUseTool` callback returns (see\n * platform.claude.com/docs/en/agent-sdk/user-input), just crossing a\n * process boundary via MCP instead of an in-process function call.\n */\n\nexport const APPROVAL_TOOL_NAME = 'approval_prompt';\n\n/** Bound on how much of a tool call's `input` gets folded into the wire `task.await_approval.summary` — mirrors `events.ts`'s `RESULT_DIAGNOSTIC_MAX_CHARS`/`truncateResultDiagnostic` convention: a human-facing summary, not a full audit record. */\nexport const APPROVAL_SUMMARY_MAX_CHARS = 500;\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** `${toolName}: ${input}`, bounded — the human-readable description carried into `task.await_approval.summary` and (via the daemon/CLI's own rendering) whatever a real approver actually reads before deciding. */\nexport function summarizeToolCall(toolName: string, input: unknown): string {\n let inputStr: string;\n try {\n inputStr = JSON.stringify(input);\n } catch (err) {\n inputStr = `<unserializable input: ${errorMessage(err)}>`;\n }\n const bounded =\n inputStr.length > APPROVAL_SUMMARY_MAX_CHARS ? `${inputStr.slice(0, APPROVAL_SUMMARY_MAX_CHARS)}… [truncated]` : inputStr;\n return `${toolName}: ${bounded}`;\n}\n\nexport interface ApprovalOutcome {\n approved: boolean;\n reason?: string;\n}\n\n/** What `byok-approval-mcp.ts` (the real entry point) injects — the one real dependency this module has on the outside world. */\nexport interface ApprovalMcpDeps {\n /** Requests a decision from the daemon this task is running on. Any rejection/throw here is treated as fail-closed (deny) by `handleMcpRequest` — never surfaced to claude as a raw protocol error. */\n requestApproval(taskId: string, summary: string): Promise<ApprovalOutcome>;\n}\n\ninterface JsonRpcRequestLike {\n jsonrpc?: unknown;\n id?: unknown;\n method?: unknown;\n params?: unknown;\n}\n\n/**\n * Handles exactly one already-parsed JSON-RPC request object and returns the\n * exact response object to write back (`undefined` for a notification that\n * expects no reply, e.g. `notifications/initialized`) — no stdio/process\n * concerns at all, so tests call this directly with a stub {@link\n * ApprovalMcpDeps} instead of spawning a real process or a real control\n * socket. `serveApprovalMcpOverStdio` below is the only caller in production.\n */\nexport async function handleMcpRequest(\n req: JsonRpcRequestLike,\n deps: ApprovalMcpDeps,\n taskId: string,\n): Promise<Record<string, unknown> | undefined> {\n const id = req.id;\n\n if (req.method === 'initialize') {\n const params = (req.params ?? {}) as { protocolVersion?: unknown };\n return {\n jsonrpc: '2.0',\n id,\n result: {\n protocolVersion: typeof params.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',\n capabilities: { tools: {} },\n serverInfo: { name: 'byok-approval-mcp', version: '0.0.1' },\n },\n };\n }\n\n if (req.method === 'notifications/initialized') {\n return undefined; // notification — no response\n }\n\n if (req.method === 'tools/list') {\n return {\n jsonrpc: '2.0',\n id,\n result: {\n tools: [\n {\n name: APPROVAL_TOOL_NAME,\n description:\n 'Requests operator approval for a pending tool call. Blocks until a human (or this device\\'s daemon) decides, or the configured timeout elapses (fail-closed deny on timeout).',\n inputSchema: {\n type: 'object',\n properties: {\n tool_name: { type: 'string' },\n input: { type: 'object' },\n },\n },\n },\n ],\n },\n };\n }\n\n if (req.method === 'tools/call') {\n const params = (req.params ?? {}) as { name?: unknown; arguments?: unknown };\n if (params.name !== APPROVAL_TOOL_NAME) {\n return { jsonrpc: '2.0', id, error: { code: -32602, message: `unknown tool \"${String(params.name)}\"` } };\n }\n const args = (params.arguments ?? {}) as { tool_name?: unknown; input?: unknown };\n const toolName = typeof args.tool_name === 'string' ? args.tool_name : 'unknown tool';\n const input = args.input ?? {};\n const summary = summarizeToolCall(toolName, input);\n\n let outcome: ApprovalOutcome;\n try {\n outcome = await deps.requestApproval(taskId, summary);\n } catch (err) {\n // Fail-closed (task's own mandate): a daemon that's unreachable, a\n // control request that times out, or any other failure reaching the\n // approving device must never leave claude's own MCP call unanswered\n // — that risks claude abandoning the whole turn on its own (M4 Phase 3\n // STEP 0 found claude gives up on a permission-prompt-tool call that\n // never answers at all, ~1.5s in) rather than cleanly denying just\n // this one tool call and letting the conversation continue.\n outcome = { approved: false, reason: `could not reach the approving device: ${errorMessage(err)}` };\n }\n\n const payload = outcome.approved\n ? { behavior: 'allow' as const, updatedInput: input }\n : { behavior: 'deny' as const, message: outcome.reason ?? 'denied' };\n return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(payload) }] } };\n }\n\n if (id !== undefined) {\n return { jsonrpc: '2.0', id, error: { code: -32601, message: `unknown method: ${String(req.method)}` } };\n }\n return undefined;\n}\n\nexport interface ServeApprovalMcpOptions {\n taskId: string;\n deps: ApprovalMcpDeps;\n /** Defaults to `process.stdin`/`process.stdout` — overridable so tests can drive this over in-memory streams. */\n input?: NodeJS.ReadableStream;\n output?: NodeJS.WritableStream;\n}\n\n/**\n * Wires {@link handleMcpRequest} to real NDJSON stdio (the MCP stdio\n * transport: one JSON-RPC message per line, both directions) — the only\n * genuinely process-shaped piece of this module. Each line is handled\n * independently and asynchronously (never serialized against the others),\n * since claude's own parallel-tool-use can legitimately fire more than one\n * concurrent `tools/call` over the same connection.\n */\nexport function serveApprovalMcpOverStdio(opts: ServeApprovalMcpOptions): void {\n const input = opts.input ?? process.stdin;\n const output = opts.output ?? process.stdout;\n const rl = createInterface({ input, terminal: false });\n\n rl.on('line', (line) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return; // a stray non-JSON line is not this server's concern\n }\n void handleMcpRequest(parsed as JsonRpcRequestLike, opts.deps, opts.taskId).then((response) => {\n if (response !== undefined) output.write(`${JSON.stringify(response)}\\n`);\n });\n });\n}\n","#!/usr/bin/env node\nimport { connectControlClient, type ControlClient } from './control-client';\nimport { serveApprovalMcpOverStdio, type ApprovalMcpDeps } from './approval-mcp-server';\n\n/**\n * `byok-approval-mcp`: the bin entry `claude`'s own `--permission-prompt-tool`\n * spawns as its child process under `PermissionPolicy.mode: 'confirm'` — see\n * `approval-mcp-server.ts`'s module doc comment for the full protocol/design\n * writeup and `../adapters/claude/permission-mapping.ts`'s `confirm`-mode\n * doc comment for the empirical basis. This file is intentionally thin (glue\n * only, never imported by a test — mirrors `byok-agent.ts`'s own split from\n * `bin/commands/*.ts`): read env, wire a real `ApprovalMcpDeps.requestApproval`\n * against the control socket, hand off to `serveApprovalMcpOverStdio`.\n *\n * Env vars (set by `../adapters/claude/claude-adapter.ts`'s `start()` via the\n * generated `--mcp-config`'s own `env` block — never read from the daemon's\n * ambient environment, since a DIFFERENT product/device's config must never\n * leak in):\n * BYOK_STORE_DIR — this daemon's control-socket storeDir.\n * BYOK_PRODUCT_ID — this daemon's productId.\n * BYOK_TASK_ID — the task this approval request belongs to.\n * BYOK_APPROVAL_TIMEOUT_MS — `RuntimeOperationStartInput.approvalChannel.timeoutMs`,\n * echoed here so this process's OWN control\n * request waits at least that long (plus\n * slop) rather than timing out earlier than\n * the daemon's own authoritative deadline.\n */\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Slop added on top of the daemon's own approval timeout for this process's OWN control-socket request timeout — the daemon's `TaskRunner.requestApproval` is the authoritative deadline (see its own doc comment); this must never fire first and race it. */\nconst REQUEST_TIMEOUT_SLOP_MS = 5_000;\n\n/**\n * Reads a required env var, or exits fatally. Declared to RETURN `string`\n * (never `string | undefined`) so every caller gets a real, closure-safe\n * `string` type structurally — TypeScript's control-flow narrowing from an\n * `if (!x) process.exit(1)` guard does NOT survive being captured by a\n * nested function/closure (`getClient` below), so relying on that narrowing\n * directly on `process.env.X` would silently widen back to `string |\n * undefined` inside any closure that reads it. `process.exit` is typed\n * `never`, so this function's own control flow is sound without an explicit\n * `else`/`throw`.\n */\nfunction requireEnv(name: string): string {\n const value = process.env[name];\n if (!value) {\n process.stderr.write(`byok-approval-mcp: missing required env var ${name} — refusing to start\\n`);\n process.exit(1);\n }\n return value;\n}\n\nasync function main(): Promise<void> {\n const storeDir = requireEnv('BYOK_STORE_DIR');\n const productId = requireEnv('BYOK_PRODUCT_ID');\n const taskId = requireEnv('BYOK_TASK_ID');\n const timeoutMs = Number(process.env.BYOK_APPROVAL_TIMEOUT_MS ?? '600000');\n\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n process.stderr.write('byok-approval-mcp: invalid BYOK_APPROVAL_TIMEOUT_MS env var — refusing to start\\n');\n process.exit(1);\n }\n\n // Connect lazily (on first tools/call, not at startup) and cache the\n // in-flight connection attempt itself (not just the resolved client) so\n // concurrent tools/call requests (claude's parallel tool use can fire more\n // than one) share ONE connection attempt rather than racing several.\n let clientPromise: Promise<ControlClient> | undefined;\n function getClient(): Promise<ControlClient> {\n if (!clientPromise) {\n clientPromise = connectControlClient({ storeDir, productId, requestTimeoutMs: timeoutMs + REQUEST_TIMEOUT_SLOP_MS }).then(\n (conn) => {\n if (!conn.ok) {\n clientPromise = undefined; // let the NEXT call retry a fresh connection rather than caching a permanent failure\n throw new Error(conn.reason);\n }\n return conn.client;\n },\n );\n clientPromise.catch(() => {\n clientPromise = undefined;\n });\n }\n return clientPromise;\n }\n\n const deps: ApprovalMcpDeps = {\n requestApproval: async (tId, summary) => {\n let client: ControlClient;\n try {\n client = await getClient();\n } catch (err) {\n throw new Error(`could not connect to the daemon control socket: ${errorMessage(err)}`);\n }\n try {\n return await client.request('approvals.request', { taskId: tId, summary });\n } catch (err) {\n // The connection may be genuinely broken (daemon restarted, socket\n // reset, etc.) — drop the cache so a LATER tools/call in this same\n // claude session gets a fresh connection instead of repeating the\n // same dead one forever. This call itself still fails closed via\n // handleMcpRequest's own catch.\n clientPromise = undefined;\n throw err;\n }\n },\n };\n\n serveApprovalMcpOverStdio({ taskId, deps });\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`byok-approval-mcp: fatal error: ${errorMessage(err)}\\n`);\n process.exit(1);\n});\n"]}
1
+ {"version":3,"sources":["../../src/daemon/control-protocol.ts","../../src/bin/control-client.ts","../../src/bin/approval-mcp-server.ts","../../src/bin/byok-approval-mcp.ts"],"names":["fs","fsConstants","errorMessage"],"mappings":"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","file":"byok-approval-mcp.js","sourcesContent":["import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport path from 'node:path';\nimport type { TaskState } from '@byok-sdk/protocol';\nimport type { ApprovalDecision, PendingApproval } from './approvals';\nimport type { StorageCategory } from './journal/journal';\nimport type { StoragePressureState } from './journal/storage-policy';\nimport type { OperationalHealthSnapshot } from './operational-health';\nimport type { LocalAgentReleaseIdentity } from '../release-identity';\nimport type { McpToolsetConfig, McpToolsetRegistryStatus } from '../types';\n\n/**\n * M4 Phase 2: shared local-IPC contract between the daemon's control server\n * (`control-server.ts`) and the CLI's control client (`bin/control-client.ts`)\n * — frame shapes, endpoint path/pipe-name derivation, and the HMAC handshake\n * math. Both sides import from here so the two can never independently drift\n * (e.g. a mismatched HMAC label string, or a socket path computed two\n * slightly different ways).\n *\n * Transport: NDJSON (one JSON object per line) over a Unix domain socket\n * (darwin/linux) or a Windows named pipe — both addressed by the same\n * path-like string via Node's `net` module, so neither `control-server.ts`\n * nor `bin/control-client.ts` needs to special-case the transport itself,\n * only the path/pipe-name derivation below.\n */\n\nexport const CONTROL_PROTOCOL_VERSION = 1;\n\n/** Handshake must complete within this long, on both sides — see each side's own timer. */\nexport const HANDSHAKE_TIMEOUT_MS = 3000;\n\n// ---------------------------------------------------------------------------\n// Endpoint path / pipe name derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Conservative soft limit for a Unix domain socket path, in UTF-8 bytes.\n * macOS's `sockaddr_un.sun_path` is 104 bytes total (including the NUL\n * terminator and any prefix the kernel reserves), so anything comfortably\n * under 104 avoids `ENAMETOOLONG` at `bind()`/`connect()` time on the\n * tightest common platform.\n */\nconst UNIX_SOCKET_PATH_SOFT_LIMIT = 100;\n\n/**\n * The one fixed root the long-path fallback below binds under. Deliberately\n * NOT `os.tmpdir()`, which reads `TMPDIR`/`TMP`/`TEMP` — an endpoint address\n * that varies with the environment is not an endpoint both sides can find,\n * and a caller may have pointed `TMPDIR` INSIDE the very tree that made the\n * natural path too long, where `os.tmpdir()` yields an address LONGER than\n * the one being escaped (`bind()` then fails `EINVAL` and the daemon degrades\n * to no control socket at all). A literal `/tmp` is POSIX-guaranteed,\n * environment-independent, and short enough that this candidate always fits\n * the budget above. Same fix, same reasons as `daemon-owner.ts`'s\n * `STORE_MUTEX_FALLBACK_ROOT`; kept as its own constant because the two\n * derivations differ in name and ownership contract and must stay\n * independently readable.\n */\nconst CONTROL_SOCKET_FALLBACK_ROOT = '/tmp';\n\nfunction shortHash(input: string): string {\n return createHash('sha256').update(input, 'utf8').digest('hex').slice(0, 16);\n}\n\n/**\n * The Unix domain socket path for a daemon rooted at `storeDir`. Prefers\n * `<storeDir>/control.sock` (keeps every one of this daemon's local state\n * files under one directory, which is already created+chmod'd 0700 by the\n * time this matters — see `control-server.ts`'s `startControlServer`);\n * falls back, whenever the natural path would risk exceeding {@link\n * UNIX_SOCKET_PATH_SOFT_LIMIT}, to a short, deterministic path nested under\n * a PER-DAEMON PRIVATE subdirectory of {@link CONTROL_SOCKET_FALLBACK_ROOT}\n * — derived from a hash of `storeDir` alone, so both the daemon and any CLI\n * invocation pointed at the same `storeDir` independently compute the\n * identical fallback path.\n *\n * That root was `os.tmpdir()` until it was proven to break both halves of\n * that sentence: it reads `TMPDIR`, so the daemon (under a service manager)\n * and the CLI (in an operator shell) derived DIFFERENT addresses for one\n * store, and under a `TMPDIR` nested in the same long tree the fallback came\n * out LONGER than the path it escaped — `bind()` `EINVAL`, and the daemon\n * ran on with no control socket at all.\n *\n * Nested one level deep (rather than a bare `<hash>.sock` file directly in\n * that shared, world-traversable root) specifically so\n * `control-server.ts`'s `bindControlEndpoint` can create+chmod that\n * subdirectory 0700 BEFORE ever binding inside it — the directory's own\n * mode gates traversal into it regardless of the socket file's own\n * (briefly default-permissioned, until the post-bind `chmod`) mode, closing\n * what would otherwise be a real window for another user on the same\n * machine to reach a socket living directly in a shared tmpdir.\n */\nexport function controlSocketPath(storeDir: string): string {\n const candidate = path.join(storeDir, 'control.sock');\n if (Buffer.byteLength(candidate, 'utf8') <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;\n return path.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, 'sock');\n}\n\n/**\n * The Windows named pipe name for a daemon identified by `productId` +\n * (`path.resolve`-normalized) `storeDir`. Named pipes have no filesystem\n * path (no stale-file cleanup concern the way Unix sockets have — see\n * `control-server.ts`), but DO share one flat namespace across the whole\n * machine, so the name must be scoped to this exact daemon instance: two\n * different products, or two different store directories (e.g. two agents\n * of the same product — see `templates/service/README.md`'s \"running\n * multiple agents\" section), must never collide. `storeDir` is resolved\n * before hashing so a trivial path-form difference (trailing slash, etc.)\n * between the two sides can't split the name.\n *\n * NOT keyed by the OS user: a WinSW-installed service runs the daemon under\n * the Windows service account (e.g. `SYSTEM`) while the operator CLI runs\n * as the interactive user, so both sides must derive the identical name\n * from the same `storeDir` alone. Impostor servers are defeated by the\n * mutual HMAC handshake below, not by pipe-name secrecy — keying by user\n * was security theater that broke the service-account topology.\n */\nexport function controlPipeName(productId: string, storeDir: string): string {\n const id = shortHash(`${productId}|${path.resolve(storeDir)}`);\n return `\\\\\\\\.\\\\pipe\\\\byok-${id}`;\n}\n\n/**\n * Dispatches to {@link controlPipeName} on `win32`, {@link controlSocketPath}\n * everywhere else. `platform` defaults to `process.platform`; overridable\n * for tests exercising a specific platform's branch on any host (mirrors\n * `lifecycle/create-service-lifecycle.ts`'s identical `platform` override —\n * the REAL win32 named-pipe semantics can only be proven on actual Windows,\n * which CI's `ipc-smoke` job does; this override just makes the PATH-CHOICE\n * logic itself testable everywhere).\n */\nexport function controlEndpointPath(productId: string, storeDir: string, platform: NodeJS.Platform = process.platform): string {\n return platform === 'win32' ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);\n}\n\n/** Where the daemon writes its per-session control-auth token (see the handshake section below). Always a real file, even on Windows (pipes have no path of their own to piggyback secrets on). */\nexport function controlTokenPath(storeDir: string): string {\n return path.join(storeDir, 'control.token');\n}\n\n// ---------------------------------------------------------------------------\n// Handshake: mutual HMAC proof, token never sent over the wire\n// ---------------------------------------------------------------------------\n\nconst SERVER_PROOF_LABEL = 'byok-control-server|';\nconst CLIENT_AUTH_LABEL = 'byok-control-client|';\n\nexport function randomNonceHex(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction hmacHex(token: string, message: string): string {\n return createHmac('sha256', token).update(message, 'utf8').digest('hex');\n}\n\n/** What the server proves to the client: it holds `token`, bound to the client's own nonce so a captured proof can't be replayed against a different handshake. */\nexport function computeServerProof(token: string, clientNonce: string): string {\n return hmacHex(token, SERVER_PROOF_LABEL + clientNonce);\n}\n\n/** What the client proves to the server, symmetrically, bound to the server's nonce. */\nexport function computeClientAuth(token: string, serverNonce: string): string {\n return hmacHex(token, CLIENT_AUTH_LABEL + serverNonce);\n}\n\n/** Constant-time hex-string comparison (`crypto.timingSafeEqual` requires equal-length buffers; a length mismatch is itself a safe, immediate \"not equal\" — no early-exit on content). */\nexport function timingSafeEqualHex(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'hex');\n const bufB = Buffer.from(b, 'hex');\n if (bufA.length !== bufB.length) return false;\n return timingSafeEqual(bufA, bufB);\n}\n\n// ---------------------------------------------------------------------------\n// Handshake frame shapes + parsers\n// ---------------------------------------------------------------------------\n\nexport interface ClientHello {\n v: 1;\n hello: 'client';\n nonce: string;\n}\nexport interface ServerHello {\n v: 1;\n hello: 'server';\n proof: string;\n nonce: string;\n}\nexport interface ClientAuth {\n v: 1;\n auth: string;\n}\nexport interface ServerReady {\n v: 1;\n ready: true;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport function parseClientHello(value: unknown): ClientHello | undefined {\n if (!isRecord(value)) return undefined;\n if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== 'client' || typeof value.nonce !== 'string') return undefined;\n return { v: CONTROL_PROTOCOL_VERSION, hello: 'client', nonce: value.nonce };\n}\n\nexport function parseServerHello(value: unknown): ServerHello | undefined {\n if (!isRecord(value)) return undefined;\n if (\n value.v !== CONTROL_PROTOCOL_VERSION ||\n value.hello !== 'server' ||\n typeof value.proof !== 'string' ||\n typeof value.nonce !== 'string'\n ) {\n return undefined;\n }\n return { v: CONTROL_PROTOCOL_VERSION, hello: 'server', proof: value.proof, nonce: value.nonce };\n}\n\nexport function parseClientAuth(value: unknown): ClientAuth | undefined {\n if (!isRecord(value)) return undefined;\n if (value.v !== CONTROL_PROTOCOL_VERSION || typeof value.auth !== 'string') return undefined;\n return { v: CONTROL_PROTOCOL_VERSION, auth: value.auth };\n}\n\nexport function parseServerReady(value: unknown): ServerReady | undefined {\n if (!isRecord(value)) return undefined;\n if (value.v !== CONTROL_PROTOCOL_VERSION || value.ready !== true) return undefined;\n return { v: CONTROL_PROTOCOL_VERSION, ready: true };\n}\n\n// ---------------------------------------------------------------------------\n// RPC frame shapes (post-handshake)\n// ---------------------------------------------------------------------------\n\nexport interface RawControlRequest {\n /** Not narrowed to `1` here on purpose — an unexpected value is a `bad_version` RESPONSE, not a parse failure; see `control-server.ts`. */\n v: unknown;\n id: string;\n method: string;\n params?: unknown;\n}\n\n/** Loose shape check for an incoming request line: only `id`/`method` need to be well-formed for the server to be able to respond at all (including a `bad_version`/`unknown_method` response) — `v` is deliberately passed through unvalidated. */\nexport function parseRawControlRequest(value: unknown): RawControlRequest | undefined {\n if (!isRecord(value)) return undefined;\n if (typeof value.id !== 'string' || typeof value.method !== 'string') return undefined;\n return { v: value.v, id: value.id, method: value.method, params: value.params };\n}\n\nexport interface ControlErrorShape {\n code: string;\n message: string;\n}\n\nexport interface ControlResponseOk {\n v: 1;\n id: string;\n ok: true;\n result?: unknown;\n /** Present (and `true`) only on the final frame of a streaming method — see `control-server.ts`'s dispatch. */\n done?: true;\n}\n\nexport interface ControlResponseErr {\n v: 1;\n id: string;\n ok: false;\n error: ControlErrorShape;\n}\n\nexport type ControlResponse = ControlResponseOk | ControlResponseErr;\n\nexport interface ControlEventFrame {\n v: 1;\n id: string;\n event: unknown;\n}\n\nexport function encodeFrame(frame: unknown): string {\n return `${JSON.stringify(frame)}\\n`;\n}\n\n/** Thrown by a method handler to control the wire error `{code, message}` a caller sees — anything else thrown surfaces as a generic `internal_error`. See `control-server.ts`'s dispatch and `bin/control-client.ts`'s `request()` (which re-throws this same class on the client side). */\nexport class ControlError extends Error {\n constructor(\n public readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = 'ControlError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// NDJSON line framing\n// ---------------------------------------------------------------------------\n\n/**\n * Bound on a single NDJSON line's byte length. Every real frame this\n * protocol ever sends (handshake frames, requests/responses/events) is well\n * under this — it exists purely as a defensive cap against a misbehaving or\n * hostile peer streaming an unterminated line forever to grow `pending`\n * without bound. Exceeding it is a fail-closed condition: {@link\n * NdjsonLineReader.push} throws, and every caller (`control-server.ts`,\n * `bin/control-client.ts`) destroys the connection on that throw.\n */\nexport const MAX_LINE_BYTES = 64 * 1024;\n\n/**\n * Buffers raw socket bytes and yields complete lines. Splits on the raw byte\n * `0x0a` BEFORE any UTF-8 decoding (mirrors `bin/audit-log.ts`'s\n * `followAuditLog`) — `0x0A` can only ever appear as an actual newline in\n * valid UTF-8, so this never risks decoding a multi-byte character that\n * happened to straddle a chunk boundary.\n */\nexport class NdjsonLineReader {\n private pending: Buffer = Buffer.alloc(0);\n\n /** @throws if the still-unterminated remainder exceeds {@link MAX_LINE_BYTES} — see that constant's own doc comment. */\n push(chunk: Buffer): string[] {\n this.pending = this.pending.length > 0 ? Buffer.concat([this.pending, chunk]) : chunk;\n const lines: string[] = [];\n let newlineIndex: number;\n // eslint-disable-next-line no-cond-assign\n while ((newlineIndex = this.pending.indexOf(0x0a)) !== -1) {\n const line = this.pending.subarray(0, newlineIndex).toString('utf8');\n this.pending = this.pending.subarray(newlineIndex + 1);\n if (line.length > 0) lines.push(line);\n }\n if (this.pending.length > MAX_LINE_BYTES) {\n throw new Error(`NDJSON line exceeded ${MAX_LINE_BYTES} bytes without a terminating newline`);\n }\n return lines;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Method contracts (Phase 2 surface) — shared by create-daemon.ts's control\n// method registry and the CLI commands that call them.\n// ---------------------------------------------------------------------------\n\nexport interface ControlActiveTask {\n taskId: string;\n state: TaskState;\n}\n\n/**\n * M4 Phase 4 (part B.3, observability): a cheap per-active-task queue-depth\n * watermark for the `status` result. The IDEAL metric here would be each\n * runtime adapter's own event-queue depth (`util/async-queue.ts`'s\n * `AsyncQueue`) — but that queue lives inside each adapter's concrete\n * `Session` implementation, and `Session.events` (`types.ts`) is typed only\n * as a plain `AsyncIterable<AgentEvent>`, which has no queryable backlog\n * size; reaching it would mean adding a new method to the `Session`\n * interface AND implementing it in all three bundled adapters\n * (pi/claude/codex), which is out of scope for this pass. This instead\n * reflects two things `TaskRunner` already cheaply knows about the SAME\n * task without any new plumbing: how much progress is buffered locally\n * (not yet flushed as a `task.progress` batch), and how many out-of-band\n * approval requests are currently in flight for it. See\n * `task-runner.ts`'s `getQueueWatermarks` for how each field is computed.\n */\nexport interface TaskQueueWatermark {\n taskId: string;\n /** Events buffered in this task's `ProgressBatcher`, not yet flushed as a `task.progress` batch. */\n progressBatcherPending: number;\n /** Approval requests currently in flight for this task: 1 if one is actively dispatched (registered + `task.await_approval` sent) plus however many more are queued behind it (M4 Phase 4 fold-in — see `TaskRunner.requestApproval`). */\n pendingApprovals: number;\n}\n\n/**\n * S3b (L-003): local storage usage and pressure, as the `status` method\n * reports them (architecture §12.7.2.1).\n *\n * Named `storage*` throughout, NOT `watermark*`: {@link TaskQueueWatermark}\n * above is a per-task progress-buffer depth and has nothing to do with disk.\n * Two unrelated concepts sharing a name on one status result is how an\n * operator reads the wrong number during an incident.\n *\n * Present only when a daemon actually runs a storage policy\n * (`DaemonConfig.hostedJournal.storagePolicy`). Absent means \"not measured\",\n * which is a different statement from \"measured, and fine\" — so it is an\n * absent field rather than a zeroed one.\n */\nexport interface ControlStorageStatus {\n /** §12.7.2.1's four states. `hard-pressure` declines new offers; `emergency` refuses to ack at all. */\n pressureState: StoragePressureState;\n /** `maxStoreBytes` — the budget `usedBytes` is measured against. */\n budgetBytes: number;\n /** Total across every category below, as of `measuredAt`. */\n usedBytes: number;\n /** Bytes available to this daemon on the store's filesystem — the free-space axis of the watermark, independent of the budget. */\n freeBytes: number;\n measuredAt: string;\n /** The five §12.7.2.1 categories, always reported separately — a single total cannot drive a category-scoped cleanup order or a category-scoped never-delete list. */\n categories: ControlStorageCategoryUsage[];\n /** The most recent bounded WAL checkpoint + incremental vacuum, if one has run in this daemon's lifetime. */\n lastCompaction?: ControlStorageCompaction;\n}\n\nexport interface ControlStorageCategoryUsage {\n category: StorageCategory;\n bytes: number;\n /** `true` when this is a host-reported or sampled figure rather than one measured off the filesystem. */\n approximate: boolean;\n}\n\nexport interface ControlStorageCompaction {\n checkpointed: boolean;\n walFramesRemaining: number;\n pagesVacuumed: number;\n durationMs: number;\n at: string;\n}\n\n/** Result shape for the `status` method — see `create-daemon.ts`'s control-method wiring for how each field is sourced, and `bin/format.ts`'s `formatLiveStatusLines` for how the CLI renders it. */\nexport interface ControlStatusResult {\n /** Process-immutable Local Agent application release; absent only for an older control peer. */\n localAgentRelease?: Readonly<LocalAgentReleaseIdentity>;\n pid: number;\n uptimeMs: number;\n paired: boolean;\n deviceId?: string;\n /** The connection state machine's own current value (`ws-transport.ts`'s `ConnectionState`) — e.g. `'open'`, `'degraded'` (long-poll fallback), `'revoked'`, `'closed'`, `'connecting'`. */\n transport: string;\n activeTasks: ControlActiveTask[];\n runtimeIds: string[];\n /** M4 Phase 4 (part B.3): per-active-task queue watermarks — see {@link TaskQueueWatermark}. */\n queueWatermarks: TaskQueueWatermark[];\n /**\n * Finding F4 (cross-model adversarial review): the actual pending\n * approvals currently dispatched — the SAME entries `approvals.list`\n * returns (`ApprovalRegistry.list()`), surfaced here too so a single\n * `status` call can show an operator every `approvalId` they'd need to\n * `approve`/`reject`, without a second control-socket round trip. This is\n * `approvalsPending`'s own source list (`approvalsPending ===\n * approvals.length`, always).\n */\n approvals: PendingApproval[];\n /** M4 Phase 4 (part B.3): total approvals currently DISPATCHED (registered) across the whole daemon — the same count `approvals.list` returns, surfaced here too for a one-call status view. */\n approvalsPending: number;\n /** S3b (L-003): local storage usage + pressure — see {@link ControlStorageStatus}. Absent unless a storage policy is configured. */\n storage?: ControlStorageStatus;\n /** Local lifecycle/retry budget. This is not the transport state above. */\n operationalHealth: OperationalHealthSnapshot;\n /** Redacted content-addressed status from the daemon's single local registry. */\n toolsets: McpToolsetRegistryStatus;\n}\n\nexport interface ToolsetsReloadParams {\n expectedRevision: string;\n mcpToolsets: Record<string, McpToolsetConfig>;\n}\n\n/** Maximum opaque pairing-code payload accepted over local control IPC. */\nexport const ENROLLMENT_PAIRING_CODE_MAX_BYTES = 1024;\n\nexport interface EnrollmentPairParams {\n pairingCode: string;\n}\n\n/**\n * Exact shape gate for service-identity pairing. The code is opaque server\n * authority: this only bounds transport bytes and rejects unknown fields; it\n * never parses, normalizes or logs the code.\n */\nexport function parseEnrollmentPairParams(value: unknown): EnrollmentPairParams | undefined {\n if (!isRecord(value) || Object.keys(value).some((key) => key !== 'pairingCode')) return undefined;\n if (\n typeof value.pairingCode !== 'string' ||\n value.pairingCode.length === 0 ||\n Buffer.byteLength(value.pairingCode, 'utf8') > ENROLLMENT_PAIRING_CODE_MAX_BYTES\n ) {\n return undefined;\n }\n return { pairingCode: value.pairingCode };\n}\n\n/** Shape-only parser; executable definition validation remains registry-owned. */\nexport function parseToolsetsReloadParams(value: unknown): ToolsetsReloadParams | undefined {\n if (!isRecord(value) || Object.keys(value).some((key) => key !== 'expectedRevision' && key !== 'mcpToolsets')) {\n return undefined;\n }\n if (typeof value.expectedRevision !== 'string' || !isRecord(value.mcpToolsets) || Array.isArray(value.mcpToolsets)) {\n return undefined;\n }\n return {\n expectedRevision: value.expectedRevision,\n mcpToolsets: value.mcpToolsets as Record<string, McpToolsetConfig>,\n };\n}\n\nexport interface ApprovalsListResult {\n approvals: PendingApproval[];\n}\n\nexport type { ApprovalDecision, PendingApproval } from './approvals';\n\nexport interface ApprovalsResolveParams {\n approvalId: string;\n decision: ApprovalDecision;\n reason?: string;\n}\n\nexport function parseApprovalsResolveParams(value: unknown): ApprovalsResolveParams | undefined {\n if (!isRecord(value)) return undefined;\n if (typeof value.approvalId !== 'string') return undefined;\n if (value.decision !== 'approve' && value.decision !== 'reject') return undefined;\n if (value.reason !== undefined && typeof value.reason !== 'string') return undefined;\n return { approvalId: value.approvalId, decision: value.decision, reason: value.reason };\n}\n\n/**\n * M4 Phase 3: the control method `byok-approval-mcp` (`bin/byok-approval-mcp.ts`)\n * calls FROM a claude-spawned MCP-server child process — a genuinely\n * different OS process from the daemon, reachable only over this same\n * control socket (see `../types.ts`'s `ApprovalChannel` doc comment for the\n * full why). `taskId` correlates the request to an active task;\n * `summary` is a short, human-readable description of the gated action\n * (carried verbatim into the wire `task.await_approval.summary`).\n */\nexport interface ApprovalsRequestParams {\n taskId: string;\n summary: string;\n}\n\nexport function parseApprovalsRequestParams(value: unknown): ApprovalsRequestParams | undefined {\n if (!isRecord(value)) return undefined;\n if (typeof value.taskId !== 'string' || value.taskId.length === 0) return undefined;\n if (typeof value.summary !== 'string') return undefined;\n return { taskId: value.taskId, summary: value.summary };\n}\n\n/** Result of `approvals.request` — the outcome `byok-approval-mcp` translates into its own MCP `allow`/`deny` answer. */\nexport interface ApprovalsRequestResult {\n approved: boolean;\n reason?: string;\n}\n\nexport interface AgentMessagePublishParams {\n contextToken: string;\n contentType: 'text/plain' | 'text/markdown';\n body: string;\n}\n\nexport function parseAgentMessagePublishParams(value: unknown): AgentMessagePublishParams | undefined {\n if (!isRecord(value) || Object.keys(value).some((key) => key !== 'contextToken' && key !== 'contentType' && key !== 'body')) return undefined;\n if (typeof value.contextToken !== 'string' || value.contextToken.length < 32 || value.contextToken.length > 160) return undefined;\n if (value.contentType !== 'text/plain' && value.contentType !== 'text/markdown') return undefined;\n if (typeof value.body !== 'string' || value.body.length === 0) return undefined;\n return { contextToken: value.contextToken, contentType: value.contentType, body: value.body };\n}\n\nexport interface AgentMessagePublishResult {\n messageId: string;\n state: 'staged' | 'pending';\n}\n\nexport interface AgentMemoryRecallParams {\n contextToken: string;\n path: string;\n ifRevision?: string;\n}\n\nexport interface AgentMemorySaveParams {\n contextToken: string;\n op: 'replace' | 'delete';\n path: string;\n expectedRevision: string;\n content?: string;\n}\n\nfunction validAgentMemoryContextToken(value: unknown): value is string {\n return typeof value === 'string' && value.length >= 32 && value.length <= 160 && !/[\\u0000\\r\\n]/u.test(value);\n}\n\n/** Parser only validates the local IPC shape. Agent identity and memory root stay daemon-owned. */\nexport function parseAgentMemoryRecallParams(value: unknown): AgentMemoryRecallParams | undefined {\n if (!isRecord(value) || Object.keys(value).some((key) => key !== 'contextToken' && key !== 'path' && key !== 'ifRevision')) return undefined;\n if (!validAgentMemoryContextToken(value.contextToken) || typeof value.path !== 'string' || (value.ifRevision !== undefined && typeof value.ifRevision !== 'string')) return undefined;\n return { contextToken: value.contextToken, path: value.path, ...(value.ifRevision === undefined ? {} : { ifRevision: value.ifRevision }) };\n}\n\nexport function parseAgentMemorySaveParams(value: unknown): AgentMemorySaveParams | undefined {\n if (!isRecord(value) || Object.keys(value).some((key) => key !== 'contextToken' && key !== 'op' && key !== 'path' && key !== 'expectedRevision' && key !== 'content')) return undefined;\n if (!validAgentMemoryContextToken(value.contextToken) || (value.op !== 'replace' && value.op !== 'delete') || typeof value.path !== 'string' || typeof value.expectedRevision !== 'string' || (value.op === 'replace' && typeof value.content !== 'string') || (value.op === 'delete' && value.content !== undefined)) return undefined;\n const content = value.content;\n return { contextToken: value.contextToken, op: value.op, path: value.path, expectedRevision: value.expectedRevision, ...(typeof content === 'string' ? { content } : {}) };\n}\n\n// ---------------------------------------------------------------------------\n// `assertion.issue` (plan device-assertion-broker)\n// ---------------------------------------------------------------------------\n\n/**\n * Params for `assertion.issue`: a sibling local process (the host's own CLI,\n * installed alongside this daemon) asking the daemon to mint one short-lived,\n * audience-scoped device assertion with the paired device key. See\n * `@byok-sdk/core`'s `device-assertion.ts` for the envelope, and\n * `create-daemon.ts`'s handler for the six fail-closed gates every call passes\n * through in a fixed order.\n *\n * One field, and nothing else. In particular there is deliberately no caller\n * identity, no requested TTL, and no requested claim set: every process running\n * as this UID can reach the control socket, so anything a caller \"tells\" the\n * daemon about itself is decoration, and a caller-chosen lifetime is just the\n * TTL ceiling handed to whoever asks.\n */\nexport interface AssertionIssueParams {\n audience: string;\n}\n\n/**\n * Bound on the `audience` a caller may send, in UTF-8 bytes — mirrors\n * `@byok-sdk/core`'s `DEVICE_ASSERTION_AUDIENCE_MAX_BYTES`. Restated here rather\n * than imported so the WIRE bound is checked before anything reaches the claim\n * schema: this is the frame-level shape gate, and it must reject an oversized\n * value without that value ever reaching a signer or an audit line.\n */\nexport const ASSERTION_AUDIENCE_MAX_BYTES = 256;\n\n/**\n * Strict shape check. `undefined` means `bad_request` — a distinct gate from\n * `audience_denied` (see `create-daemon.ts`): \"you sent something that is not a\n * request\" and \"you asked for an audience you may not have\" are different\n * facts, and collapsing them would let a caller probe the allowlist by\n * malforming requests.\n *\n * Rejects an unknown key outright rather than ignoring it. A tolerated extra\n * field is how a future caller comes to believe it can influence the claim set.\n */\nexport function parseAssertionIssueParams(value: unknown): AssertionIssueParams | undefined {\n if (!isRecord(value)) return undefined;\n const keys = Object.keys(value);\n if (keys.length !== 1 || keys[0] !== 'audience') return undefined;\n const { audience } = value;\n if (typeof audience !== 'string' || audience.length === 0) return undefined;\n if (Buffer.byteLength(audience, 'utf8') > ASSERTION_AUDIENCE_MAX_BYTES) return undefined;\n return { audience };\n}\n\n/**\n * Result of `assertion.issue`. `assertion` is the full signing envelope\n * (`DeviceAssertionEnvelopeV1`), carried as an opaque JSON value on this wire —\n * the caller hands it to the host's cloud, which parses and verifies it with\n * core's own `verifyDeviceAssertion`. `expiresAt` is repeated outside the\n * envelope purely so a caller can schedule a refresh without parsing claims it\n * has no business interpreting.\n */\nexport interface AssertionIssueResult {\n assertion: unknown;\n expiresAt: string;\n}\n\n/**\n * The six `ControlError` codes `assertion.issue` can answer with, in the exact\n * order the handler checks them (`create-daemon.ts`). Each one is a distinct\n * refusal with a distinct cause; none of them ever signs anything.\n *\n * - `assertion_disabled` — this daemon has no `deviceAssertion` config, or an\n * empty audience allowlist. The feature is OFF by default.\n * - `bad_request` — params were not `{audience: string}` within the byte bound.\n * - `audience_denied` — the audience is not in the configured allowlist. The\n * message never echoes the allowlist: a refusal must not be an enumeration\n * oracle.\n * - `shutting_down` — a shutdown has been requested. Closes the window between\n * the shutdown RPC being acknowledged and the control socket actually\n * closing, during which a device that is being unpaired could otherwise still\n * mint credentials.\n * - `revoked` — the server has revoked this device.\n * - `not_paired` — there is no device record on disk (never paired, or already\n * cleared).\n */\nexport const ASSERTION_ISSUE_ERROR_CODES = [\n 'assertion_disabled',\n 'bad_request',\n 'audience_denied',\n 'shutting_down',\n 'revoked',\n 'not_paired',\n] as const;\n\nexport type AssertionIssueErrorCode = (typeof ASSERTION_ISSUE_ERROR_CODES)[number];\n\nexport type ShutdownReason = 'unpair' | 'operator';\n\nexport interface ShutdownParams {\n reason?: ShutdownReason;\n}\n\nexport function parseShutdownParams(value: unknown): ShutdownParams {\n if (!isRecord(value)) return {};\n return value.reason === 'unpair' || value.reason === 'operator' ? { reason: value.reason } : {};\n}\n","import { constants as fsConstants, promises as fs } from 'node:fs';\nimport net from 'node:net';\nimport {\n CONTROL_PROTOCOL_VERSION,\n ControlError,\n HANDSHAKE_TIMEOUT_MS,\n NdjsonLineReader,\n computeClientAuth,\n computeServerProof,\n controlEndpointPath,\n controlTokenPath,\n encodeFrame,\n isRecord,\n parseServerHello,\n parseServerReady,\n randomNonceHex,\n timingSafeEqualHex,\n} from '../daemon/control-protocol';\n\n/**\n * M4 Phase 2: the CLI-side half of the control socket — connects, performs\n * the mutual HMAC handshake (`../daemon/control-protocol.ts`), and exposes a\n * small `request()`/`subscribe()` surface every rewired command\n * (`status`/`tasks --follow`/`unpair`/`approve`/`reject`) builds on.\n *\n * `connectControlClient` never throws for the ordinary \"daemon isn't\n * running\" case — it returns a typed `{ok:false, reason}` result instead\n * (see {@link ConnectControlResult}), so a caller can render a clean\n * fallback message rather than catching an exception. Missing\n * `control.token` (the daemon was never started, or was stopped) is by far\n * the most common reason; any other connect/handshake failure collapses\n * into the same shape — a CLI user doesn't need to know WHY the control\n * socket isn't reachable, only that it isn't and a fallback is being used.\n */\n\nexport interface ControlClientOptions {\n storeDir: string;\n productId: string;\n /** Default: `HANDSHAKE_TIMEOUT_MS` (3000ms) — matches the server's own handshake timeout. */\n handshakeTimeoutMs?: number;\n /** Default 10000ms — applied per `request()` call; never applied to `subscribe()`, which is expected to stay open indefinitely. */\n requestTimeoutMs?: number;\n}\n\nexport interface ControlClient {\n /** Sends `{method, params}`, resolves with the server's `result`, or rejects with a {@link ControlError} (or a plain `Error` for a connection-level failure/timeout). */\n request<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Sends a streaming request; `onEvent` fires for each `event` frame. Returns a handle whose `close()` ends the WHOLE connection (per the protocol: \"client may just close the connection to unsubscribe\") — don't share a client between a `subscribe()` and other concurrent `request()` calls if you need them to outlive each other. */\n subscribe(method: string, params: unknown, onEvent: (event: unknown) => void): { close: () => void };\n /** Closes the underlying connection. Safe to call more than once. */\n close(): void;\n}\n\nexport type ConnectControlResult = { ok: true; client: ControlClient } | { ok: false; reason: string };\n\nconst MAX_CONTROL_TOKEN_BYTES = 256;\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction sameFileState(left: import('node:fs').BigIntStats, right: import('node:fs').BigIntStats): boolean {\n return (\n left.dev === right.dev &&\n left.ino === right.ino &&\n left.size === right.size &&\n left.mtimeNs === right.mtimeNs &&\n left.ctimeNs === right.ctimeNs\n );\n}\n\n/**\n * Read the authentication token through a bounded pathname-bound handle.\n * Diagnostics calls this client even when no daemon is expected, so a FIFO,\n * symlink swap, or oversized local file must become an ordinary offline result\n * rather than blocking the CLI or consuming unbounded memory.\n */\nasync function readControlToken(tokenPath: string): Promise<string | undefined> {\n let namedBefore: import('node:fs').BigIntStats;\n try {\n namedBefore = await fs.lstat(tokenPath, { bigint: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw err;\n }\n if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {\n throw new Error('control token is not a real regular file');\n }\n const handle = await fs.open(\n tokenPath,\n fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0) | (fsConstants.O_NOFOLLOW ?? 0),\n );\n try {\n const opened = await handle.stat({ bigint: true });\n const namedAfterOpen = await fs.lstat(tokenPath, { bigint: true });\n if (\n !opened.isFile() ||\n !namedAfterOpen.isFile() ||\n namedAfterOpen.isSymbolicLink() ||\n !sameFileState(namedBefore, opened) ||\n !sameFileState(opened, namedAfterOpen)\n ) {\n throw new Error('control token pathname changed before safe open');\n }\n if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {\n throw new Error('control token exceeds the bounded read limit');\n }\n const size = Number(opened.size);\n const bytes = Buffer.alloc(size);\n const { bytesRead } = await handle.read(bytes, 0, size, 0);\n const afterRead = await handle.stat({ bigint: true });\n const namedAfterRead = await fs.lstat(tokenPath, { bigint: true });\n if (\n bytesRead !== size ||\n namedAfterRead.isSymbolicLink() ||\n !sameFileState(opened, afterRead) ||\n !sameFileState(afterRead, namedAfterRead)\n ) {\n throw new Error('control token changed during bounded read');\n }\n return bytes.toString('utf8').trim();\n } finally {\n await handle.close();\n }\n}\n\n/** Reads the control token, connects, and performs the handshake — see the module doc comment for why failures here collapse into `{ok:false, reason}` rather than throwing. */\nexport async function connectControlClient(opts: ControlClientOptions): Promise<ConnectControlResult> {\n const tokenPath = controlTokenPath(opts.storeDir);\n let token: string;\n try {\n const read = await readControlToken(tokenPath);\n if (read === undefined) {\n return { ok: false, reason: 'daemon is not running (no control.token found)' };\n }\n token = read;\n } catch (err) {\n return { ok: false, reason: `could not read the control token: ${errorMessage(err)}` };\n }\n if (!token) {\n return { ok: false, reason: 'control token file is empty' };\n }\n\n const endpoint = controlEndpointPath(opts.productId, opts.storeDir);\n try {\n const client = await connectAndHandshake(endpoint, token, opts);\n return { ok: true, client };\n } catch (err) {\n return { ok: false, reason: `daemon control socket not reachable: ${errorMessage(err)}` };\n }\n}\n\nfunction connectAndHandshake(endpoint: string, token: string, opts: ControlClientOptions): Promise<ControlClient> {\n return new Promise((resolve, reject) => {\n const socket = net.createConnection(endpoint);\n const reader = new NdjsonLineReader();\n let phase: 'server-hello' | 'ready' = 'server-hello';\n let settled = false;\n const clientNonce = randomNonceHex();\n\n const timer = setTimeout(() => {\n fail(new Error('handshake timed out'));\n }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);\n timer.unref?.();\n\n function fail(err: unknown): void {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n socket.removeAllListeners();\n socket.destroy();\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n\n function succeed(): void {\n settled = true;\n clearTimeout(timer);\n socket.removeListener('error', onError);\n socket.removeListener('data', onData);\n resolve(createControlClient(socket, reader, opts));\n }\n\n function onData(chunk: Buffer): void {\n // Hardening finding (P2 re-gate): `reader.push` throws once the\n // still-unterminated remainder exceeds MAX_LINE_BYTES\n // (control-protocol.ts) — a hostile/broken peer sending a >64KiB line\n // with no newline. Uncaught, this would propagate out of the 'data'\n // listener and crash the whole CLI process (Node has no default\n // recovery for an exception thrown inside an EventEmitter callback).\n // Mirrors control-server.ts's own identical guard around its matching\n // `reader.push` call — route into this same handshake's `fail()` path\n // instead, exactly like every other handshake failure here.\n let lines: string[];\n try {\n lines = reader.push(chunk);\n } catch (err) {\n fail(err);\n return;\n }\n for (const line of lines) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n fail(new Error('malformed handshake frame'));\n return;\n }\n if (phase === 'server-hello') {\n const hello = parseServerHello(parsed);\n if (!hello) {\n fail(new Error('malformed or unexpected server hello'));\n return;\n }\n if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {\n fail(new Error('server failed to prove it holds the control token'));\n return;\n }\n socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));\n phase = 'ready';\n continue;\n }\n if (!parseServerReady(parsed)) {\n fail(new Error('server did not confirm readiness'));\n return;\n }\n succeed();\n return;\n }\n }\n\n function onError(err: unknown): void {\n fail(err);\n }\n\n socket.once('error', onError);\n socket.once('connect', () => {\n socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: 'client', nonce: clientNonce }));\n socket.on('data', onData);\n });\n });\n}\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (err: unknown) => void;\n onEvent?: (event: unknown) => void;\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(message)), ms);\n timer.unref?.();\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err: unknown) => {\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n\nfunction createControlClient(socket: net.Socket, reader: NdjsonLineReader, opts: ControlClientOptions): ControlClient {\n const pending = new Map<string, PendingCall>();\n let idSeq = 0;\n let closed = false;\n\n function handleFrame(parsed: unknown): void {\n if (!isRecord(parsed) || typeof parsed.id !== 'string') return;\n const entry = pending.get(parsed.id);\n if (!entry) return;\n if ('event' in parsed) {\n entry.onEvent?.(parsed.event);\n return;\n }\n if (parsed.ok === true) {\n pending.delete(parsed.id);\n entry.resolve(parsed.done === true ? undefined : (parsed as { result?: unknown }).result);\n return;\n }\n pending.delete(parsed.id);\n const shape = (parsed as { error?: { code?: unknown; message?: unknown } }).error;\n entry.reject(\n new ControlError(\n typeof shape?.code === 'string' ? shape.code : 'internal_error',\n typeof shape?.message === 'string' ? shape.message : 'unknown control error',\n ),\n );\n }\n\n socket.on('data', (chunk: Buffer) => {\n // Same hardening as connectAndHandshake's own onData above: `reader.push`\n // throws on a >64KiB unterminated line (MAX_LINE_BYTES,\n // control-protocol.ts) — uncaught, that would crash this CLI process\n // from inside an EventEmitter 'data' callback. This connection is\n // already past the handshake (there is no `fail()` closure here), so\n // fail closed the same way a malformed/unexpected frame elsewhere in\n // this function already does: destroy the connection (which rejects\n // every pending call via the 'close' handler below) rather than risk\n // continuing to read from a peer that just proved it doesn't speak this\n // protocol.\n let lines: string[];\n try {\n lines = reader.push(chunk);\n } catch {\n socket.destroy();\n return;\n }\n for (const line of lines) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue; // ignore a malformed line rather than tearing down an otherwise-healthy connection\n }\n handleFrame(parsed);\n }\n });\n socket.on('close', () => {\n closed = true;\n for (const entry of pending.values()) entry.reject(new Error('control connection closed'));\n pending.clear();\n });\n socket.on('error', () => {\n // 'close' always follows and rejects every pending call above — swallow\n // here so a peer reset never becomes an unhandled 'error' crash.\n });\n\n function send(method: string, params: unknown, onEvent?: (event: unknown) => void): { id: string; promise: Promise<unknown> } {\n const id = `c${++idSeq}`;\n const promise = new Promise<unknown>((resolve, reject) => {\n pending.set(id, { resolve, reject, onEvent });\n });\n socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));\n return { id, promise };\n }\n\n return {\n async request<T>(method: string, params?: unknown): Promise<T> {\n if (closed) throw new Error('control connection is closed');\n const { promise } = send(method, params);\n const result = await withTimeout(promise, opts.requestTimeoutMs ?? 10_000, `control request \"${method}\" timed out`);\n return result as T;\n },\n subscribe(method, params, onEvent) {\n const { id, promise } = send(method, params, onEvent);\n promise.catch(() => {\n // subscribe()'s lifetime is managed via close()/onEvent, not this\n // promise — swallow so destroying the connection later (which\n // rejects every pending call, this one included) never surfaces as\n // an unhandled rejection.\n });\n return {\n close: (): void => {\n pending.delete(id);\n socket.destroy();\n },\n };\n },\n close(): void {\n socket.destroy();\n },\n };\n}\n\n/**\n * Unpair's own poll for \"has the daemon actually exited yet\" (see\n * `bin/commands/unpair.ts`): both the control token file being gone AND a\n * fresh connect attempt being refused, checked directly rather than through\n * a full `connectControlClient` handshake — cheaper per poll, and matches\n * the exact two-condition check the M4 design calls for.\n */\nexport async function isControlDaemonGone(storeDir: string, productId: string): Promise<boolean> {\n const tokenGone = await fs.stat(controlTokenPath(storeDir)).then(\n () => false,\n (err) => (err as NodeJS.ErrnoException).code === 'ENOENT',\n );\n if (!tokenGone) return false;\n\n const endpoint = controlEndpointPath(productId, storeDir);\n return new Promise<boolean>((resolve) => {\n const socket = net.createConnection(endpoint);\n const finish = (gone: boolean): void => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(gone);\n };\n socket.once('connect', () => finish(false));\n socket.once('error', (err: NodeJS.ErrnoException) => finish(err.code === 'ECONNREFUSED' || err.code === 'ENOENT'));\n });\n}\n","import { createInterface } from 'node:readline';\n\n/**\n * M4 Phase 3: the testable core of `byok-approval-mcp` (`byok-approval-mcp.ts`\n * is the thin stdio-wiring entry point — mirrors this repo's existing\n * `bin/commands/*.ts` split: real logic lives in a plain module, the bin\n * script itself is glue no test ever imports directly).\n *\n * `byok-approval-mcp` is the MCP stdio server `claude`'s own\n * `--permission-prompt-tool` spawns AS ITS OWN CHILD PROCESS when the claude\n * adapter runs a task under `PermissionPolicy.mode: 'confirm'` (see\n * `../adapters/claude/permission-mapping.ts`'s `confirm`-mode doc comment).\n * It implements just enough of the MCP stdio transport (JSON-RPC 2.0,\n * newline-delimited, per the spec) to expose ONE tool — empirically\n * confirmed end-to-end against the real installed claude 2.1.216 binary\n * (M4 Phase 3 STEP 0): `initialize` -> `notifications/initialized` ->\n * `tools/list` -> `tools/call`, with the tool's arguments shaped exactly\n * `{tool_name, input, tool_use_id}` (claude's own real wire shape,\n * live-captured) and its expected response shaped\n * `{content:[{type:'text', text: JSON.stringify({behavior:'allow',\n * updatedInput} | {behavior:'deny', message})}]}` — the SAME shape the\n * Claude Agent SDK's in-process `canUseTool` callback returns (see\n * platform.claude.com/docs/en/agent-sdk/user-input), just crossing a\n * process boundary via MCP instead of an in-process function call.\n */\n\nexport const APPROVAL_TOOL_NAME = 'approval_prompt';\n\n/** Bound on how much of a tool call's `input` gets folded into the wire `task.await_approval.summary` — mirrors `events.ts`'s `RESULT_DIAGNOSTIC_MAX_CHARS`/`truncateResultDiagnostic` convention: a human-facing summary, not a full audit record. */\nexport const APPROVAL_SUMMARY_MAX_CHARS = 500;\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** `${toolName}: ${input}`, bounded — the human-readable description carried into `task.await_approval.summary` and (via the daemon/CLI's own rendering) whatever a real approver actually reads before deciding. */\nexport function summarizeToolCall(toolName: string, input: unknown): string {\n let inputStr: string;\n try {\n inputStr = JSON.stringify(input);\n } catch (err) {\n inputStr = `<unserializable input: ${errorMessage(err)}>`;\n }\n const bounded =\n inputStr.length > APPROVAL_SUMMARY_MAX_CHARS ? `${inputStr.slice(0, APPROVAL_SUMMARY_MAX_CHARS)}… [truncated]` : inputStr;\n return `${toolName}: ${bounded}`;\n}\n\nexport interface ApprovalOutcome {\n approved: boolean;\n reason?: string;\n}\n\n/** What `byok-approval-mcp.ts` (the real entry point) injects — the one real dependency this module has on the outside world. */\nexport interface ApprovalMcpDeps {\n /** Requests a decision from the daemon this task is running on. Any rejection/throw here is treated as fail-closed (deny) by `handleMcpRequest` — never surfaced to claude as a raw protocol error. */\n requestApproval(taskId: string, summary: string): Promise<ApprovalOutcome>;\n}\n\ninterface JsonRpcRequestLike {\n jsonrpc?: unknown;\n id?: unknown;\n method?: unknown;\n params?: unknown;\n}\n\n/**\n * Handles exactly one already-parsed JSON-RPC request object and returns the\n * exact response object to write back (`undefined` for a notification that\n * expects no reply, e.g. `notifications/initialized`) — no stdio/process\n * concerns at all, so tests call this directly with a stub {@link\n * ApprovalMcpDeps} instead of spawning a real process or a real control\n * socket. `serveApprovalMcpOverStdio` below is the only caller in production.\n */\nexport async function handleMcpRequest(\n req: JsonRpcRequestLike,\n deps: ApprovalMcpDeps,\n taskId: string,\n): Promise<Record<string, unknown> | undefined> {\n const id = req.id;\n\n if (req.method === 'initialize') {\n const params = (req.params ?? {}) as { protocolVersion?: unknown };\n return {\n jsonrpc: '2.0',\n id,\n result: {\n protocolVersion: typeof params.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',\n capabilities: { tools: {} },\n serverInfo: { name: 'byok-approval-mcp', version: '0.0.1' },\n },\n };\n }\n\n if (req.method === 'notifications/initialized') {\n return undefined; // notification — no response\n }\n\n if (req.method === 'tools/list') {\n return {\n jsonrpc: '2.0',\n id,\n result: {\n tools: [\n {\n name: APPROVAL_TOOL_NAME,\n description:\n 'Requests operator approval for a pending tool call. Blocks until a human (or this device\\'s daemon) decides, or the configured timeout elapses (fail-closed deny on timeout).',\n inputSchema: {\n type: 'object',\n properties: {\n tool_name: { type: 'string' },\n input: { type: 'object' },\n },\n },\n },\n ],\n },\n };\n }\n\n if (req.method === 'tools/call') {\n const params = (req.params ?? {}) as { name?: unknown; arguments?: unknown };\n if (params.name !== APPROVAL_TOOL_NAME) {\n return { jsonrpc: '2.0', id, error: { code: -32602, message: `unknown tool \"${String(params.name)}\"` } };\n }\n const args = (params.arguments ?? {}) as { tool_name?: unknown; input?: unknown };\n const toolName = typeof args.tool_name === 'string' ? args.tool_name : 'unknown tool';\n const input = args.input ?? {};\n const summary = summarizeToolCall(toolName, input);\n\n let outcome: ApprovalOutcome;\n try {\n outcome = await deps.requestApproval(taskId, summary);\n } catch (err) {\n // Fail-closed (task's own mandate): a daemon that's unreachable, a\n // control request that times out, or any other failure reaching the\n // approving device must never leave claude's own MCP call unanswered\n // — that risks claude abandoning the whole turn on its own (M4 Phase 3\n // STEP 0 found claude gives up on a permission-prompt-tool call that\n // never answers at all, ~1.5s in) rather than cleanly denying just\n // this one tool call and letting the conversation continue.\n outcome = { approved: false, reason: `could not reach the approving device: ${errorMessage(err)}` };\n }\n\n const payload = outcome.approved\n ? { behavior: 'allow' as const, updatedInput: input }\n : { behavior: 'deny' as const, message: outcome.reason ?? 'denied' };\n return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(payload) }] } };\n }\n\n if (id !== undefined) {\n return { jsonrpc: '2.0', id, error: { code: -32601, message: `unknown method: ${String(req.method)}` } };\n }\n return undefined;\n}\n\nexport interface ServeApprovalMcpOptions {\n taskId: string;\n deps: ApprovalMcpDeps;\n /** Defaults to `process.stdin`/`process.stdout` — overridable so tests can drive this over in-memory streams. */\n input?: NodeJS.ReadableStream;\n output?: NodeJS.WritableStream;\n}\n\n/**\n * Wires {@link handleMcpRequest} to real NDJSON stdio (the MCP stdio\n * transport: one JSON-RPC message per line, both directions) — the only\n * genuinely process-shaped piece of this module. Each line is handled\n * independently and asynchronously (never serialized against the others),\n * since claude's own parallel-tool-use can legitimately fire more than one\n * concurrent `tools/call` over the same connection.\n */\nexport function serveApprovalMcpOverStdio(opts: ServeApprovalMcpOptions): void {\n const input = opts.input ?? process.stdin;\n const output = opts.output ?? process.stdout;\n const rl = createInterface({ input, terminal: false });\n\n rl.on('line', (line) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return; // a stray non-JSON line is not this server's concern\n }\n void handleMcpRequest(parsed as JsonRpcRequestLike, opts.deps, opts.taskId).then((response) => {\n if (response !== undefined) output.write(`${JSON.stringify(response)}\\n`);\n });\n });\n}\n","#!/usr/bin/env node\nimport { connectControlClient, type ControlClient } from './control-client';\nimport { serveApprovalMcpOverStdio, type ApprovalMcpDeps } from './approval-mcp-server';\n\n/**\n * `byok-approval-mcp`: the bin entry `claude`'s own `--permission-prompt-tool`\n * spawns as its child process under `PermissionPolicy.mode: 'confirm'` — see\n * `approval-mcp-server.ts`'s module doc comment for the full protocol/design\n * writeup and `../adapters/claude/permission-mapping.ts`'s `confirm`-mode\n * doc comment for the empirical basis. This file is intentionally thin (glue\n * only, never imported by a test — mirrors `byok-agent.ts`'s own split from\n * `bin/commands/*.ts`): read env, wire a real `ApprovalMcpDeps.requestApproval`\n * against the control socket, hand off to `serveApprovalMcpOverStdio`.\n *\n * Env vars (set by `../adapters/claude/claude-adapter.ts`'s `start()` via the\n * generated `--mcp-config`'s own `env` block — never read from the daemon's\n * ambient environment, since a DIFFERENT product/device's config must never\n * leak in):\n * BYOK_STORE_DIR — this daemon's control-socket storeDir.\n * BYOK_PRODUCT_ID — this daemon's productId.\n * BYOK_TASK_ID — the task this approval request belongs to.\n * BYOK_APPROVAL_TIMEOUT_MS — `RuntimeOperationStartInput.approvalChannel.timeoutMs`,\n * echoed here so this process's OWN control\n * request waits at least that long (plus\n * slop) rather than timing out earlier than\n * the daemon's own authoritative deadline.\n */\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Slop added on top of the daemon's own approval timeout for this process's OWN control-socket request timeout — the daemon's `TaskRunner.requestApproval` is the authoritative deadline (see its own doc comment); this must never fire first and race it. */\nconst REQUEST_TIMEOUT_SLOP_MS = 5_000;\n\n/**\n * Reads a required env var, or exits fatally. Declared to RETURN `string`\n * (never `string | undefined`) so every caller gets a real, closure-safe\n * `string` type structurally — TypeScript's control-flow narrowing from an\n * `if (!x) process.exit(1)` guard does NOT survive being captured by a\n * nested function/closure (`getClient` below), so relying on that narrowing\n * directly on `process.env.X` would silently widen back to `string |\n * undefined` inside any closure that reads it. `process.exit` is typed\n * `never`, so this function's own control flow is sound without an explicit\n * `else`/`throw`.\n */\nfunction requireEnv(name: string): string {\n const value = process.env[name];\n if (!value) {\n process.stderr.write(`byok-approval-mcp: missing required env var ${name} — refusing to start\\n`);\n process.exit(1);\n }\n return value;\n}\n\nasync function main(): Promise<void> {\n const storeDir = requireEnv('BYOK_STORE_DIR');\n const productId = requireEnv('BYOK_PRODUCT_ID');\n const taskId = requireEnv('BYOK_TASK_ID');\n const timeoutMs = Number(process.env.BYOK_APPROVAL_TIMEOUT_MS ?? '600000');\n\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n process.stderr.write('byok-approval-mcp: invalid BYOK_APPROVAL_TIMEOUT_MS env var — refusing to start\\n');\n process.exit(1);\n }\n\n // Connect lazily (on first tools/call, not at startup) and cache the\n // in-flight connection attempt itself (not just the resolved client) so\n // concurrent tools/call requests (claude's parallel tool use can fire more\n // than one) share ONE connection attempt rather than racing several.\n let clientPromise: Promise<ControlClient> | undefined;\n function getClient(): Promise<ControlClient> {\n if (!clientPromise) {\n clientPromise = connectControlClient({ storeDir, productId, requestTimeoutMs: timeoutMs + REQUEST_TIMEOUT_SLOP_MS }).then(\n (conn) => {\n if (!conn.ok) {\n clientPromise = undefined; // let the NEXT call retry a fresh connection rather than caching a permanent failure\n throw new Error(conn.reason);\n }\n return conn.client;\n },\n );\n clientPromise.catch(() => {\n clientPromise = undefined;\n });\n }\n return clientPromise;\n }\n\n const deps: ApprovalMcpDeps = {\n requestApproval: async (tId, summary) => {\n let client: ControlClient;\n try {\n client = await getClient();\n } catch (err) {\n throw new Error(`could not connect to the daemon control socket: ${errorMessage(err)}`);\n }\n try {\n return await client.request('approvals.request', { taskId: tId, summary });\n } catch (err) {\n // The connection may be genuinely broken (daemon restarted, socket\n // reset, etc.) — drop the cache so a LATER tools/call in this same\n // claude session gets a fresh connection instead of repeating the\n // same dead one forever. This call itself still fails closed via\n // handleMcpRequest's own catch.\n clientPromise = undefined;\n throw err;\n }\n },\n };\n\n serveApprovalMcpOverStdio({ taskId, deps });\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`byok-approval-mcp: fatal error: ${errorMessage(err)}\\n`);\n process.exit(1);\n});\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Root-bound filesystem authority used by Agent memory. Implementations must
3
+ * pin the exact leased Agent-home object, reject symlink/reparse traversal,
4
+ * and keep every operation relative to that pinned root.
5
+ */
6
+ export interface AgentMemoryFilesystem {
7
+ read(path: string, maxBytes: number): Promise<AgentMemoryFilesystemFileState>;
8
+ replace(path: string, expectedRevision: string, content: string, maxBytes: number): Promise<AgentMemoryFilesystemFileState>;
9
+ delete(path: string, expectedRevision: string): Promise<void>;
10
+ append(path: string, content: string, maxBytes: number): Promise<void>;
11
+ walk(path: string, maxEntries: number): Promise<readonly string[]>;
12
+ close(): Promise<void>;
13
+ }
14
+ export interface AgentMemoryFilesystemFileState {
15
+ readonly exists: boolean;
16
+ readonly content: string;
17
+ readonly revision: string;
18
+ readonly byteCount: number;
19
+ }
20
+ /** Product-owned deployment pointer. The SDK never searches PATH. */
21
+ export interface AgentMemoryFilesystemHelperConfig {
22
+ readonly helperBin: string;
23
+ }
@@ -0,0 +1,10 @@
1
+ import type { AgentHomeLease } from '../agent-home';
2
+ import type { AgentMemoryFilesystem } from './agent-memory-filesystem';
3
+ export declare const AGENT_MEMORY_FILESYSTEM_HELPER_PROTOCOL = 2;
4
+ export declare const AGENT_MEMORY_FILESYSTEM_HELPER_VERSION = "2";
5
+ export declare function isAgentMemoryFilesystemHelperSupported(): boolean;
6
+ export declare function openAgentMemoryFilesystemHelper(input: Readonly<{
7
+ helperBin: string;
8
+ canonicalHome: string;
9
+ homeIdentity: AgentHomeLease['homeIdentity'];
10
+ }>): Promise<AgentMemoryFilesystem>;
@@ -0,0 +1,160 @@
1
+ import { AGENT_MEMORY_PROJECTION_CAPABILITY, type AgentMemoryProjectionMutation } from '@byok-sdk/protocol';
2
+ import { type AgentHomeLease, type AgentRef } from '../agent-home';
3
+ import type { AgentMemoryFilesystem } from './agent-memory-filesystem';
4
+ export declare const AGENT_MEMORY_AUDIT_FILENAME = "agent-memory-audit-v1.jsonl";
5
+ /** v2 is one atomically replaced state file, never an append-only log. */
6
+ export declare const AGENT_MEMORY_OUTBOX_FILENAME = "agent-memory-redacted-outbox-v2.json";
7
+ export declare const AGENT_MEMORY_MAX_FILE_BYTES: number;
8
+ export declare const AGENT_MEMORY_MAX_SNAPSHOT_BYTES: number;
9
+ export declare const AGENT_MEMORY_MAX_SNAPSHOT_FILES = 128;
10
+ export declare const AGENT_MEMORY_MAX_SNAPSHOT_ENTRIES = 512;
11
+ /** One atomically replaced audit/outbox state must fit the cross-platform helper contract. */
12
+ export declare const AGENT_MEMORY_MAX_LOCAL_LOG_BYTES: number;
13
+ export declare class AgentMemoryError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ export declare class AgentMemoryRevisionConflictError extends AgentMemoryError {
17
+ readonly expectedRevision: string;
18
+ readonly actualRevision: string;
19
+ constructor(expectedRevision: string, actualRevision: string);
20
+ }
21
+ export interface AgentMemoryTaskContext {
22
+ readonly taskId: string;
23
+ readonly tenantId: string;
24
+ readonly deviceId: string;
25
+ readonly agentRef: AgentRef;
26
+ readonly sessionRef: string;
27
+ readonly runtimeId: string;
28
+ readonly canonicalHome: string;
29
+ readonly leaseId: string;
30
+ readonly homeIdentity: AgentHomeLease['homeIdentity'];
31
+ /** Optional task-scoped external root handle; omission selects the native Linux backend. */
32
+ readonly filesystem?: AgentMemoryFilesystem;
33
+ }
34
+ export interface AgentMemoryFile {
35
+ readonly path: string;
36
+ readonly revision: string;
37
+ readonly byteCount: number;
38
+ readonly content: string;
39
+ }
40
+ export interface AgentMemorySnapshot {
41
+ readonly files: readonly AgentMemoryFile[];
42
+ readonly totalBytes: number;
43
+ }
44
+ /**
45
+ * Embedder-owned redaction authority. Its output is opaque bytes: the local
46
+ * relative paths, source revisions, and raw source content never enter the
47
+ * outbox or projection port.
48
+ */
49
+ export interface AgentMemoryRedactor {
50
+ redact(input: Readonly<AgentMemorySnapshot>): Promise<Uint8Array> | Uint8Array;
51
+ }
52
+ /** Host-issued projection binding; no model-provided consent boolean exists. */
53
+ export interface AgentMemoryProjectionGrant {
54
+ readonly grantRef: AgentMemoryProjectionMutation['grantRef'];
55
+ readonly writerEpoch: AgentMemoryProjectionMutation['writerEpoch'];
56
+ readonly policyRevision: AgentMemoryProjectionMutation['policyRevision'];
57
+ }
58
+ export interface AgentMemoryRedactedOutboxRecord {
59
+ readonly version: 2;
60
+ readonly mutation: AgentMemoryProjectionMutation;
61
+ readonly createdAt: string;
62
+ }
63
+ /**
64
+ * Optional bridge to a complete hosted client. It receives the canonical,
65
+ * already-redacted protocol mutation only; this package intentionally owns no
66
+ * consent, cloud transport, or raw-source upload fallback.
67
+ */
68
+ export interface AgentMemoryProjectionPort {
69
+ publish(input: Readonly<{
70
+ mutation: AgentMemoryProjectionMutation;
71
+ }>): Promise<{
72
+ readonly accepted: boolean;
73
+ }>;
74
+ }
75
+ /**
76
+ * Replay exposes only ordering metadata. The redacted mutation body remains in
77
+ * the durable outbox and is never copied into a task-close error or outcome.
78
+ */
79
+ export type AgentMemoryProjectionReplayDrainedOutcome = Readonly<{
80
+ readonly status: 'drained';
81
+ }>;
82
+ export type AgentMemoryProjectionReplayPendingOutcome = Readonly<{
83
+ readonly status: 'pending';
84
+ readonly writerEpoch: number;
85
+ readonly sourceSeq: number;
86
+ readonly mutationId: string;
87
+ }>;
88
+ export type AgentMemoryProjectionReplayOutcome = AgentMemoryProjectionReplayDrainedOutcome | AgentMemoryProjectionReplayPendingOutcome;
89
+ export declare class AgentMemoryProjectionReplayPendingError extends AgentMemoryError {
90
+ readonly outcome: AgentMemoryProjectionReplayPendingOutcome;
91
+ constructor(outcome: AgentMemoryProjectionReplayPendingOutcome);
92
+ }
93
+ /** Every member is mandatory before network projection is permitted. */
94
+ export interface AgentMemoryHostedProjection {
95
+ readonly capability?: typeof AGENT_MEMORY_PROJECTION_CAPABILITY;
96
+ readonly grant?: AgentMemoryProjectionGrant;
97
+ readonly redactor?: AgentMemoryRedactor;
98
+ readonly port?: AgentMemoryProjectionPort;
99
+ }
100
+ /** Model input can name one allowed file, never a root, directory, glob, or internal SDK state. */
101
+ export declare function validateAgentMemoryPath(value: unknown): string;
102
+ /**
103
+ * The sole platform gate for the Agent-memory write authority. Linux retains
104
+ * the native procfs descriptor backend. macOS requires the explicit external
105
+ * helper that passed its local race proof. Windows remains unavailable rather
106
+ * than selecting an unproved fallback.
107
+ */
108
+ export declare function isAgentMemorySecureFilesystemAvailable(externalHelperConfigured?: boolean): boolean;
109
+ export interface AgentMemoryAuditWarning {
110
+ /** Metadata-only signal: the local source operation already succeeded. */
111
+ readonly code: 'agent_memory_audit_unavailable';
112
+ }
113
+ export interface AgentMemoryRecallResult {
114
+ readonly path: string;
115
+ readonly revision: string;
116
+ readonly content: string;
117
+ readonly auditWarning?: AgentMemoryAuditWarning;
118
+ }
119
+ export interface AgentMemorySaveResult {
120
+ readonly path: string;
121
+ readonly revision?: string;
122
+ readonly deleted: boolean;
123
+ readonly auditWarning?: AgentMemoryAuditWarning;
124
+ }
125
+ export declare class AgentMemoryService {
126
+ private readonly input;
127
+ constructor(input: AgentMemoryTaskContext);
128
+ recall(input: {
129
+ readonly path: unknown;
130
+ readonly ifRevision?: unknown;
131
+ }): Promise<Readonly<AgentMemoryRecallResult>>;
132
+ save(input: {
133
+ readonly op: unknown;
134
+ readonly path: unknown;
135
+ readonly expectedRevision: unknown;
136
+ readonly content?: unknown;
137
+ }): Promise<Readonly<AgentMemorySaveResult>>;
138
+ }
139
+ /** Bounded stable snapshot, called after Session.close() while the Agent lease still exists. */
140
+ export declare function captureAgentMemorySnapshot(input: AgentMemoryTaskContext): Promise<AgentMemorySnapshot>;
141
+ export declare class AgentMemoryRedactedOutbox {
142
+ private readonly context;
143
+ private readonly grant;
144
+ readonly filePath: string;
145
+ private state;
146
+ private fileRevision;
147
+ private writeTail;
148
+ private constructor();
149
+ static open(input: AgentMemoryTaskContext, grant: AgentMemoryProjectionGrant): Promise<AgentMemoryRedactedOutbox>;
150
+ pending(): readonly AgentMemoryRedactedOutboxRecord[];
151
+ append(bytes: Uint8Array): Promise<AgentMemoryRedactedOutboxRecord>;
152
+ replay(port: AgentMemoryProjectionPort): Promise<AgentMemoryProjectionReplayOutcome>;
153
+ private load;
154
+ private loadBody;
155
+ private admitGrant;
156
+ private persist;
157
+ private exclusive;
158
+ }
159
+ /** Missing capability, grant, redactor, or port has exactly zero network side effects. */
160
+ export declare function snapshotAndProjectAgentMemory(input: AgentMemoryTaskContext, projection: AgentMemoryHostedProjection | undefined): Promise<void>;