@byok-sdk/client 0.3.0 → 0.4.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.
- package/README.md +13 -0
- package/dist/adapters/claude/claude-adapter.d.ts +4 -20
- package/dist/adapters/claude/events.d.ts +3 -0
- package/dist/adapters/claude/process-client.d.ts +9 -1
- package/dist/adapters/codex/codex-adapter.d.ts +4 -16
- package/dist/adapters/codex/process-runner.d.ts +4 -1
- package/dist/adapters/index.d.ts +3 -1
- package/dist/adapters/index.js +923 -258
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -16
- package/dist/adapters/pi/rpc-client.d.ts +9 -1
- package/dist/adapters/process-tree.d.ts +19 -0
- package/dist/bin/audit-log.d.ts +12 -0
- package/dist/bin/byok-agent.js +1293 -484
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/workspaces.d.ts +11 -0
- package/dist/bin/format.d.ts +13 -0
- package/dist/bin/runtime-probe.d.ts +1 -1
- package/dist/bin/tasks-view.d.ts +13 -0
- package/dist/daemon/approvals.d.ts +2 -2
- package/dist/daemon/connection-manager.d.ts +4 -2
- package/dist/daemon/control-server.d.ts +18 -1
- package/dist/daemon/create-daemon.d.ts +2 -2
- package/dist/daemon/daemon-owner.d.ts +4 -2
- package/dist/daemon/environment.d.ts +9 -9
- package/dist/daemon/git-workspace.d.ts +21 -0
- package/dist/daemon/observer.d.ts +13 -0
- package/dist/daemon/presence-publisher.d.ts +29 -0
- package/dist/daemon/runtime-capabilities.d.ts +1 -1
- package/dist/daemon/task-runner.d.ts +27 -34
- package/dist/daemon/ws-transport.d.ts +3 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1254 -432
- package/dist/index.js.map +1 -1
- package/dist/runtime-failure.d.ts +64 -0
- package/dist/types.d.ts +100 -73
- package/package.json +4 -4
|
@@ -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';\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 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}\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 — `TaskContext.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';\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 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}\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,5 +1,16 @@
|
|
|
1
1
|
import type { DaemonConfig } from '../../index';
|
|
2
2
|
import { GitWorkspaceStore, type GitWorkspaceLedgerRecord } from '../../daemon/git-workspace-store';
|
|
3
|
+
/**
|
|
4
|
+
* Error categories stable enough to render from a ledger record — projected
|
|
5
|
+
* from `daemon/git-workspace.ts`'s `GIT_ERROR_CATEGORIES` (the single source
|
|
6
|
+
* of truth for the `GitErrorCategory` union) rather than carrying a literal
|
|
7
|
+
* copy that could drift from it; the runtime half of that guarantee is
|
|
8
|
+
* `__tests__/git-category-drift.test.ts`.
|
|
9
|
+
*
|
|
10
|
+
* @internal Exported for the drift-guard test only (never re-exported from
|
|
11
|
+
* `index.ts`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const STABLE_ERROR_CATEGORIES: Set<string>;
|
|
3
14
|
export interface WorkspacesCommandDeps {
|
|
4
15
|
log?: (line: string) => void;
|
|
5
16
|
store?: Pick<GitWorkspaceStore, 'list'>;
|
package/dist/bin/format.d.ts
CHANGED
|
@@ -3,6 +3,19 @@ import type { ConnectionState, DaemonBranding, DaemonEvent, DaemonTaskInfo } fro
|
|
|
3
3
|
import type { ControlStatusResult, PendingApproval } from '../daemon/control-protocol';
|
|
4
4
|
import type { ProbedRuntime } from './runtime-probe';
|
|
5
5
|
import type { TaskCounts, DerivedTaskInfo } from './tasks-view';
|
|
6
|
+
/**
|
|
7
|
+
* Git failures are rendered only from the closed stable category set —
|
|
8
|
+
* projected from `daemon/git-workspace.ts`'s `GIT_ERROR_CATEGORIES` (the
|
|
9
|
+
* single source of truth for the `GitErrorCategory` union) rather than
|
|
10
|
+
* carrying a literal copy that could drift from it; the runtime half of
|
|
11
|
+
* that guarantee is `__tests__/git-category-drift.test.ts`. Typed
|
|
12
|
+
* `Set<string>` (not `Set<GitErrorCategory>`) because what it filters is
|
|
13
|
+
* unvalidated ledger strings, never typed values.
|
|
14
|
+
*
|
|
15
|
+
* @internal Exported for the drift-guard test only (never re-exported from
|
|
16
|
+
* `index.ts`).
|
|
17
|
+
*/
|
|
18
|
+
export declare const STABLE_GIT_ERROR_CATEGORIES: Set<string>;
|
|
6
19
|
/** Renders one `task.progress`-derived `AgentEvent` compactly — the inner payload of a `progress`-kind `DaemonEvent`. */
|
|
7
20
|
export declare function formatAgentEvent(event: AgentEvent): string;
|
|
8
21
|
export interface FormatDaemonEventLineOptions {
|
|
@@ -21,7 +21,7 @@ export interface ProbedRuntime {
|
|
|
21
21
|
permissionModes: string[];
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Runs `detect()
|
|
24
|
+
* Runs `detect()` and reads each adapter's frozen descriptor, in parallel. Every
|
|
25
25
|
* bundled adapter's own `detect()` already catches its own failures (e.g.
|
|
26
26
|
* `pi-adapter.ts`'s `detect()` wraps its version probe in try/catch and
|
|
27
27
|
* resolves `{present: false}` rather than rejecting) — the catch here is a
|
package/dist/bin/tasks-view.d.ts
CHANGED
|
@@ -17,6 +17,19 @@ export interface TaskGitStatus {
|
|
|
17
17
|
export type DerivedTaskInfo = DaemonTaskInfo & {
|
|
18
18
|
git?: TaskGitStatus;
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Both stable-set filters below are projected from `daemon/git-workspace.ts`'s
|
|
22
|
+
* `GIT_WORKSPACE_PHASES`/`GIT_ERROR_CATEGORIES` (the single sources of truth
|
|
23
|
+
* for the `GitWorkspacePhase`/`GitErrorCategory` unions) rather than carrying
|
|
24
|
+
* literal copies that could drift from them; the runtime half of that
|
|
25
|
+
* guarantee is `__tests__/git-category-drift.test.ts`. Same projection-of-
|
|
26
|
+
* union idiom as `TASK_STATES` from `@byok-sdk/protocol` above.
|
|
27
|
+
*
|
|
28
|
+
* @internal Exported for the drift-guard test only (never re-exported from
|
|
29
|
+
* `index.ts`).
|
|
30
|
+
*/
|
|
31
|
+
export declare const STABLE_GIT_PHASES: Set<string>;
|
|
32
|
+
export declare const STABLE_GIT_ERROR_CATEGORIES: Set<string>;
|
|
20
33
|
/**
|
|
21
34
|
* Reconstructs the same shape `DaemonObserver.tasks()` exposes to a LIVE
|
|
22
35
|
* in-process daemon, but from a REPLAYED sequence of already-logged
|
|
@@ -16,7 +16,7 @@ export type ApprovalDecision = 'approve' | 'reject';
|
|
|
16
16
|
/**
|
|
17
17
|
* M4 (additive-minor, `task.approval_resolved`): distinguishes a resolution
|
|
18
18
|
* that arrived over the wire (a server-sent `task.approve`/`task.reject`,
|
|
19
|
-
* relayed here via `
|
|
19
|
+
* relayed here via `RuntimeOperationStartInput.approvalChannel.resolve` — `task-runner.ts`'s
|
|
20
20
|
* `handleOffer`) from one this device decided on its own (the local
|
|
21
21
|
* `approvals.resolve` control-socket RPC, a fail-closed `requestApproval`
|
|
22
22
|
* timeout, or a fail-closed finish/eviction rejection). `TaskRunner` uses
|
|
@@ -79,7 +79,7 @@ export declare class ApprovalRegistry {
|
|
|
79
79
|
* `requestApproval` timeout and `finish()` fail-closed cleanup
|
|
80
80
|
* (`task-runner.ts`) — resolves a decision this device made on its own.
|
|
81
81
|
* The one exception, a server-sent wire `task.approve`/`task.reject`
|
|
82
|
-
* relayed through `
|
|
82
|
+
* relayed through `RuntimeOperationStartInput.approvalChannel.resolve`
|
|
83
83
|
* (`task-runner.ts`'s `handleOffer`), passes `'wire'` explicitly.
|
|
84
84
|
*/
|
|
85
85
|
resolve(approvalId: string, decision: ApprovalDecision, reason?: string, origin?: ApprovalOrigin): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CapabilityFlag, type Envelope, type RuntimeInfo } from '@byok-sdk/protocol';
|
|
1
|
+
import { type CapabilityFlag, type Envelope, type RuntimeInfo, type ToolsetId } from '@byok-sdk/protocol';
|
|
2
2
|
import { AuthManager } from './auth-manager';
|
|
3
3
|
import type { CursorStore } from './cursor-store';
|
|
4
4
|
import { type FleetJitter } from './deterministic-jitter';
|
|
@@ -10,6 +10,8 @@ export interface ConnectionManagerOptions {
|
|
|
10
10
|
productId: string;
|
|
11
11
|
capabilities: CapabilityFlag[];
|
|
12
12
|
runtimes: RuntimeInfo[];
|
|
13
|
+
/** Sorted logical IDs from the validated local registry; executable definitions stay local. */
|
|
14
|
+
configuredToolsets?: readonly ToolsetId[];
|
|
13
15
|
auth: AuthManager;
|
|
14
16
|
cursorStore: CursorStore;
|
|
15
17
|
/**
|
|
@@ -126,7 +128,7 @@ export declare class ConnectionManager {
|
|
|
126
128
|
* already-delivered seqs (see its own doc comment) so the failed seq's own
|
|
127
129
|
* redelivery can get through — but that same frozen watermark also means
|
|
128
130
|
* every OTHER seq above it rides along on every re-poll too. Without this,
|
|
129
|
-
* a seq already mid-flight (e.g. a `task.offer` whose
|
|
131
|
+
* a seq already mid-flight (e.g. a `task.offer` whose prepared operation start()
|
|
130
132
|
* hasn't resolved yet) would be re-enqueued into `processingChain` on
|
|
131
133
|
* every such re-poll, piling up duplicate copies that — once the first
|
|
132
134
|
* finally resolves and the chain unwinds through them — run its handler
|
|
@@ -38,7 +38,24 @@ export interface ControlServerOptions {
|
|
|
38
38
|
export interface ControlServerHandle {
|
|
39
39
|
/** The bound Unix socket path or Windows pipe name. */
|
|
40
40
|
endpoint: string;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Stops accepting new connections and destroys every open one, WITHOUT
|
|
43
|
+
* removing the socket/token files. Idempotent, and the first half of
|
|
44
|
+
* {@link close}.
|
|
45
|
+
*
|
|
46
|
+
* Split out from `close()` for `create-daemon.ts`'s shutdown sequence
|
|
47
|
+
* (plan `shutdown-lease-order`): the socket/token files ARE this daemon's
|
|
48
|
+
* only external liveness signal (`isControlDaemonGone` in
|
|
49
|
+
* `bin/control-client.ts` reads "token file gone AND a connect refused"),
|
|
50
|
+
* so a shutdown must be able to stop serving RPCs, release its
|
|
51
|
+
* store-mutation lease, and only THEN publish "gone" by removing the
|
|
52
|
+
* files. Between the two stages a connect is already refused while the
|
|
53
|
+
* token file still exists — which reads as "still running", the
|
|
54
|
+
* conservative answer, rather than as "exited while still holding the
|
|
55
|
+
* lease".
|
|
56
|
+
*/
|
|
57
|
+
stopServing(): Promise<void>;
|
|
58
|
+
/** Runs {@link stopServing}, then removes the socket/token files (a Windows pipe leaves no file to remove). Idempotent. */
|
|
42
59
|
close(): Promise<void>;
|
|
43
60
|
}
|
|
44
61
|
/**
|
|
@@ -116,7 +116,7 @@ export interface DaemonConfig {
|
|
|
116
116
|
* `TaskRunner.pickAdapter`'s no-explicit-runtime branch (`task-runner.ts`)
|
|
117
117
|
* — tried in listed order; the first candidate that is both PRESENT
|
|
118
118
|
* (`adapter.detect()`) and CAPABLE (declares the offer's
|
|
119
|
-
* `PermissionPolicy.mode` in its own `capabilities
|
|
119
|
+
* `PermissionPolicy.mode` in its own `descriptor.capabilities.permissionModes` —
|
|
120
120
|
* see `adapterSupportsMode`) wins. Unset defaults to
|
|
121
121
|
* `DEFAULT_RUNTIME_PREFERENCE` (`task-runner.ts`): `['claude', 'codex',
|
|
122
122
|
* 'pi']` — pi LAST, deliberately.
|
|
@@ -176,7 +176,7 @@ export interface DaemonConfig {
|
|
|
176
176
|
* an id with no matching adapter is simply never looked up). `allow`
|
|
177
177
|
* entries are exact variable names or `*`-suffixed prefixes, merged in
|
|
178
178
|
* alongside that runtime adapter's own declared
|
|
179
|
-
* `environmentRequirements
|
|
179
|
+
* `descriptor.environmentRequirements` — this can never override the hard
|
|
180
180
|
* `BYOK_*` deny (see `environment.ts`'s own doc comment).
|
|
181
181
|
*/
|
|
182
182
|
runtimeEnvironment?: Record<string, {
|
|
@@ -27,8 +27,10 @@ export interface DaemonOwnerLease {
|
|
|
27
27
|
* nested one level deep so that directory can be created 0700 BEFORE anything
|
|
28
28
|
* binds inside it (the nesting convention is `controlSocketPath`'s).
|
|
29
29
|
*
|
|
30
|
-
* `controlSocketPath`
|
|
31
|
-
*
|
|
30
|
+
* `controlSocketPath`'s fallback now binds under its own FIXED `/tmp` root
|
|
31
|
+
* (`control-protocol.ts`'s `CONTROL_SOCKET_FALLBACK_ROOT` — not
|
|
32
|
+
* `os.tmpdir()`); this mutex must not regress to `os.tmpdir()` for the same
|
|
33
|
+
* two independent reasons proven by CI job 94334133652. Correctness:
|
|
32
34
|
* `os.tmpdir()` is environment-derived, and a lock address that differs
|
|
33
35
|
* between two contending processes admits two writers. Reachability: the
|
|
34
36
|
* caller may have pointed `TMPDIR` INSIDE the very tree that made the natural
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* M5: per-runtime environment allowlist for spawned agent child processes.
|
|
3
3
|
*
|
|
4
4
|
* Before this module existed, `task-runner.ts` built every task's
|
|
5
|
-
* `
|
|
5
|
+
* `RuntimeOperationStartInput.env` as `process.env` verbatim — the daemon's OWN full
|
|
6
6
|
* environment, unfiltered, handed to whichever runtime CLI (`pi`/`claude`/
|
|
7
7
|
* `codex`) `pickAdapter` selected. Any credential-shaped variable sitting in
|
|
8
8
|
* the daemon's own environment for a completely unrelated reason (an
|
|
@@ -20,10 +20,9 @@
|
|
|
20
20
|
* behave sanely in a non-interactive shell.
|
|
21
21
|
* 2. Whatever ADDITIONAL names the *specific* runtime adapter about to be
|
|
22
22
|
* spawned declares it actually needs
|
|
23
|
-
* (`RuntimeAdapter.environmentRequirements
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* not by an extra check here.
|
|
23
|
+
* (`RuntimeAdapter.descriptor.environmentRequirements` — see
|
|
24
|
+
* `../types.ts`). A descriptor that declares no names gets the platform
|
|
25
|
+
* baseline only; descriptors are required and frozen before claim.
|
|
27
26
|
* 3. A per-device, per-runtime operator override (`DaemonConfig
|
|
28
27
|
* .runtimeEnvironment` — see `create-daemon.ts`) — a local escape hatch
|
|
29
28
|
* for a product/operator that knows it needs one more variable forwarded
|
|
@@ -39,8 +38,8 @@
|
|
|
39
38
|
*/
|
|
40
39
|
/**
|
|
41
40
|
* What one runtime adapter declares it needs beyond the always-included
|
|
42
|
-
* platform baseline.
|
|
43
|
-
* `RuntimeAdapter.environmentRequirements
|
|
41
|
+
* platform baseline. Declared in the required frozen
|
|
42
|
+
* `RuntimeAdapter.descriptor.environmentRequirements` (`../types.ts`).
|
|
44
43
|
*/
|
|
45
44
|
export interface RuntimeEnvironmentRequirements {
|
|
46
45
|
/**
|
|
@@ -69,8 +68,9 @@ export interface BuildRuntimeEnvOptions {
|
|
|
69
68
|
ambient: NodeJS.ProcessEnv;
|
|
70
69
|
/**
|
|
71
70
|
* The selected runtime adapter's own declared requirements —
|
|
72
|
-
* `undefined`
|
|
73
|
-
*
|
|
71
|
+
* `undefined` means "platform baseline only" for this helper. The public
|
|
72
|
+
* RuntimeAdapter descriptor always supplies this object before TaskRunner
|
|
73
|
+
* invokes the helper.
|
|
74
74
|
*/
|
|
75
75
|
requirements?: RuntimeEnvironmentRequirements;
|
|
76
76
|
/**
|