@byok-sdk/client 0.1.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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/adapters/claude/claude-adapter.d.ts +155 -0
  4. package/dist/adapters/claude/events.d.ts +91 -0
  5. package/dist/adapters/claude/permission-mapping.d.ts +142 -0
  6. package/dist/adapters/claude/process-client.d.ts +103 -0
  7. package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +45 -0
  8. package/dist/adapters/claude/resolve-bin.d.ts +27 -0
  9. package/dist/adapters/codex/codex-adapter.d.ts +97 -0
  10. package/dist/adapters/codex/events.d.ts +41 -0
  11. package/dist/adapters/codex/permission-mapping.d.ts +107 -0
  12. package/dist/adapters/codex/process-runner.d.ts +86 -0
  13. package/dist/adapters/codex/resolve-bin.d.ts +25 -0
  14. package/dist/adapters/pi/events.d.ts +58 -0
  15. package/dist/adapters/pi/permission-mapping.d.ts +46 -0
  16. package/dist/adapters/pi/pi-adapter.d.ts +29 -0
  17. package/dist/adapters/pi/resolve-bin.d.ts +44 -0
  18. package/dist/adapters/pi/rpc-client.d.ts +101 -0
  19. package/dist/bin/approval-mcp-server.d.ts +69 -0
  20. package/dist/bin/audit-log.d.ts +212 -0
  21. package/dist/bin/byok-agent.d.ts +2 -0
  22. package/dist/bin/byok-agent.js +12347 -0
  23. package/dist/bin/byok-agent.js.map +1 -0
  24. package/dist/bin/byok-approval-mcp.d.ts +2 -0
  25. package/dist/bin/byok-approval-mcp.js +498 -0
  26. package/dist/bin/byok-approval-mcp.js.map +1 -0
  27. package/dist/bin/commands/approvals.d.ts +31 -0
  28. package/dist/bin/commands/approve-reject.d.ts +29 -0
  29. package/dist/bin/commands/doctor.d.ts +15 -0
  30. package/dist/bin/commands/pair.d.ts +7 -0
  31. package/dist/bin/commands/runtimes.d.ts +7 -0
  32. package/dist/bin/commands/service.d.ts +54 -0
  33. package/dist/bin/commands/start.d.ts +21 -0
  34. package/dist/bin/commands/status.d.ts +25 -0
  35. package/dist/bin/commands/support-bundle.d.ts +7 -0
  36. package/dist/bin/commands/tasks.d.ts +27 -0
  37. package/dist/bin/commands/unpair.d.ts +124 -0
  38. package/dist/bin/commands/workspaces.d.ts +16 -0
  39. package/dist/bin/config.d.ts +41 -0
  40. package/dist/bin/control-client.d.ts +50 -0
  41. package/dist/bin/format.d.ts +67 -0
  42. package/dist/bin/runtime-probe.d.ts +34 -0
  43. package/dist/bin/tasks-view.d.ts +49 -0
  44. package/dist/daemon/approvals.d.ts +87 -0
  45. package/dist/daemon/auth-manager.d.ts +52 -0
  46. package/dist/daemon/blob-client.d.ts +22 -0
  47. package/dist/daemon/connection-manager.d.ts +502 -0
  48. package/dist/daemon/control-protocol.d.ts +299 -0
  49. package/dist/daemon/control-server.d.ts +61 -0
  50. package/dist/daemon/create-daemon.d.ts +364 -0
  51. package/dist/daemon/cursor-store.d.ts +30 -0
  52. package/dist/daemon/daemon-owner.d.ts +24 -0
  53. package/dist/daemon/deterministic-jitter.d.ts +20 -0
  54. package/dist/daemon/device-keys.d.ts +45 -0
  55. package/dist/daemon/device-proof-signer.d.ts +41 -0
  56. package/dist/daemon/environment.d.ts +96 -0
  57. package/dist/daemon/git-workspace-store.d.ts +49 -0
  58. package/dist/daemon/git-workspace.d.ts +80 -0
  59. package/dist/daemon/http-client.d.ts +8 -0
  60. package/dist/daemon/journal/journal.d.ts +342 -0
  61. package/dist/daemon/journal/sqlite-journal.d.ts +173 -0
  62. package/dist/daemon/journal/sqlite-support.d.ts +96 -0
  63. package/dist/daemon/journal/storage-policy.d.ts +368 -0
  64. package/dist/daemon/long-poll-transport.d.ts +142 -0
  65. package/dist/daemon/observer.d.ts +345 -0
  66. package/dist/daemon/operational-health.d.ts +87 -0
  67. package/dist/daemon/policy.d.ts +16 -0
  68. package/dist/daemon/progress-batcher.d.ts +31 -0
  69. package/dist/daemon/runtime-capabilities.d.ts +36 -0
  70. package/dist/daemon/session-workspace-store.d.ts +75 -0
  71. package/dist/daemon/store.d.ts +54 -0
  72. package/dist/daemon/task-runner.d.ts +862 -0
  73. package/dist/daemon/truth-memory-client.d.ts +94 -0
  74. package/dist/daemon/url.d.ts +60 -0
  75. package/dist/daemon/ws-transport.d.ts +110 -0
  76. package/dist/diagnostics/diagnostics.d.ts +114 -0
  77. package/dist/diagnostics/support-bundle.d.ts +81 -0
  78. package/dist/index.d.ts +42 -0
  79. package/dist/index.js +10056 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/lifecycle/create-service-lifecycle.d.ts +59 -0
  82. package/dist/lifecycle/exec-runner.d.ts +141 -0
  83. package/dist/lifecycle/launchd.d.ts +55 -0
  84. package/dist/lifecycle/service-types.d.ts +177 -0
  85. package/dist/lifecycle/systemd.d.ts +46 -0
  86. package/dist/lifecycle/winsw.d.ts +64 -0
  87. package/dist/types.d.ts +178 -0
  88. package/dist/util/async-queue.d.ts +45 -0
  89. package/dist/util/atomic-write.d.ts +86 -0
  90. package/dist/util/secure-dir.d.ts +124 -0
  91. package/package.json +66 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env node
2
+ import { promises, constants } from 'fs';
3
+ import net from 'net';
4
+ import { randomBytes, createHash, timingSafeEqual, createHmac } from 'crypto';
5
+ import os from 'os';
6
+ import path from 'path';
7
+ import { createInterface } from 'readline';
8
+
9
+ var CONTROL_PROTOCOL_VERSION = 1;
10
+ var HANDSHAKE_TIMEOUT_MS = 3e3;
11
+ var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
12
+ function shortHash(input) {
13
+ return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
14
+ }
15
+ function controlSocketPath(storeDir) {
16
+ const candidate = path.join(storeDir, "control.sock");
17
+ if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
18
+ return path.join(os.tmpdir(), `byok-${shortHash(storeDir)}`, "sock");
19
+ }
20
+ function controlPipeName(productId, storeDir) {
21
+ const id = shortHash(`${productId}|${path.resolve(storeDir)}`);
22
+ return `\\\\.\\pipe\\byok-${id}`;
23
+ }
24
+ function controlEndpointPath(productId, storeDir, platform = process.platform) {
25
+ return platform === "win32" ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);
26
+ }
27
+ function controlTokenPath(storeDir) {
28
+ return path.join(storeDir, "control.token");
29
+ }
30
+ var SERVER_PROOF_LABEL = "byok-control-server|";
31
+ var CLIENT_AUTH_LABEL = "byok-control-client|";
32
+ function randomNonceHex() {
33
+ return randomBytes(32).toString("hex");
34
+ }
35
+ function hmacHex(token, message) {
36
+ return createHmac("sha256", token).update(message, "utf8").digest("hex");
37
+ }
38
+ function computeServerProof(token, clientNonce) {
39
+ return hmacHex(token, SERVER_PROOF_LABEL + clientNonce);
40
+ }
41
+ function computeClientAuth(token, serverNonce) {
42
+ return hmacHex(token, CLIENT_AUTH_LABEL + serverNonce);
43
+ }
44
+ function timingSafeEqualHex(a, b) {
45
+ const bufA = Buffer.from(a, "hex");
46
+ const bufB = Buffer.from(b, "hex");
47
+ if (bufA.length !== bufB.length) return false;
48
+ return timingSafeEqual(bufA, bufB);
49
+ }
50
+ function isRecord(value) {
51
+ return typeof value === "object" && value !== null;
52
+ }
53
+ function parseServerHello(value) {
54
+ if (!isRecord(value)) return void 0;
55
+ if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "server" || typeof value.proof !== "string" || typeof value.nonce !== "string") {
56
+ return void 0;
57
+ }
58
+ return { v: CONTROL_PROTOCOL_VERSION, hello: "server", proof: value.proof, nonce: value.nonce };
59
+ }
60
+ function parseServerReady(value) {
61
+ if (!isRecord(value)) return void 0;
62
+ if (value.v !== CONTROL_PROTOCOL_VERSION || value.ready !== true) return void 0;
63
+ return { v: CONTROL_PROTOCOL_VERSION, ready: true };
64
+ }
65
+ function encodeFrame(frame) {
66
+ return `${JSON.stringify(frame)}
67
+ `;
68
+ }
69
+ var ControlError = class extends Error {
70
+ constructor(code, message) {
71
+ super(message);
72
+ this.code = code;
73
+ this.name = "ControlError";
74
+ }
75
+ code;
76
+ };
77
+ var MAX_LINE_BYTES = 64 * 1024;
78
+ var NdjsonLineReader = class {
79
+ pending = Buffer.alloc(0);
80
+ /** @throws if the still-unterminated remainder exceeds {@link MAX_LINE_BYTES} — see that constant's own doc comment. */
81
+ push(chunk) {
82
+ this.pending = this.pending.length > 0 ? Buffer.concat([this.pending, chunk]) : chunk;
83
+ const lines = [];
84
+ let newlineIndex;
85
+ while ((newlineIndex = this.pending.indexOf(10)) !== -1) {
86
+ const line = this.pending.subarray(0, newlineIndex).toString("utf8");
87
+ this.pending = this.pending.subarray(newlineIndex + 1);
88
+ if (line.length > 0) lines.push(line);
89
+ }
90
+ if (this.pending.length > MAX_LINE_BYTES) {
91
+ throw new Error(`NDJSON line exceeded ${MAX_LINE_BYTES} bytes without a terminating newline`);
92
+ }
93
+ return lines;
94
+ }
95
+ };
96
+
97
+ // src/bin/control-client.ts
98
+ var MAX_CONTROL_TOKEN_BYTES = 256;
99
+ function errorMessage(err) {
100
+ return err instanceof Error ? err.message : String(err);
101
+ }
102
+ function sameFileState(left, right) {
103
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
104
+ }
105
+ async function readControlToken(tokenPath) {
106
+ let namedBefore;
107
+ try {
108
+ namedBefore = await promises.lstat(tokenPath, { bigint: true });
109
+ } catch (err) {
110
+ if (err.code === "ENOENT") return void 0;
111
+ throw err;
112
+ }
113
+ if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
114
+ throw new Error("control token is not a real regular file");
115
+ }
116
+ const handle = await promises.open(
117
+ tokenPath,
118
+ constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
119
+ );
120
+ try {
121
+ const opened = await handle.stat({ bigint: true });
122
+ const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
123
+ if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState(namedBefore, opened) || !sameFileState(opened, namedAfterOpen)) {
124
+ throw new Error("control token pathname changed before safe open");
125
+ }
126
+ if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
127
+ throw new Error("control token exceeds the bounded read limit");
128
+ }
129
+ const size = Number(opened.size);
130
+ const bytes = Buffer.alloc(size);
131
+ const { bytesRead } = await handle.read(bytes, 0, size, 0);
132
+ const afterRead = await handle.stat({ bigint: true });
133
+ const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
134
+ if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState(opened, afterRead) || !sameFileState(afterRead, namedAfterRead)) {
135
+ throw new Error("control token changed during bounded read");
136
+ }
137
+ return bytes.toString("utf8").trim();
138
+ } finally {
139
+ await handle.close();
140
+ }
141
+ }
142
+ async function connectControlClient(opts) {
143
+ const tokenPath = controlTokenPath(opts.storeDir);
144
+ let token;
145
+ try {
146
+ const read = await readControlToken(tokenPath);
147
+ if (read === void 0) {
148
+ return { ok: false, reason: "daemon is not running (no control.token found)" };
149
+ }
150
+ token = read;
151
+ } catch (err) {
152
+ return { ok: false, reason: `could not read the control token: ${errorMessage(err)}` };
153
+ }
154
+ if (!token) {
155
+ return { ok: false, reason: "control token file is empty" };
156
+ }
157
+ const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
158
+ try {
159
+ const client = await connectAndHandshake(endpoint, token, opts);
160
+ return { ok: true, client };
161
+ } catch (err) {
162
+ return { ok: false, reason: `daemon control socket not reachable: ${errorMessage(err)}` };
163
+ }
164
+ }
165
+ function connectAndHandshake(endpoint, token, opts) {
166
+ return new Promise((resolve, reject) => {
167
+ const socket = net.createConnection(endpoint);
168
+ const reader = new NdjsonLineReader();
169
+ let phase = "server-hello";
170
+ let settled = false;
171
+ const clientNonce = randomNonceHex();
172
+ const timer = setTimeout(() => {
173
+ fail(new Error("handshake timed out"));
174
+ }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
175
+ timer.unref?.();
176
+ function fail(err) {
177
+ if (settled) return;
178
+ settled = true;
179
+ clearTimeout(timer);
180
+ socket.removeAllListeners();
181
+ socket.destroy();
182
+ reject(err instanceof Error ? err : new Error(String(err)));
183
+ }
184
+ function succeed() {
185
+ settled = true;
186
+ clearTimeout(timer);
187
+ socket.removeListener("error", onError);
188
+ socket.removeListener("data", onData);
189
+ resolve(createControlClient(socket, reader, opts));
190
+ }
191
+ function onData(chunk) {
192
+ let lines;
193
+ try {
194
+ lines = reader.push(chunk);
195
+ } catch (err) {
196
+ fail(err);
197
+ return;
198
+ }
199
+ for (const line of lines) {
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(line);
203
+ } catch {
204
+ fail(new Error("malformed handshake frame"));
205
+ return;
206
+ }
207
+ if (phase === "server-hello") {
208
+ const hello = parseServerHello(parsed);
209
+ if (!hello) {
210
+ fail(new Error("malformed or unexpected server hello"));
211
+ return;
212
+ }
213
+ if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
214
+ fail(new Error("server failed to prove it holds the control token"));
215
+ return;
216
+ }
217
+ socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
218
+ phase = "ready";
219
+ continue;
220
+ }
221
+ if (!parseServerReady(parsed)) {
222
+ fail(new Error("server did not confirm readiness"));
223
+ return;
224
+ }
225
+ succeed();
226
+ return;
227
+ }
228
+ }
229
+ function onError(err) {
230
+ fail(err);
231
+ }
232
+ socket.once("error", onError);
233
+ socket.once("connect", () => {
234
+ socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
235
+ socket.on("data", onData);
236
+ });
237
+ });
238
+ }
239
+ function withTimeout(promise, ms, message) {
240
+ return new Promise((resolve, reject) => {
241
+ const timer = setTimeout(() => reject(new Error(message)), ms);
242
+ timer.unref?.();
243
+ promise.then(
244
+ (value) => {
245
+ clearTimeout(timer);
246
+ resolve(value);
247
+ },
248
+ (err) => {
249
+ clearTimeout(timer);
250
+ reject(err);
251
+ }
252
+ );
253
+ });
254
+ }
255
+ function createControlClient(socket, reader, opts) {
256
+ const pending = /* @__PURE__ */ new Map();
257
+ let idSeq = 0;
258
+ let closed = false;
259
+ function handleFrame(parsed) {
260
+ if (!isRecord(parsed) || typeof parsed.id !== "string") return;
261
+ const entry = pending.get(parsed.id);
262
+ if (!entry) return;
263
+ if ("event" in parsed) {
264
+ entry.onEvent?.(parsed.event);
265
+ return;
266
+ }
267
+ if (parsed.ok === true) {
268
+ pending.delete(parsed.id);
269
+ entry.resolve(parsed.done === true ? void 0 : parsed.result);
270
+ return;
271
+ }
272
+ pending.delete(parsed.id);
273
+ const shape = parsed.error;
274
+ entry.reject(
275
+ new ControlError(
276
+ typeof shape?.code === "string" ? shape.code : "internal_error",
277
+ typeof shape?.message === "string" ? shape.message : "unknown control error"
278
+ )
279
+ );
280
+ }
281
+ socket.on("data", (chunk) => {
282
+ let lines;
283
+ try {
284
+ lines = reader.push(chunk);
285
+ } catch {
286
+ socket.destroy();
287
+ return;
288
+ }
289
+ for (const line of lines) {
290
+ let parsed;
291
+ try {
292
+ parsed = JSON.parse(line);
293
+ } catch {
294
+ continue;
295
+ }
296
+ handleFrame(parsed);
297
+ }
298
+ });
299
+ socket.on("close", () => {
300
+ closed = true;
301
+ for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
302
+ pending.clear();
303
+ });
304
+ socket.on("error", () => {
305
+ });
306
+ function send(method, params, onEvent) {
307
+ const id = `c${++idSeq}`;
308
+ const promise = new Promise((resolve, reject) => {
309
+ pending.set(id, { resolve, reject, onEvent });
310
+ });
311
+ socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
312
+ return { id, promise };
313
+ }
314
+ return {
315
+ async request(method, params) {
316
+ if (closed) throw new Error("control connection is closed");
317
+ const { promise } = send(method, params);
318
+ const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
319
+ return result;
320
+ },
321
+ subscribe(method, params, onEvent) {
322
+ const { id, promise } = send(method, params, onEvent);
323
+ promise.catch(() => {
324
+ });
325
+ return {
326
+ close: () => {
327
+ pending.delete(id);
328
+ socket.destroy();
329
+ }
330
+ };
331
+ },
332
+ close() {
333
+ socket.destroy();
334
+ }
335
+ };
336
+ }
337
+ var APPROVAL_TOOL_NAME = "approval_prompt";
338
+ var APPROVAL_SUMMARY_MAX_CHARS = 500;
339
+ function errorMessage2(err) {
340
+ return err instanceof Error ? err.message : String(err);
341
+ }
342
+ function summarizeToolCall(toolName, input) {
343
+ let inputStr;
344
+ try {
345
+ inputStr = JSON.stringify(input);
346
+ } catch (err) {
347
+ inputStr = `<unserializable input: ${errorMessage2(err)}>`;
348
+ }
349
+ const bounded = inputStr.length > APPROVAL_SUMMARY_MAX_CHARS ? `${inputStr.slice(0, APPROVAL_SUMMARY_MAX_CHARS)}\u2026 [truncated]` : inputStr;
350
+ return `${toolName}: ${bounded}`;
351
+ }
352
+ async function handleMcpRequest(req, deps, taskId) {
353
+ const id = req.id;
354
+ if (req.method === "initialize") {
355
+ const params = req.params ?? {};
356
+ return {
357
+ jsonrpc: "2.0",
358
+ id,
359
+ result: {
360
+ protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : "2024-11-05",
361
+ capabilities: { tools: {} },
362
+ serverInfo: { name: "byok-approval-mcp", version: "0.0.1" }
363
+ }
364
+ };
365
+ }
366
+ if (req.method === "notifications/initialized") {
367
+ return void 0;
368
+ }
369
+ if (req.method === "tools/list") {
370
+ return {
371
+ jsonrpc: "2.0",
372
+ id,
373
+ result: {
374
+ tools: [
375
+ {
376
+ name: APPROVAL_TOOL_NAME,
377
+ description: "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).",
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ tool_name: { type: "string" },
382
+ input: { type: "object" }
383
+ }
384
+ }
385
+ }
386
+ ]
387
+ }
388
+ };
389
+ }
390
+ if (req.method === "tools/call") {
391
+ const params = req.params ?? {};
392
+ if (params.name !== APPROVAL_TOOL_NAME) {
393
+ return { jsonrpc: "2.0", id, error: { code: -32602, message: `unknown tool "${String(params.name)}"` } };
394
+ }
395
+ const args = params.arguments ?? {};
396
+ const toolName = typeof args.tool_name === "string" ? args.tool_name : "unknown tool";
397
+ const input = args.input ?? {};
398
+ const summary = summarizeToolCall(toolName, input);
399
+ let outcome;
400
+ try {
401
+ outcome = await deps.requestApproval(taskId, summary);
402
+ } catch (err) {
403
+ outcome = { approved: false, reason: `could not reach the approving device: ${errorMessage2(err)}` };
404
+ }
405
+ const payload = outcome.approved ? { behavior: "allow", updatedInput: input } : { behavior: "deny", message: outcome.reason ?? "denied" };
406
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(payload) }] } };
407
+ }
408
+ if (id !== void 0) {
409
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: `unknown method: ${String(req.method)}` } };
410
+ }
411
+ return void 0;
412
+ }
413
+ function serveApprovalMcpOverStdio(opts) {
414
+ const input = opts.input ?? process.stdin;
415
+ const output = opts.output ?? process.stdout;
416
+ const rl = createInterface({ input, terminal: false });
417
+ rl.on("line", (line) => {
418
+ const trimmed = line.trim();
419
+ if (!trimmed) return;
420
+ let parsed;
421
+ try {
422
+ parsed = JSON.parse(trimmed);
423
+ } catch {
424
+ return;
425
+ }
426
+ void handleMcpRequest(parsed, opts.deps, opts.taskId).then((response) => {
427
+ if (response !== void 0) output.write(`${JSON.stringify(response)}
428
+ `);
429
+ });
430
+ });
431
+ }
432
+
433
+ // src/bin/byok-approval-mcp.ts
434
+ function errorMessage3(err) {
435
+ return err instanceof Error ? err.message : String(err);
436
+ }
437
+ var REQUEST_TIMEOUT_SLOP_MS = 5e3;
438
+ function requireEnv(name) {
439
+ const value = process.env[name];
440
+ if (!value) {
441
+ process.stderr.write(`byok-approval-mcp: missing required env var ${name} \u2014 refusing to start
442
+ `);
443
+ process.exit(1);
444
+ }
445
+ return value;
446
+ }
447
+ async function main() {
448
+ const storeDir = requireEnv("BYOK_STORE_DIR");
449
+ const productId = requireEnv("BYOK_PRODUCT_ID");
450
+ const taskId = requireEnv("BYOK_TASK_ID");
451
+ const timeoutMs = Number(process.env.BYOK_APPROVAL_TIMEOUT_MS ?? "600000");
452
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
453
+ process.stderr.write("byok-approval-mcp: invalid BYOK_APPROVAL_TIMEOUT_MS env var \u2014 refusing to start\n");
454
+ process.exit(1);
455
+ }
456
+ let clientPromise;
457
+ function getClient() {
458
+ if (!clientPromise) {
459
+ clientPromise = connectControlClient({ storeDir, productId, requestTimeoutMs: timeoutMs + REQUEST_TIMEOUT_SLOP_MS }).then(
460
+ (conn) => {
461
+ if (!conn.ok) {
462
+ clientPromise = void 0;
463
+ throw new Error(conn.reason);
464
+ }
465
+ return conn.client;
466
+ }
467
+ );
468
+ clientPromise.catch(() => {
469
+ clientPromise = void 0;
470
+ });
471
+ }
472
+ return clientPromise;
473
+ }
474
+ const deps = {
475
+ requestApproval: async (tId, summary) => {
476
+ let client;
477
+ try {
478
+ client = await getClient();
479
+ } catch (err) {
480
+ throw new Error(`could not connect to the daemon control socket: ${errorMessage3(err)}`);
481
+ }
482
+ try {
483
+ return await client.request("approvals.request", { taskId: tId, summary });
484
+ } catch (err) {
485
+ clientPromise = void 0;
486
+ throw err;
487
+ }
488
+ }
489
+ };
490
+ serveApprovalMcpOverStdio({ taskId, deps });
491
+ }
492
+ main().catch((err) => {
493
+ process.stderr.write(`byok-approval-mcp: fatal error: ${errorMessage3(err)}
494
+ `);
495
+ process.exit(1);
496
+ });
497
+ //# sourceMappingURL=byok-approval-mcp.js.map
498
+ //# sourceMappingURL=byok-approval-mcp.js.map
@@ -0,0 +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 os from 'node:os';\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\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 `os.tmpdir()` — derived from a hash\n * of `storeDir` alone, so both the daemon and any CLI invocation pointed at\n * the same `storeDir` independently compute the identical fallback path.\n *\n * Nested one level deep (rather than a bare `<hash>.sock` file directly in\n * the shared, world-traversable `os.tmpdir()`) 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(os.tmpdir(), `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\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"]}