@canonmsg/agent-sdk 12.4.1 → 12.6.1

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 (40) hide show
  1. package/README.md +37 -6
  2. package/dist/chunks/{chunk-6PKXASSK.js → chunk-5J4YPLNW.js} +394 -1687
  3. package/dist/chunks/{chunk-NXL4C2R6.js → chunk-DAJ7L7TN.js} +954 -429
  4. package/dist/chunks/chunk-FVYKLH6I.js +1475 -0
  5. package/dist/chunks/chunk-HH7KR22P.js +406 -0
  6. package/dist/chunks/chunk-QK37HAHC.js +118 -0
  7. package/dist/chunks/{chunk-GWQF4WFT.js → chunk-S7BV74DN.js} +734 -812
  8. package/dist/chunks/{mls-crypto-LGPCXM3K.js → mls-crypto-4DZT2UFH.js} +2 -1
  9. package/dist/cli.js +70 -32
  10. package/dist/drivers/acp.d.ts +56 -0
  11. package/dist/drivers/codex.d.ts +1 -2
  12. package/dist/drivers/connector.d.ts +5 -4
  13. package/dist/drivers/subprocess.d.ts +35 -0
  14. package/dist/index.d.ts +3 -1
  15. package/dist/index.js +12 -3
  16. package/dist/internal/backend-contracts/connectorRelay.d.ts +34 -0
  17. package/dist/internal/backend-contracts/endpoint.d.ts +1 -1
  18. package/dist/internal/backend-contracts/endpointContentPolicy.d.ts +2 -2
  19. package/dist/internal/backend-contracts/index.d.ts +1 -0
  20. package/dist/internal/backend-contracts/loggedOutSessions.d.ts +7 -0
  21. package/dist/internal/backend-contracts/mlsDevCredential.d.ts +3 -0
  22. package/dist/internal/endpoint/engine.d.ts +8 -1
  23. package/dist/internal/endpoint/mls-dev-client.d.ts +11 -0
  24. package/dist/internal/endpoint/mls-dev-text.d.ts +3 -0
  25. package/dist/internal/endpoint/node-mls-custody-protocol.d.ts +5 -0
  26. package/dist/internal/endpoint/node.d.ts +1 -0
  27. package/dist/media.js +4 -2
  28. package/dist/mls/darwin-arm64/canon-mls-custody +0 -0
  29. package/dist/mls/linux-arm64/canon-mls-custody +0 -0
  30. package/dist/mls/linux-x64/canon-mls-custody +0 -0
  31. package/dist/mls/wasm/canon_mls_wasm.js +363 -0
  32. package/dist/mls/wasm/canon_mls_wasm_bg.wasm +0 -0
  33. package/dist/mls-wasm-host.js +258 -0
  34. package/dist/runner/mls-bundled.d.ts +15 -2
  35. package/dist/runner/mls-managed.d.ts +10 -2
  36. package/dist/runner/mls.d.ts +7 -1
  37. package/dist/runner/relay.d.ts +3 -3
  38. package/dist/runner/runner.d.ts +18 -7
  39. package/dist/runner/types.d.ts +12 -4
  40. package/package.json +1 -1
@@ -1,9 +1,19 @@
1
+ import {
2
+ assertPrivateHome,
3
+ assertPrivateWindowsFile,
4
+ privateKey,
5
+ restrictWindowsFile,
6
+ runnerLockName,
7
+ secureWindowsDirectory,
8
+ wasmMlsCustody,
9
+ windowsAclProblems,
10
+ writeDurable
11
+ } from "./chunk-HH7KR22P.js";
1
12
  import {
2
13
  ApprovalManager,
3
14
  BoundedFileReadError,
4
15
  CanonClient,
5
16
  MAX_CANON_MEDIA_BYTES,
6
- NodeMlsDevCustodyError,
7
17
  RUNTIME_NEW_SESSION_ACTION,
8
18
  RUNTIME_STOP_ACTION,
9
19
  RUNTIME_STOP_AND_DROP_ACTION,
@@ -12,8 +22,6 @@ import {
12
22
  buildChunkedMessagePartMetadata,
13
23
  conversationMediaCacheDir,
14
24
  createCompletedOutputJournal,
15
- createNodeMlsDevCustody,
16
- createPortableNodeMlsDevCustody,
17
25
  createRuntimeHeartbeat,
18
26
  createRuntimeStatePublisher,
19
27
  inferUploadMimeType,
@@ -30,7 +38,15 @@ import {
30
38
  sendMessageWithRetryChunked,
31
39
  splitTextByUtf8Bytes,
32
40
  verifyCanonRuntimeConnection
33
- } from "./chunk-6PKXASSK.js";
41
+ } from "./chunk-5J4YPLNW.js";
42
+ import {
43
+ NodeMlsDevCustodyError,
44
+ createNodeMlsDevCustody,
45
+ createPortableNodeMlsDevCustody
46
+ } from "./chunk-FVYKLH6I.js";
47
+ import {
48
+ parseMlsDevClientConfiguration
49
+ } from "./chunk-QK37HAHC.js";
34
50
  import {
35
51
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
36
52
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
@@ -54,12 +70,11 @@ import {
54
70
  mlsDevProtocolCarriesCards,
55
71
  parseConnectorRelayRequest,
56
72
  parseDirectConversationSelection,
57
- parseMlsDevClientConfiguration,
58
73
  parseMlsDevCredentialToken,
59
74
  parseMlsDevGroupHead,
60
75
  parseMlsDevGroupRoster,
61
76
  resolveAllowedUploadMimeType
62
- } from "./chunk-GWQF4WFT.js";
77
+ } from "./chunk-S7BV74DN.js";
63
78
 
64
79
  // src/runner/types.ts
65
80
  var DriverSessionError = class extends Error {
@@ -106,11 +121,13 @@ async function loadInstructions(path) {
106
121
  if (real !== path) refuse(`must be given by its real path (${real}), with no symbolic links`);
107
122
  const parent = await stat(dirname(path)).catch(() => refuse("cannot be read"));
108
123
  const uid = process.getuid?.();
109
- if (parent.uid !== uid && parent.uid !== 0 || (parent.mode & 18) !== 0) refuse("its directory must not be writable by group or others");
124
+ const windows = process.platform === "win32" ? windowsAclProblems([[dirname(path), "unwritable"], [path, "unwritable"]]) : void 0;
125
+ if (windows?.length) refuse(`must not be writable by others: ${windows[0]}`);
126
+ if (!windows && (parent.uid !== uid && parent.uid !== 0 || (parent.mode & 18) !== 0)) refuse("its directory must not be writable by group or others");
110
127
  const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(() => refuse("cannot be read"));
111
128
  try {
112
129
  const info = await file.stat();
113
- if (info.uid !== uid || (info.mode & 18) !== 0) refuse("must be owned by you and not writable by group or others");
130
+ if (!windows && (info.uid !== uid || (info.mode & 18) !== 0)) refuse("must be owned by you and not writable by group or others");
114
131
  const bytes = await readBoundedRegularFileHandle(file, MAX_INSTRUCTIONS_BYTES).catch((error) => refuse(error instanceof BoundedFileReadError ? error.failure === "too-large" ? "is larger than 32 KiB" : "is not a regular file" : "cannot be read"));
115
132
  let text3;
116
133
  try {
@@ -126,11 +143,13 @@ async function loadInstructions(path) {
126
143
  }
127
144
  __name(loadInstructions, "loadInstructions");
128
145
 
129
- // src/drivers/claude.ts
130
- import { randomUUID } from "node:crypto";
146
+ // src/drivers/acp.ts
147
+ import { spawn as spawn2 } from "node:child_process";
148
+ import { randomBytes } from "node:crypto";
131
149
  import { realpathSync } from "node:fs";
132
150
  import { tmpdir } from "node:os";
133
- import { basename, dirname as dirname2 } from "node:path";
151
+ import { basename } from "node:path";
152
+ import { pathToFileURL } from "node:url";
134
153
 
135
154
  // src/drivers/call-scope.ts
136
155
  import { createHash } from "node:crypto";
@@ -141,17 +160,464 @@ function callScope(name, call) {
141
160
  }
142
161
  __name(callScope, "callScope");
143
162
 
163
+ // src/drivers/installed-cli.ts
164
+ import { spawn } from "node:child_process";
165
+ import { existsSync, readFileSync } from "node:fs";
166
+ import { dirname as dirname2, extname, join } from "node:path";
167
+ function shimTarget(shim) {
168
+ const last = [...shim.matchAll(/"([^"]+)"\s+%\*/g)].at(-1)?.[1];
169
+ const variable = /^%(\w+)%$/.exec(last ?? "")?.[1];
170
+ const target = variable ? new RegExp(`SET "${variable}=([^"]+)"`, "i").exec(shim)?.[1] : last;
171
+ return /^%~?dp0%?\\(.+)$/.exec(target ?? "")?.[1];
172
+ }
173
+ __name(shimTarget, "shimTarget");
174
+ function installedCli(path) {
175
+ const given = { file: path, args: [] };
176
+ if (process.platform !== "win32") return given;
177
+ const names = extname(path) ? [path] : [`${path}.exe`, `${path}.cmd`];
178
+ const directories = /[\\/]/.test(path) ? [""] : (process.env.PATH ?? "").split(";").filter(Boolean);
179
+ for (const directory of directories) {
180
+ for (const name of names) {
181
+ const file = join(directory, name);
182
+ if (!existsSync(file)) continue;
183
+ if (!/\.(?:cmd|bat)$/i.test(file)) return { file, args: [] };
184
+ const target = shimTarget(readFileSync(file, "utf8"));
185
+ if (!target) throw new Error(`${file} is not an npm shim: give the path of the program it runs.`);
186
+ const script = join(dirname2(file), target);
187
+ return /\.[cm]?js$/i.test(script) ? { file: process.execPath, args: [script] } : { file: script, args: [] };
188
+ }
189
+ }
190
+ return given;
191
+ }
192
+ __name(installedCli, "installedCli");
193
+ function stopCli(child) {
194
+ if (process.platform !== "win32") {
195
+ child.kill("SIGTERM");
196
+ return;
197
+ }
198
+ if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
199
+ spawn(
200
+ join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe"),
201
+ ["/PID", String(child.pid), "/T", "/F"],
202
+ { stdio: "ignore", windowsHide: true }
203
+ ).once("error", () => {
204
+ child.kill();
205
+ });
206
+ }
207
+ __name(stopCli, "stopCli");
208
+
209
+ // src/drivers/subprocess.ts
210
+ import { createHash as createHash2 } from "node:crypto";
211
+ import { createInterface } from "node:readline";
212
+ var isRecord = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
213
+ var text = /* @__PURE__ */ __name((value) => typeof value === "string" ? value : "", "text");
214
+ var oneLine = /* @__PURE__ */ __name((value, max) => {
215
+ const line = value.replace(/\s+/g, " ").trim();
216
+ return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
217
+ }, "oneLine");
218
+ var RISKY = /\brm\s+-[a-z]*[rf]|\bgit\s+(reset\s+--hard|push\s+(-f|--force))|\bdrop\s+(table|database)\b|\bmkfs\b|\bdd\s+if=/i;
219
+ var guestTurn = /* @__PURE__ */ __name((turn) => !turn.input.messages.every((message2) => message2.isOwner || message2.trustedAgent === true), "guestTurn");
220
+ async function approve(turn, request) {
221
+ const asked = createHash2("sha256").update(JSON.stringify([request.scopeKey, request.title, request.detail])).digest("hex").slice(0, 12);
222
+ const answer = await turn.ask({ kind: "approve", ...request, id: `${request.id}#${asked}` }).catch(() => void 0);
223
+ if (!answer) return "stopped";
224
+ if (answer.status !== "answered" || !answer.allow) return "denied";
225
+ return answer.scope === "session" ? "session" : "once";
226
+ }
227
+ __name(approve, "approve");
228
+ function stdioRpc(child, handlers, { name, envelope }) {
229
+ let next = 0;
230
+ const pending = /* @__PURE__ */ new Map();
231
+ const write = /* @__PURE__ */ __name((message2) => {
232
+ if (child.stdin?.writable) child.stdin.write(`${JSON.stringify({ ...envelope, ...message2 })}
233
+ `);
234
+ }, "write");
235
+ child.stdin?.on("error", () => void 0);
236
+ createInterface({ input: child.stdout }).on("error", () => void 0).on("line", (line) => {
237
+ let message2;
238
+ try {
239
+ message2 = JSON.parse(line);
240
+ } catch {
241
+ return;
242
+ }
243
+ if (!isRecord(message2)) return;
244
+ const params = isRecord(message2.params) ? message2.params : {};
245
+ if (typeof message2.method !== "string") {
246
+ const waiting = pending.get(Number(message2.id));
247
+ pending.delete(Number(message2.id));
248
+ if (isRecord(message2.error)) waiting?.reject(Object.assign(new Error(text(message2.error.message) || `${name} refused the request`), { code: message2.error.code }));
249
+ else waiting?.resolve(message2.result);
250
+ } else if (message2.id !== void 0) {
251
+ handlers.request(message2.method, params, message2.id).then(
252
+ (result) => write({ id: message2.id, result }),
253
+ (error) => write({ id: message2.id, error: {
254
+ code: typeof error?.code === "number" ? error.code : -32e3,
255
+ message: error instanceof Error ? error.message : String(error)
256
+ } })
257
+ );
258
+ } else handlers.notify(message2.method, params);
259
+ });
260
+ child.stderr?.resume();
261
+ return {
262
+ call: /* @__PURE__ */ __name((method, params) => new Promise((resolve3, reject) => {
263
+ next += 1;
264
+ pending.set(next, { resolve: /* @__PURE__ */ __name((value) => resolve3(isRecord(value) ? value : {}), "resolve"), reject });
265
+ write({ id: next, method, params });
266
+ }), "call"),
267
+ notify: /* @__PURE__ */ __name((method, params) => write({ method, ...params ? { params } : {} }), "notify"),
268
+ fail(error) {
269
+ for (const waiting of pending.values()) waiting.reject(error);
270
+ pending.clear();
271
+ }
272
+ };
273
+ }
274
+ __name(stdioRpc, "stdioRpc");
275
+
276
+ // src/drivers/acp.ts
277
+ var PROTOCOL = 1;
278
+ var SESSION = "sessionId";
279
+ var OWNER_MODE = "ownerMode";
280
+ var CANON = "canon";
281
+ var METHOD_NOT_FOUND = -32601;
282
+ var AUTH_REQUIRED = -32e3;
283
+ var SYSTEM = "You are an agent in Canon, a messaging app where people and AI agents talk. Each prompt holds the new messages in one conversation. Your final message in a turn is posted there as your reply, so put your whole answer in it. Canon's tools are on the MCP server whose name starts with canon_. To ask the people in the conversation a question, call its ask_question tool and wait for the answer. When you have nothing to add, call its no_reply tool instead of answering.";
284
+ var GUEST = "Only your owner can let you use tools in this conversation, and this turn holds a message from someone else: do not read files, run commands or use any tool but those of the MCP server whose name starts with canon_. Answer from the conversation and what you already know, and say so when something needs your owner.";
285
+ var OWNER_ONLY = "This agent only takes messages from its owner.";
286
+ var PIPE_GRACE_MS = 1e3;
287
+ var KILL_AFTER_MS = 2e3;
288
+ var MACHINE_KINDS = /* @__PURE__ */ new Set(["read", "edit", "delete", "move", "search", "execute", "fetch"]);
289
+ var BRIDGE = `
290
+ const { request } = require('node:http');
291
+ const url = new URL(process.env.TURN_TOOLS_URL);
292
+ const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' };
293
+ require('node:readline').createInterface({ input: process.stdin }).on('line', (line) => {
294
+ const post = request(url, { method: 'POST', headers }, (response) => {
295
+ let body = '';
296
+ response.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
297
+ response.on('end', () => { if (body.trim()) process.stdout.write(body.trim() + '\\n'); });
298
+ });
299
+ post.on('error', () => undefined);
300
+ post.end(line);
301
+ });`;
302
+ function startAgent(command, args, cwd) {
303
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("CANON_")));
304
+ const cli = installedCli(command);
305
+ return spawn2(cli.file, [...cli.args, ...args], { cwd, env, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32", windowsHide: true });
306
+ }
307
+ __name(startAgent, "startAgent");
308
+ function signalAgent(child, signal) {
309
+ if (process.platform === "win32") {
310
+ stopCli(child);
311
+ return;
312
+ }
313
+ try {
314
+ if (child.pid) process.kill(-child.pid, signal);
315
+ else child.kill(signal);
316
+ } catch {
317
+ }
318
+ }
319
+ __name(signalAgent, "signalAgent");
320
+ var refuseMethod = /* @__PURE__ */ __name((method) => Promise.reject(Object.assign(new Error(`Canon does not offer ${method}`), { code: METHOD_NOT_FOUND })), "refuseMethod");
321
+ var restoreMethod = /* @__PURE__ */ __name((capabilities) => {
322
+ if (isRecord(capabilities.sessionCapabilities) && isRecord(capabilities.sessionCapabilities.resume)) return "session/resume";
323
+ return capabilities.loadSession === true ? "session/load" : void 0;
324
+ }, "restoreMethod");
325
+ async function initialize(link, agent) {
326
+ const result = await link.call("initialize", {
327
+ protocolVersion: PROTOCOL,
328
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
329
+ clientInfo: { name: "canon-agent", title: "Canon", version: "1" }
330
+ });
331
+ if (result.protocolVersion !== PROTOCOL) {
332
+ throw new Error(`${agent} speaks Agent Client Protocol version ${String(result.protocolVersion)}; the acp driver speaks version ${PROTOCOL}`);
333
+ }
334
+ return { capabilities: isRecord(result.agentCapabilities) ? result.agentCapabilities : {}, info: isRecord(result.agentInfo) ? result.agentInfo : {} };
335
+ }
336
+ __name(initialize, "initialize");
337
+ async function checkAcp({ command, args = [], cwd, timeoutMs = 3e4 }) {
338
+ const child = startAgent(command, args, realpathSync(cwd));
339
+ const link = stdioRpc(child, { notify: /* @__PURE__ */ __name(() => void 0, "notify"), request: /* @__PURE__ */ __name((method) => refuseMethod(method), "request") }, { name: basename(command), envelope: { jsonrpc: "2.0" } });
340
+ let timer;
341
+ try {
342
+ return await new Promise((resolve3, reject) => {
343
+ timer = setTimeout(() => reject(new Error(`${command} did not answer the Agent Client Protocol's initialize within ${timeoutMs / 1e3} s`)), timeoutMs);
344
+ child.once("error", (error) => reject(new Error(`Could not run ${command}: ${error.message}`)));
345
+ child.once("close", () => reject(new Error(`${command} exited before answering the Agent Client Protocol's initialize`)));
346
+ initialize(link, command).then(({ capabilities, info }) => resolve3({
347
+ name: text(info.title) || text(info.name) || basename(command),
348
+ version: text(info.version),
349
+ sessionMemory: restoreMethod(capabilities) ? "native" : "none"
350
+ }), reject);
351
+ });
352
+ } finally {
353
+ clearTimeout(timer);
354
+ child.stdin?.end();
355
+ signalAgent(child, "SIGTERM");
356
+ setTimeout(() => signalAgent(child, "SIGKILL"), KILL_AFTER_MS).unref();
357
+ }
358
+ }
359
+ __name(checkAcp, "checkAcp");
360
+ var present = /* @__PURE__ */ __name((fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== null && value !== void 0)), "present");
361
+ var toolIdentity = /* @__PURE__ */ __name((call) => text(call.name) || text(call.title), "toolIdentity");
362
+ function canonTool(call, turn, server, guest) {
363
+ if (!["", "other"].includes(text(call.kind)) || guest && !text(call.name)) return false;
364
+ const name = toolIdentity(call).replace(/\W+/g, "_").replace(/^mcp_+/, "");
365
+ return turn.tools.definitions.some((tool) => name === `${server}_${tool.name}` || name === `${server}__${tool.name}`);
366
+ }
367
+ __name(canonTool, "canonTool");
368
+ function acpDriver(options) {
369
+ const { command, args = [] } = options;
370
+ if (options.guestMode !== void 0 && (options.unguardedGuests || !options.guestMode.trim())) {
371
+ throw new Error("guestMode must name a mode, and cannot go with unguardedGuests");
372
+ }
373
+ const cwd = realpathSync(options.cwd);
374
+ const agent = basename(command);
375
+ const interruptGraceMs = options.interruptGraceMs ?? 5e3;
376
+ const framing = withInstructions(SYSTEM, options.instructions);
377
+ const running = /* @__PURE__ */ new Map();
378
+ const end = /* @__PURE__ */ __name(async (session) => {
379
+ const live = running.get(session);
380
+ if (!live) return;
381
+ live.close();
382
+ await live.exited;
383
+ }, "end");
384
+ return {
385
+ name: "acp",
386
+ capabilities: {
387
+ sessionMemory: options.sessionMemory ?? "none",
388
+ ask: ["approve", "choose", "text"],
389
+ streaming: true,
390
+ activity: true,
391
+ tools: true,
392
+ outboundFiles: { roots: [cwd, realpathSync(tmpdir())] },
393
+ ownerHistoryOnly: options.guestMode === void 0 && !options.unguardedGuests
394
+ },
395
+ async runTurn(turn) {
396
+ await running.get(turn.session)?.exited;
397
+ if (turn.signal.aborted) throw turn.signal.reason;
398
+ const guest = guestTurn(turn);
399
+ if (guest && options.guestMode === void 0 && !options.unguardedGuests) return { reply: OWNER_ONLY };
400
+ const child = startAgent(command, args, cwd);
401
+ let exit = /* @__PURE__ */ __name(() => void 0, "exit");
402
+ const exited = new Promise((resolve3) => {
403
+ exit = resolve3;
404
+ });
405
+ let killing;
406
+ let stopping = false;
407
+ const entry = { close: /* @__PURE__ */ __name(() => {
408
+ stopping = true;
409
+ signalAgent(child, "SIGTERM");
410
+ killing ??= setTimeout(() => signalAgent(child, "SIGKILL"), KILL_AFTER_MS);
411
+ }, "close"), exited };
412
+ running.set(turn.session, entry);
413
+ const server = `${CANON}_${randomBytes(4).toString("hex")}`;
414
+ return new Promise((resolve3, reject) => {
415
+ const messages = [];
416
+ const calls = /* @__PURE__ */ new Map();
417
+ let sessionId = "";
418
+ let answering = false;
419
+ let continues = false;
420
+ let closed = false;
421
+ let ended = Promise.resolve();
422
+ let closing;
423
+ const visible = /* @__PURE__ */ __name(() => messages.map((message2) => message2.text).filter((part) => part.trim()).join("\n\n"), "visible");
424
+ const closeLater = /* @__PURE__ */ __name(() => {
425
+ if (!closed) closing ??= setTimeout(entry.close, interruptGraceMs);
426
+ }, "closeLater");
427
+ const finish = /* @__PURE__ */ __name(() => {
428
+ if (!closed) child.stdin?.end();
429
+ closeLater();
430
+ }, "finish");
431
+ const interrupt = /* @__PURE__ */ __name(() => {
432
+ if (sessionId && answering) link.notify("session/cancel", { sessionId });
433
+ closeLater();
434
+ }, "interrupt");
435
+ const settle = /* @__PURE__ */ __name((outcome) => {
436
+ answering = false;
437
+ finish();
438
+ if (turn.signal.aborted) reject(turn.signal.reason);
439
+ else if (outcome instanceof Error) reject(outcome);
440
+ else resolve3(outcome);
441
+ }, "settle");
442
+ const stopGuest = /* @__PURE__ */ __name((what) => {
443
+ link.notify("session/cancel", { sessionId });
444
+ entry.close();
445
+ settle(new DriverSessionError(`${agent} ${what} in a turn holding someone other than the owner's message`));
446
+ }, "stopGuest");
447
+ const guard = /* @__PURE__ */ __name((call) => {
448
+ if (guest && MACHINE_KINDS.has(text(call.kind)) && ["in_progress", "completed"].includes(text(call.status))) stopGuest(`ran a ${text(call.kind)} tool`);
449
+ }, "guard");
450
+ const known = /* @__PURE__ */ __name((update2, session) => {
451
+ const key = JSON.stringify([text(session), text(update2.toolCallId)]);
452
+ const call = { ...calls.get(key), ...present(update2) };
453
+ calls.set(key, call);
454
+ return call;
455
+ }, "known");
456
+ const update = /* @__PURE__ */ __name((params) => {
457
+ const change = isRecord(params.update) ? params.update : {};
458
+ if (!answering) return;
459
+ const tool = change.sessionUpdate === "tool_call" || change.sessionUpdate === "tool_call_update";
460
+ const call = tool ? known(change, params.sessionId) : void 0;
461
+ if (call) guard(call);
462
+ if (guest && options.guestMode !== void 0 && change.sessionUpdate === "current_mode_update" && change.currentModeId !== options.guestMode) stopGuest("left its guest mode");
463
+ if (text(params.sessionId) !== sessionId) return;
464
+ if (change.sessionUpdate === "agent_message_chunk") {
465
+ const chunk = isRecord(change.content) && change.content.type === "text" ? text(change.content.text) : "";
466
+ if (!chunk) return;
467
+ const id = text(change.messageId);
468
+ const message2 = id ? messages.find((known2) => known2.id === id) : continues ? messages.at(-1) : void 0;
469
+ if (message2) message2.text += chunk;
470
+ else messages.push({ id, text: chunk });
471
+ continues = true;
472
+ turn.onText(visible());
473
+ } else if (call) {
474
+ if (change.sessionUpdate === "tool_call") continues = false;
475
+ turn.onActivity({
476
+ id: text(call.toolCallId),
477
+ kind: "tool",
478
+ title: oneLine(text(call.title) || "Use a tool", 120),
479
+ state: call.status === "completed" ? "done" : call.status === "failed" ? "failed" : "running"
480
+ });
481
+ }
482
+ }, "update");
483
+ const permission = /* @__PURE__ */ __name(async (params) => {
484
+ const offered = (Array.isArray(params.options) ? params.options : []).filter(isRecord);
485
+ const pick = /* @__PURE__ */ __name((kind) => offered.find((option) => option.kind === kind), "pick");
486
+ const select = /* @__PURE__ */ __name((option) => ({ outcome: option ? { outcome: "selected", optionId: text(option.optionId) } : { outcome: "cancelled" } }), "select");
487
+ if (turn.signal.aborted) return select(void 0);
488
+ const allow = pick("allow_once");
489
+ const refuse = select(pick("reject_once") ?? pick("reject_always"));
490
+ const call = known(isRecord(params.toolCall) ? params.toolCall : {}, params.sessionId);
491
+ if (allow && canonTool(call, turn, server, guest)) return select(allow);
492
+ if (guest || !allow) return refuse;
493
+ const input = call.rawInput === void 0 || ["{}", "[]", '""', "null"].includes(JSON.stringify(call.rawInput)) ? "" : JSON.stringify(call.rawInput);
494
+ const title = oneLine(text(call.title) || "Use a tool", 200);
495
+ const paths = (Array.isArray(call.locations) ? call.locations : []).filter(isRecord).map((location) => text(location.path));
496
+ const edits = (Array.isArray(call.content) ? call.content : []).filter(isRecord).filter((item) => item.type === "diff").map((item) => `${text(item.path)}
497
+ ${text(item.newText)}`);
498
+ const id = `${text(params.sessionId) === sessionId ? "" : `${text(params.sessionId)}:`}${text(call.toolCallId) || "tool"}`;
499
+ const verdict = await approve(turn, {
500
+ id,
501
+ title,
502
+ detail: [input, ...paths, ...edits].filter(Boolean).join("\n"),
503
+ scopeKey: callScope(text(call.kind) || "tool", [toolIdentity(call) || title, input, call.locations ?? null]),
504
+ allowSession: input !== "" && text(call.name) !== "",
505
+ // JSON punctuation goes first, so a command given as an argument list reads as one.
506
+ ...call.kind === "delete" || RISKY.test(input.replace(/[",[\]{}]+/g, " ")) ? { risk: "destructive" } : {}
507
+ });
508
+ if (verdict === "stopped" || turn.signal.aborted) return select(void 0);
509
+ return verdict === "denied" ? refuse : select(allow);
510
+ }, "permission");
511
+ const link = stdioRpc(child, {
512
+ notify: /* @__PURE__ */ __name((method, params) => {
513
+ if (method === "session/update") update(params);
514
+ }, "notify"),
515
+ request: /* @__PURE__ */ __name((method, params) => method === "session/request_permission" ? permission(params) : refuseMethod(method), "request")
516
+ }, { name: agent, envelope: { jsonrpc: "2.0" } });
517
+ const gone = /* @__PURE__ */ __name((error) => {
518
+ if (closed) return;
519
+ closed = true;
520
+ clearTimeout(closing);
521
+ clearTimeout(killing);
522
+ turn.signal.removeEventListener("abort", interrupt);
523
+ link.fail(error);
524
+ settle(error);
525
+ void ended.then(() => {
526
+ if (running.get(turn.session) === entry) running.delete(turn.session);
527
+ exit();
528
+ });
529
+ }, "gone");
530
+ child.once("exit", () => {
531
+ if (stopping || guest || turn.signal.aborted) {
532
+ signalAgent(child, "SIGTERM");
533
+ ended = new Promise((resolve4) => {
534
+ setTimeout(() => {
535
+ signalAgent(child, "SIGKILL");
536
+ resolve4();
537
+ }, KILL_AFTER_MS);
538
+ });
539
+ }
540
+ setTimeout(() => {
541
+ child.stdout?.destroy();
542
+ child.stderr?.destroy();
543
+ }, PIPE_GRACE_MS).unref();
544
+ });
545
+ child.once("error", (error) => gone(new Error(`Could not run ${command}: ${error.message}`)));
546
+ child.once("close", () => gone(new Error(`${agent} exited before finishing the turn`)));
547
+ turn.signal.addEventListener("abort", interrupt, { once: true });
548
+ const signInError = /* @__PURE__ */ __name((error) => error.code === AUTH_REQUIRED && !turn.signal.aborted ? new Error(`${agent} needs signing in: run it once yourself, then start this agent again`) : void 0, "signInError");
549
+ async function openSession(capabilities) {
550
+ const mcp = isRecord(capabilities.mcpCapabilities) ? capabilities.mcpCapabilities : {};
551
+ const { url } = turn.tools;
552
+ const mcpServers = !url ? [] : mcp.http === true ? [{ type: "http", name: server, url, headers: [] }] : [{ name: server, command: process.execPath, args: ["-e", BRIDGE], env: [{ name: "TURN_TOOLS_URL", value: url }] }];
553
+ const restore = options.sessionMemory === "native" ? restoreMethod(capabilities) : void 0;
554
+ const saved = restore && turn.state.get(SESSION);
555
+ if (saved) {
556
+ const restored = await link.call(restore, { sessionId: saved, cwd, mcpServers }).catch((error) => {
557
+ throw signInError(error) ?? (turn.signal.aborted ? error : new DriverSessionError(`${agent} could not restore session ${saved}`, { cause: error }));
558
+ });
559
+ return { id: saved, fresh: false, keep: false, modes: restored.modes };
560
+ }
561
+ const opened = await link.call("session/new", { cwd, mcpServers }).catch((error) => {
562
+ throw signInError(error) ?? error;
563
+ });
564
+ if (!text(opened.sessionId)) throw new Error(`${agent} opened no session`);
565
+ return { id: text(opened.sessionId), fresh: true, keep: Boolean(restore), modes: opened.modes };
566
+ }
567
+ __name(openSession, "openSession");
568
+ async function applyMode(modes) {
569
+ if (!options.guestMode) return;
570
+ const current = isRecord(modes) ? text(modes.currentModeId) : "";
571
+ if (!turn.state.get(OWNER_MODE) && current) turn.state.set(OWNER_MODE, current);
572
+ const wanted = guest ? options.guestMode : turn.state.get(OWNER_MODE) ?? current;
573
+ if (wanted && wanted !== current) {
574
+ await link.call("session/set_mode", { sessionId, modeId: wanted }).catch((error) => {
575
+ throw new Error(`${agent} could not enter mode "${wanted}": ${oneLine(error.message, 200)}`);
576
+ });
577
+ }
578
+ }
579
+ __name(applyMode, "applyMode");
580
+ void (async () => {
581
+ const { capabilities } = await initialize(link, agent);
582
+ const session = await openSession(capabilities);
583
+ sessionId = session.id;
584
+ await applyMode(session.modes);
585
+ if (turn.signal.aborted) return finish();
586
+ if (session.keep) turn.state.set(SESSION, sessionId);
587
+ answering = true;
588
+ const { stopReason } = await link.call("session/prompt", { sessionId, prompt: [
589
+ { type: "text", text: [...session.fresh ? [framing] : [], ...guest ? [GUEST] : [], turn.prompt].join("\n\n") },
590
+ ...turn.files.map((file) => ({ type: "resource_link", name: basename(file), uri: pathToFileURL(file).href }))
591
+ ] });
592
+ if (stopReason === "end_turn") return settle({ reply: messages.filter((message2) => message2.text.trim()).at(-1)?.text ?? null });
593
+ settle(new Error(`${agent} ended the turn: ${String(stopReason)}`));
594
+ })().catch((error) => settle(error instanceof DriverSessionError ? error : new Error(oneLine(error instanceof Error ? error.message : String(error), 300))));
595
+ });
596
+ },
597
+ // New session (and the runner's reset after a stuck turn): the room's process goes; the runner drops the state.
598
+ reset: end,
599
+ async close() {
600
+ await Promise.all([...running.keys()].map(end));
601
+ }
602
+ };
603
+ }
604
+ __name(acpDriver, "acpDriver");
605
+
144
606
  // src/drivers/claude.ts
607
+ import { randomUUID } from "node:crypto";
608
+ import { realpathSync as realpathSync2 } from "node:fs";
609
+ import { tmpdir as tmpdir2 } from "node:os";
610
+ import { basename as basename2, dirname as dirname3 } from "node:path";
145
611
  var SDK = "@anthropic-ai/claude-agent-sdk";
146
- var SESSION = "sessionId";
612
+ var SESSION2 = "sessionId";
147
613
  var ASK_USER_DIALOG = "permission_ask_user_question";
148
614
  var CANON_TOOLS = "mcp__canon__";
149
615
  var NOT_OWNER = "Only the agent's owner can let it use tools in this conversation.";
150
616
  var OPEN_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ToolSearch"]);
151
- var SYSTEM = "You are an agent in Canon, a messaging app where people and AI agents talk. Each prompt holds the new messages in one conversation. The text of your last message in a turn is posted there as your reply, so put your whole answer in it. Tool approvals and your questions (AskUserQuestion) are put to the people in the conversation. When you have nothing to add, call the no_reply tool instead of answering.";
152
- var isRecord = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
617
+ var SYSTEM2 = "You are an agent in Canon, a messaging app where people and AI agents talk. Each prompt holds the new messages in one conversation. The text of your last message in a turn is posted there as your reply, so put your whole answer in it. Tool approvals and your questions (AskUserQuestion) are put to the people in the conversation. When you have nothing to add, call the no_reply tool instead of answering.";
618
+ var isRecord2 = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
153
619
  var forOwner = /* @__PURE__ */ __name((message2) => message2.isOwner || message2.trustedAgent === true, "forOwner");
154
- var oneLine = /* @__PURE__ */ __name((text3, max) => {
620
+ var oneLine2 = /* @__PURE__ */ __name((text3, max) => {
155
621
  const line = text3.replace(/\s+/g, " ").trim();
156
622
  return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
157
623
  }, "oneLine");
@@ -165,10 +631,10 @@ async function loadClaudeQuery() {
165
631
  __name(loadClaudeQuery, "loadClaudeQuery");
166
632
  function describeTool(name, input) {
167
633
  const label = name.startsWith("mcp__") ? name.split("__").slice(2).join("__") || name : name;
168
- const fields = isRecord(input) ? input : {};
634
+ const fields = isRecord2(input) ? input : {};
169
635
  const key = ["description", "command", "file_path", "notebook_path", "pattern", "url", "query", "path"].find((candidate) => typeof fields[candidate] === "string" && fields[candidate].trim() !== "");
170
- const detail = key && (key.endsWith("path") ? basename(fields[key]) : fields[key]);
171
- return oneLine(detail ? `${label}: ${detail}` : label, 120);
636
+ const detail = key && (key.endsWith("path") ? basename2(fields[key]) : fields[key]);
637
+ return oneLine2(detail ? `${label}: ${detail}` : label, 120);
172
638
  }
173
639
  __name(describeTool, "describeTool");
174
640
  function liveReader(turn) {
@@ -194,14 +660,14 @@ function liveReader(turn) {
194
660
  else if (event.type === "content_block_delta" && event.delta?.type === "text_delta") append(event.index ?? 0, String(event.delta.text ?? ""));
195
661
  } else if (message2.type === "assistant") {
196
662
  for (const block of message2.message.content) {
197
- if (!isRecord(block) || block.type !== "tool_use" || typeof block.id !== "string") continue;
663
+ if (!isRecord2(block) || block.type !== "tool_use" || typeof block.id !== "string") continue;
198
664
  const title = describeTool(String(block.name), block.input);
199
665
  tools.set(block.id, title);
200
666
  turn.onActivity({ id: block.id, kind: "tool", title, state: "running" });
201
667
  }
202
668
  } else if (message2.type === "user" && Array.isArray(message2.message.content)) {
203
669
  for (const block of message2.message.content) {
204
- if (!isRecord(block) || block.type !== "tool_result" || !tools.has(String(block.tool_use_id))) continue;
670
+ if (!isRecord2(block) || block.type !== "tool_result" || !tools.has(String(block.tool_use_id))) continue;
205
671
  const id = String(block.tool_use_id);
206
672
  turn.onActivity({ id, kind: "tool", title: tools.get(id), state: block.is_error === true ? "failed" : "done" });
207
673
  }
@@ -219,12 +685,12 @@ function toolScope(toolName, input) {
219
685
  return callScope(toolName, toolName.startsWith("mcp__") ? input : Object.fromEntries(Object.entries(input).filter(([key]) => key !== "description")));
220
686
  }
221
687
  __name(toolScope, "toolScope");
222
- async function approve(turn, toolName, input, prompt) {
688
+ async function approve2(turn, toolName, input, prompt) {
223
689
  const detail = typeof input.command === "string" ? input.command : JSON.stringify(input);
224
690
  const answer = await turn.ask({
225
691
  kind: "approve",
226
692
  id: prompt.toolUseID,
227
- title: oneLine(prompt.title || describeTool(toolName, input), 200),
693
+ title: oneLine2(prompt.title || describeTool(toolName, input), 200),
228
694
  scopeKey: toolScope(toolName, input),
229
695
  allowSession: prompt.suppressAlwaysAllowRule !== true,
230
696
  ...detail && detail !== "{}" ? { detail } : {},
@@ -234,14 +700,14 @@ async function approve(turn, toolName, input, prompt) {
234
700
  if (answer.status !== "answered") return { behavior: "deny", message: DECLINED[answer.status] };
235
701
  return answer.allow ? { behavior: "allow", updatedInput: input, decisionClassification: answer.scope === "session" ? "user_permanent" : "user_temporary" } : { behavior: "deny", message: "This was not allowed.", decisionClassification: "user_reject" };
236
702
  }
237
- __name(approve, "approve");
703
+ __name(approve2, "approve");
238
704
  async function answerQuestions(turn, id, questions, input) {
239
- const list = (Array.isArray(questions) ? questions : []).filter(isRecord).slice(0, 4);
705
+ const list = (Array.isArray(questions) ? questions : []).filter(isRecord2).slice(0, 4);
240
706
  if (!list.length) return { behavior: "deny", message: "There was no question to ask." };
241
707
  const answers = {};
242
708
  for (const [index, question] of list.entries()) {
243
709
  const text3 = typeof question.question === "string" ? question.question : "";
244
- const labels = [...new Set((Array.isArray(question.options) ? question.options : []).filter(isRecord).map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== ""))];
710
+ const labels = [...new Set((Array.isArray(question.options) ? question.options : []).filter(isRecord2).map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== ""))];
245
711
  const askId = list.length > 1 ? `${id}#${index + 1}` : id;
246
712
  const request = labels.length ? { kind: "choose", id: askId, question: text3, options: labels.map((label) => ({ id: label, label })), ...question.multiSelect === true ? { multiple: true } : {} } : { kind: "text", id: askId, question: text3, ...question.isSecret === true ? { secret: true } : {} };
247
713
  const answer = await turn.ask(request).catch(() => void 0);
@@ -253,10 +719,10 @@ async function answerQuestions(turn, id, questions, input) {
253
719
  }
254
720
  __name(answerQuestions, "answerQuestions");
255
721
  function claudeDriver(options) {
256
- const cwd = realpathSync(options.cwd);
722
+ const cwd = realpathSync2(options.cwd);
257
723
  const permissionMode = options.permissionMode ?? "default";
258
724
  const interruptGraceMs = options.interruptGraceMs ?? 5e3;
259
- const system = withInstructions(SYSTEM, options.instructions);
725
+ const system = withInstructions(SYSTEM2, options.instructions);
260
726
  let loaded;
261
727
  const running = /* @__PURE__ */ new Map();
262
728
  const end = /* @__PURE__ */ __name(async (session) => {
@@ -274,11 +740,11 @@ function claudeDriver(options) {
274
740
  const canUseTool = /* @__PURE__ */ __name(async (toolName, input, prompt) => {
275
741
  if (toolName.startsWith(CANON_TOOLS)) return { behavior: "allow", updatedInput: input };
276
742
  if (toolName === "AskUserQuestion") return answerQuestions(turn, prompt.toolUseID, input.questions, input);
277
- return approve(turn, toolName, input, prompt);
743
+ return approve2(turn, toolName, input, prompt);
278
744
  }, "canUseTool");
279
745
  const onUserDialog = /* @__PURE__ */ __name(async (request, { signal }) => {
280
746
  if (request.dialogKind !== ASK_USER_DIALOG) return { behavior: "cancelled" };
281
- const input = isRecord(request.payload.input) ? request.payload.input : { questions: request.payload.questions };
747
+ const input = isRecord2(request.payload.input) ? request.payload.input : { questions: request.payload.questions };
282
748
  const id = request.toolUseID || typeof request.payload.requestId === "string" && request.payload.requestId || randomUUID();
283
749
  const result = await answerQuestions(turn, id, input.questions, input);
284
750
  return signal.aborted ? { behavior: "cancelled" } : { behavior: "completed", result };
@@ -296,7 +762,7 @@ function claudeDriver(options) {
296
762
  ...resume ? { resume } : {},
297
763
  includePartialMessages: true,
298
764
  ...turn.tools.url ? { mcpServers: { canon: { type: "http", url: turn.tools.url, alwaysLoad: true } }, allowedTools: ["mcp__canon"] } : {},
299
- additionalDirectories: [...new Set(turn.files.map((file) => dirname2(file)))],
765
+ additionalDirectories: [...new Set(turn.files.map((file) => dirname3(file)))],
300
766
  canUseTool,
301
767
  onUserDialog,
302
768
  supportedDialogKinds: [ASK_USER_DIALOG],
@@ -316,13 +782,13 @@ function claudeDriver(options) {
316
782
  streaming: true,
317
783
  activity: true,
318
784
  tools: true,
319
- outboundFiles: { roots: [cwd, realpathSync(tmpdir())] }
785
+ outboundFiles: { roots: [cwd, realpathSync2(tmpdir2())] }
320
786
  },
321
787
  async runTurn(turn) {
322
788
  await running.get(turn.session)?.exited;
323
789
  if (turn.signal.aborted) throw turn.signal.reason;
324
790
  const query = await (options.query ? Promise.resolve(options.query) : loaded ??= loadClaudeQuery());
325
- const resume = turn.state.get(SESSION);
791
+ const resume = turn.state.get(SESSION2);
326
792
  const native = query({ prompt: turn.prompt, options: queryOptions(turn, resume) });
327
793
  let exit = /* @__PURE__ */ __name(() => void 0, "exit");
328
794
  const entry = { close: /* @__PURE__ */ __name(() => native.close(), "close"), exited: new Promise((resolve3) => {
@@ -349,7 +815,7 @@ function claudeDriver(options) {
349
815
  const message2 = raw;
350
816
  if (message2.type === "system" && message2.subtype === "init") started = true;
351
817
  const id = message2.session_id;
352
- if (typeof id === "string" && id && id !== session) turn.state.set(SESSION, session = id);
818
+ if (typeof id === "string" && id && id !== session) turn.state.set(SESSION2, session = id);
353
819
  reader.read(message2);
354
820
  if (message2.type !== "result") continue;
355
821
  finished = true;
@@ -359,7 +825,7 @@ function claudeDriver(options) {
359
825
  resolve3({ reply: reply.trim() ? reply : null });
360
826
  } else {
361
827
  const detail = message2.subtype === "success" ? message2.result : message2.errors.join("; ");
362
- reject(failed(new Error(`Claude Code ended the turn with ${message2.subtype}${detail ? `: ${oneLine(detail, 300)}` : ""}`)));
828
+ reject(failed(new Error(`Claude Code ended the turn with ${message2.subtype}${detail ? `: ${oneLine2(detail, 300)}` : ""}`)));
363
829
  }
364
830
  }
365
831
  if (!finished) throw new Error("Claude Code exited without finishing the turn");
@@ -384,8 +850,8 @@ function claudeDriver(options) {
384
850
  __name(claudeDriver, "claudeDriver");
385
851
 
386
852
  // src/drivers/code.ts
387
- import { realpathSync as realpathSync2 } from "node:fs";
388
- import { tmpdir as tmpdir2 } from "node:os";
853
+ import { realpathSync as realpathSync3 } from "node:fs";
854
+ import { tmpdir as tmpdir3 } from "node:os";
389
855
  function codeDriver(handler, capabilities = {}) {
390
856
  return {
391
857
  name: "code",
@@ -395,7 +861,7 @@ function codeDriver(handler, capabilities = {}) {
395
861
  streaming: true,
396
862
  activity: true,
397
863
  tools: true,
398
- outboundFiles: { roots: [realpathSync2(tmpdir2())] },
864
+ outboundFiles: { roots: [realpathSync3(tmpdir3())] },
399
865
  ...capabilities
400
866
  },
401
867
  async runTurn(turn) {
@@ -420,16 +886,14 @@ function codeDriver(handler, capabilities = {}) {
420
886
  __name(codeDriver, "codeDriver");
421
887
 
422
888
  // src/drivers/codex.ts
423
- import { spawn, spawnSync } from "node:child_process";
424
- import { createHash as createHash2 } from "node:crypto";
425
- import { realpathSync as realpathSync3 } from "node:fs";
426
- import { tmpdir as tmpdir3 } from "node:os";
427
- import { basename as basename2 } from "node:path";
428
- import { createInterface } from "node:readline";
889
+ import { spawn as spawn3, spawnSync } from "node:child_process";
890
+ import { realpathSync as realpathSync4 } from "node:fs";
891
+ import { tmpdir as tmpdir4 } from "node:os";
892
+ import { basename as basename3 } from "node:path";
429
893
  var THREAD = "threadId";
430
- var CANON = "canon";
894
+ var CANON2 = "canon";
431
895
  var API_KEY_PROVIDER = "canon_openai_api_key";
432
- var SYSTEM2 = "You are an agent in Canon, a messaging app where people and AI agents talk. Each prompt holds the new messages in one conversation. Your final message in a turn is posted there as your reply, so put your whole answer in it. Approval requests are put to the people in the conversation. To ask them a question, call the ask_question tool of the canon MCP server and wait for its answer. When you have nothing to add, call the no_reply tool of the canon MCP server instead of answering.";
896
+ var SYSTEM3 = "You are an agent in Canon, a messaging app where people and AI agents talk. Each prompt holds the new messages in one conversation. Your final message in a turn is posted there as your reply, so put your whole answer in it. Approval requests are put to the people in the conversation. To ask them a question, call the ask_question tool of the canon MCP server and wait for its answer. When you have nothing to add, call the no_reply tool of the canon MCP server instead of answering.";
433
897
  var CANON_TOOL_TIMEOUT_SEC = 2 * 60 * 60;
434
898
  var MACHINE_FEATURES = [
435
899
  "shell_tool",
@@ -466,18 +930,11 @@ var GUEST_ITEMS = /* @__PURE__ */ new Set([
466
930
  "sleep"
467
931
  ]);
468
932
  var OWN_LISTINGS = /* @__PURE__ */ new Set(["list_mcp_resources", "list_mcp_resource_templates"]);
469
- var GUEST = "Only your owner can let you use tools in this conversation, and this turn holds a message from someone else: you cannot read files, run commands or use any tool but the canon MCP server's. Answer from the conversation and what you already know, and say so when something needs your owner.";
470
- var RISKY = /\brm\s+-[a-z]*[rf]|\bgit\s+(reset\s+--hard|push\s+(-f|--force))|\bdrop\s+(table|database)\b|\bmkfs\b|\bdd\s+if=/i;
933
+ var GUEST2 = "Only your owner can let you use tools in this conversation, and this turn holds a message from someone else: you cannot read files, run commands or use any tool but the canon MCP server's. Answer from the conversation and what you already know, and say so when something needs your owner.";
471
934
  var IMAGE = /\.(png|jpe?g|gif|webp)$/i;
472
- var isRecord2 = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
473
- var guestTurn = /* @__PURE__ */ __name((turn) => !turn.input.messages.every((message2) => message2.isOwner || message2.trustedAgent === true), "guestTurn");
474
- var text = /* @__PURE__ */ __name((value) => typeof value === "string" ? value : "", "text");
475
- var oneLine2 = /* @__PURE__ */ __name((value, max) => {
476
- const line = value.replace(/\s+/g, " ").trim();
477
- return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
478
- }, "oneLine");
479
935
  function checkCodex(codexPath = "codex") {
480
- const result = spawnSync(codexPath, ["--version"], { encoding: "utf8" });
936
+ const cli = installedCli(codexPath);
937
+ const result = spawnSync(cli.file, [...cli.args, "--version"], { encoding: "utf8", windowsHide: true });
481
938
  if (result.status !== 0) {
482
939
  throw new Error(`The codex driver needs the Codex CLI (npm install -g @openai/codex) or --codex-path: ${result.error?.message ?? text(result.stderr).trim()}`);
483
940
  }
@@ -485,20 +942,20 @@ function checkCodex(codexPath = "codex") {
485
942
  }
486
943
  __name(checkCodex, "checkCodex");
487
944
  function describe(item) {
488
- const paths = (Array.isArray(item.changes) ? item.changes : []).filter(isRecord2).map((change) => basename2(text(change.path)));
945
+ const paths = (Array.isArray(item.changes) ? item.changes : []).filter(isRecord).map((change) => basename3(text(change.path)));
489
946
  switch (item.type) {
490
947
  case "commandExecution": {
491
- const actions = (Array.isArray(item.commandActions) ? item.commandActions : []).filter(isRecord2).map((action) => text(action.command));
492
- return oneLine2(actions.filter(Boolean).join(" && ") || text(item.command) || "Run a command", 120);
948
+ const actions = (Array.isArray(item.commandActions) ? item.commandActions : []).filter(isRecord).map((action) => text(action.command));
949
+ return oneLine(actions.filter(Boolean).join(" && ") || text(item.command) || "Run a command", 120);
493
950
  }
494
951
  case "fileChange":
495
- return oneLine2(paths.length ? `Edit ${paths.join(", ")}` : "Edit files", 120);
952
+ return oneLine(paths.length ? `Edit ${paths.join(", ")}` : "Edit files", 120);
496
953
  case "mcpToolCall":
497
954
  return `${text(item.server)}: ${text(item.tool)}`;
498
955
  case "webSearch":
499
- return oneLine2(`Search the web${text(item.query) ? `: ${text(item.query)}` : ""}`, 120);
956
+ return oneLine(`Search the web${text(item.query) ? `: ${text(item.query)}` : ""}`, 120);
500
957
  case "imageView":
501
- return `View ${basename2(text(item.path))}`;
958
+ return `View ${basename3(text(item.path))}`;
502
959
  case "dynamicToolCall":
503
960
  return text(item.tool) || "Tool";
504
961
  default:
@@ -506,27 +963,19 @@ function describe(item) {
506
963
  }
507
964
  }
508
965
  __name(describe, "describe");
509
- async function approve2(turn, request) {
510
- const asked = createHash2("sha256").update(JSON.stringify([request.scopeKey, request.title, request.detail])).digest("hex").slice(0, 12);
511
- const answer = await turn.ask({ kind: "approve", ...request, id: `${request.id}#${asked}` }).catch(() => void 0);
512
- if (!answer) return "stopped";
513
- if (answer.status !== "answered" || !answer.allow) return "denied";
514
- return answer.scope === "session" ? "session" : "once";
515
- }
516
- __name(approve2, "approve");
517
966
  var DECISION = { session: "acceptForSession", once: "accept", denied: "decline", stopped: "cancel" };
518
967
  async function answerCodexRequest(turn, method, params, items, rpcId) {
519
968
  const id = text(params.approvalId) || text(params.itemId) || text(params.elicitationId) || `request-${String(rpcId)}`;
520
969
  if (method === "item/commandExecution/requestApproval") {
521
970
  const command = text(params.command);
522
971
  const stdin = params.kind === "writeStdin";
523
- const network = isRecord2(params.networkApprovalContext) ? params.networkApprovalContext : void 0;
524
- const parsed = (Array.isArray(params.commandActions) ? params.commandActions : []).filter(isRecord2).map((action) => text(action.command)).filter(Boolean);
972
+ const network = isRecord(params.networkApprovalContext) ? params.networkApprovalContext : void 0;
973
+ const parsed = (Array.isArray(params.commandActions) ? params.commandActions : []).filter(isRecord).map((action) => text(action.command)).filter(Boolean);
525
974
  const destination = network && `${text(network.host)}${text(network.protocol) ? ` (${text(network.protocol)})` : ""}`;
526
975
  const scope = callScope("exec", [command, text(params.cwd), network ?? null, params.additionalPermissions ?? null]);
527
- const verdict = await approve2(turn, {
976
+ const verdict = await approve(turn, {
528
977
  id,
529
- title: oneLine2(text(params.reason) || (command ? `${stdin ? "Send input to" : "Run"}: ${parsed.join(" && ") || command}` : `Allow network access to ${destination || "the network"}`), 200),
978
+ title: oneLine(text(params.reason) || (command ? `${stdin ? "Send input to" : "Run"}: ${parsed.join(" && ") || command}` : `Allow network access to ${destination || "the network"}`), 200),
530
979
  detail: [
531
980
  command,
532
981
  text(params.cwd) && `in ${text(params.cwd)}`,
@@ -541,24 +990,24 @@ async function answerCodexRequest(turn, method, params, items, rpcId) {
541
990
  return { decision: DECISION[verdict] };
542
991
  }
543
992
  if (method === "item/fileChange/requestApproval") {
544
- const changes = (Array.isArray(items.get(text(params.itemId))?.changes) ? items.get(text(params.itemId)).changes : []).filter(isRecord2);
993
+ const changes = (Array.isArray(items.get(text(params.itemId))?.changes) ? items.get(text(params.itemId)).changes : []).filter(isRecord);
545
994
  const paths = [...new Set(changes.map((change) => text(change.path)))].sort();
546
- const verdict = await approve2(turn, {
995
+ const verdict = await approve(turn, {
547
996
  id,
548
- title: oneLine2(text(params.reason) || (paths.length ? `Edit ${paths.map((path) => basename2(path)).join(", ")}` : "Edit files"), 200),
997
+ title: oneLine(text(params.reason) || (paths.length ? `Edit ${paths.map((path) => basename3(path)).join(", ")}` : "Edit files"), 200),
549
998
  detail: [...changes.map((change) => `${text(change.path)}
550
999
  ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text(params.grantRoot)}`].filter(Boolean).join("\n\n"),
551
1000
  scopeKey: callScope("apply_patch", paths),
552
1001
  allowSession: paths.length > 0 && !params.grantRoot,
553
- ...changes.some((change) => isRecord2(change.kind) && change.kind.type === "delete") ? { risk: "destructive" } : {}
1002
+ ...changes.some((change) => isRecord(change.kind) && change.kind.type === "delete") ? { risk: "destructive" } : {}
554
1003
  });
555
1004
  return { decision: DECISION[verdict] };
556
1005
  }
557
1006
  if (method === "item/permissions/requestApproval") {
558
- const permissions = isRecord2(params.permissions) ? params.permissions : {};
559
- const verdict = await approve2(turn, {
1007
+ const permissions = isRecord(params.permissions) ? params.permissions : {};
1008
+ const verdict = await approve(turn, {
560
1009
  id,
561
- title: oneLine2(text(params.reason) || "Allow Codex more access", 200),
1010
+ title: oneLine(text(params.reason) || "Allow Codex more access", 200),
562
1011
  detail: JSON.stringify(permissions),
563
1012
  scopeKey: callScope("permissions", permissions),
564
1013
  allowSession: true
@@ -566,16 +1015,16 @@ ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text
566
1015
  return verdict === "session" || verdict === "once" ? { permissions, scope: verdict === "session" ? "session" : "turn" } : { permissions: {}, scope: "turn" };
567
1016
  }
568
1017
  if (method === "mcpServer/elicitation/request") {
569
- const meta = isRecord2(params._meta) ? params._meta : {};
1018
+ const meta = isRecord(params._meta) ? params._meta : {};
570
1019
  if (meta.codex_approval_kind !== "mcp_tool_call") return { action: "decline", content: null, _meta: null };
571
1020
  const server = text(params.serverName);
572
1021
  const named = text(meta.call_id) || text(meta.callId);
573
1022
  const running = [...items.values()].filter((item) => item.type === "mcpToolCall" && item.server === server && item.status === "inProgress");
574
1023
  const call = named ? items.get(named) : running.length === 1 ? running[0] : void 0;
575
1024
  const tool = call && call.server === server ? text(call.tool) : "";
576
- const verdict = await approve2(turn, {
1025
+ const verdict = await approve(turn, {
577
1026
  id: `mcp-${String(rpcId)}`,
578
- title: oneLine2(text(params.message) || `Use ${server}${tool ? `: ${tool}` : ""}`, 200),
1027
+ title: oneLine(text(params.message) || `Use ${server}${tool ? `: ${tool}` : ""}`, 200),
579
1028
  detail: JSON.stringify(meta.tool_params ?? call?.arguments ?? {}),
580
1029
  scopeKey: `codex:mcp:${server}:${tool}`,
581
1030
  allowSession: tool !== ""
@@ -587,10 +1036,10 @@ ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text
587
1036
  };
588
1037
  }
589
1038
  if (method === "item/tool/requestUserInput") {
590
- const questions = (Array.isArray(params.questions) ? params.questions : []).filter(isRecord2);
1039
+ const questions = (Array.isArray(params.questions) ? params.questions : []).filter(isRecord);
591
1040
  const answers = {};
592
1041
  for (const [index, question] of questions.entries()) {
593
- const labels = [...new Set((Array.isArray(question.options) ? question.options : []).filter(isRecord2).map((option) => text(option.label)).filter(Boolean))];
1042
+ const labels = [...new Set((Array.isArray(question.options) ? question.options : []).filter(isRecord).map((option) => text(option.label)).filter(Boolean))];
594
1043
  const askId = questions.length > 1 ? `${id}#${index + 1}` : id;
595
1044
  const ask = labels.length ? { kind: "choose", id: askId, question: text(question.question), options: labels.map((label) => ({ id: label, label })) } : { kind: "text", id: askId, question: text(question.question), ...question.isSecret === true ? { secret: true } : {} };
596
1045
  const answer = await turn.ask(ask).catch(() => void 0);
@@ -602,55 +1051,11 @@ ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text
602
1051
  throw new Error(`The codex driver does not handle ${method}`);
603
1052
  }
604
1053
  __name(answerCodexRequest, "answerCodexRequest");
605
- function rpc(child, handlers) {
606
- let next = 0;
607
- const pending = /* @__PURE__ */ new Map();
608
- const write = /* @__PURE__ */ __name((message2) => {
609
- if (child.stdin?.writable) child.stdin.write(`${JSON.stringify(message2)}
610
- `);
611
- }, "write");
612
- child.stdin?.on("error", () => void 0);
613
- createInterface({ input: child.stdout }).on("error", () => void 0).on("line", (line) => {
614
- let message2;
615
- try {
616
- message2 = JSON.parse(line);
617
- } catch {
618
- return;
619
- }
620
- if (!isRecord2(message2)) return;
621
- const params = isRecord2(message2.params) ? message2.params : {};
622
- if (typeof message2.method !== "string") {
623
- const waiting = pending.get(Number(message2.id));
624
- pending.delete(Number(message2.id));
625
- if (isRecord2(message2.error)) waiting?.reject(new Error(text(message2.error.message) || "Codex refused the request"));
626
- else waiting?.resolve(message2.result);
627
- } else if (message2.id !== void 0) {
628
- handlers.request(message2.method, params, message2.id).then(
629
- (result) => write({ id: message2.id, result }),
630
- (error) => write({ id: message2.id, error: { code: -32e3, message: error instanceof Error ? error.message : String(error) } })
631
- );
632
- } else handlers.notify(message2.method, params);
633
- });
634
- child.stderr?.resume();
635
- return {
636
- call: /* @__PURE__ */ __name((method, params) => new Promise((resolve3, reject) => {
637
- next += 1;
638
- pending.set(next, { resolve: /* @__PURE__ */ __name((value) => resolve3(isRecord2(value) ? value : {}), "resolve"), reject });
639
- write({ id: next, method, params });
640
- }), "call"),
641
- notify: /* @__PURE__ */ __name((method) => write({ method }), "notify"),
642
- fail(error) {
643
- for (const waiting of pending.values()) waiting.reject(error);
644
- pending.clear();
645
- }
646
- };
647
- }
648
- __name(rpc, "rpc");
649
1054
  function codexDriver(options) {
650
- const cwd = realpathSync3(options.cwd);
1055
+ const cwd = realpathSync4(options.cwd);
651
1056
  const codexPath = options.codexPath ?? "codex";
652
1057
  const interruptGraceMs = options.interruptGraceMs ?? 5e3;
653
- const developerInstructions = withInstructions(SYSTEM2, options.instructions);
1058
+ const developerInstructions = withInstructions(SYSTEM3, options.instructions);
654
1059
  const running = /* @__PURE__ */ new Map();
655
1060
  const end = /* @__PURE__ */ __name(async (session) => {
656
1061
  const live = running.get(session);
@@ -661,8 +1066,8 @@ function codexDriver(options) {
661
1066
  async function threadSettings(turn, call, env) {
662
1067
  const guest = guestTurn(turn);
663
1068
  const configured = (await call("config/read", { cwd })).config;
664
- const settings = isRecord2(configured) ? configured : {};
665
- const servers = guest && isRecord2(settings.mcp_servers) ? Object.keys(settings.mcp_servers).filter((name) => name !== CANON) : [];
1069
+ const settings = isRecord(configured) ? configured : {};
1070
+ const servers = guest && isRecord(settings.mcp_servers) ? Object.keys(settings.mcp_servers).filter((name) => name !== CANON2) : [];
666
1071
  const apiKey = Boolean(env.OPENAI_API_KEY) && [void 0, null, "openai"].includes(settings.model_provider);
667
1072
  return {
668
1073
  cwd,
@@ -675,7 +1080,7 @@ function codexDriver(options) {
675
1080
  config: {
676
1081
  mcp_servers: {
677
1082
  ...Object.fromEntries(servers.map((name) => [name, { enabled: false }])),
678
- ...turn.tools.url ? { [CANON]: { url: turn.tools.url, default_tools_approval_mode: "approve", tool_timeout_sec: CANON_TOOL_TIMEOUT_SEC } } : {}
1083
+ ...turn.tools.url ? { [CANON2]: { url: turn.tools.url, default_tools_approval_mode: "approve", tool_timeout_sec: CANON_TOOL_TIMEOUT_SEC } } : {}
679
1084
  },
680
1085
  // Questions go through Canon's ask_question alone: Codex's own request_user_input outside plan mode (a feature
681
1086
  // still under development) stays off even when the owner's configuration turns it on, so Codex never has two.
@@ -699,20 +1104,21 @@ function codexDriver(options) {
699
1104
  streaming: true,
700
1105
  activity: true,
701
1106
  tools: true,
702
- outboundFiles: { roots: [cwd, realpathSync3(tmpdir3())] }
1107
+ outboundFiles: { roots: [cwd, realpathSync4(tmpdir4())] }
703
1108
  },
704
1109
  async runTurn(turn) {
705
1110
  await running.get(turn.session)?.exited;
706
1111
  if (turn.signal.aborted) throw turn.signal.reason;
707
1112
  const guest = guestTurn(turn);
708
1113
  const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("CANON_")));
709
- const child = spawn(codexPath, ["app-server", "--listen", "stdio://"], { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
1114
+ const cli = installedCli(codexPath);
1115
+ const child = spawn3(cli.file, [...cli.args, "app-server", "--listen", "stdio://"], { cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
710
1116
  let exit = /* @__PURE__ */ __name(() => void 0, "exit");
711
1117
  const exited = new Promise((resolve3) => {
712
1118
  exit = resolve3;
713
1119
  });
714
1120
  const entry = { close: /* @__PURE__ */ __name(() => {
715
- child.kill("SIGTERM");
1121
+ stopCli(child);
716
1122
  }, "close"), exited };
717
1123
  running.set(turn.session, entry);
718
1124
  return new Promise((resolve3, reject) => {
@@ -749,19 +1155,19 @@ function codexDriver(options) {
749
1155
  const all = [...messages.values()];
750
1156
  const reply = (all.filter((message2) => message2.done && message2.final).at(-1) ?? all.filter((message2) => message2.done).at(-1) ?? all.at(-1))?.text ?? "";
751
1157
  if (ended.status === "completed") return settle({ reply: reply.trim() ? reply : null });
752
- const detail = isRecord2(ended.error) ? text(ended.error.message) : failure;
753
- settle(new Error(`Codex ended the turn ${ended.status === "interrupted" ? "interrupted" : `with an error${detail ? `: ${oneLine2(detail, 300)}` : ""}`}`));
1158
+ const detail = isRecord(ended.error) ? text(ended.error.message) : failure;
1159
+ settle(new Error(`Codex ended the turn ${ended.status === "interrupted" ? "interrupted" : `with an error${detail ? `: ${oneLine(detail, 300)}` : ""}`}`));
754
1160
  }, "conclude");
755
1161
  const guard = /* @__PURE__ */ __name((item) => {
756
- if (!guest || GUEST_ITEMS.has(text(item.type)) || item.type === "mcpToolCall" && (item.server === CANON || item.server === "codex" && OWN_LISTINGS.has(text(item.tool)))) return;
1162
+ if (!guest || GUEST_ITEMS.has(text(item.type)) || item.type === "mcpToolCall" && (item.server === CANON2 || item.server === "codex" && OWN_LISTINGS.has(text(item.tool)))) return;
757
1163
  interrupt();
758
1164
  entry.close();
759
1165
  const what = item.type === "mcpToolCall" ? `mcpToolCall (${describe(item)})` : text(item.type) || "an unknown item";
760
1166
  settle(new DriverSessionError(`Codex started ${what} in a turn holding someone other than the owner's message`));
761
1167
  }, "guard");
762
- const link = rpc(child, {
1168
+ const link = stdioRpc(child, {
763
1169
  notify(method, params) {
764
- const item = isRecord2(params.item) ? params.item : void 0;
1170
+ const item = isRecord(params.item) ? params.item : void 0;
765
1171
  if (item && method === "item/started") guard(item);
766
1172
  if (item) items.set(text(item.id), item);
767
1173
  if (!ours(params)) return;
@@ -777,15 +1183,15 @@ function codexDriver(options) {
777
1183
  const title = describe(item);
778
1184
  const state = method === "item/started" ? "running" : item.status === "failed" || item.status === "declined" ? "failed" : "done";
779
1185
  if (title) turn.onActivity({ id: text(item.id), kind: "tool", title, state });
780
- } else if (method === "error" && params.willRetry !== true && isRecord2(params.error)) {
1186
+ } else if (method === "error" && params.willRetry !== true && isRecord(params.error)) {
781
1187
  failure = text(params.error.message);
782
- } else if (method === "turn/completed" && isRecord2(params.turn)) {
1188
+ } else if (method === "turn/completed" && isRecord(params.turn)) {
783
1189
  completed.set(text(params.turn.id), params.turn);
784
1190
  conclude();
785
1191
  }
786
1192
  },
787
1193
  request: /* @__PURE__ */ __name((method, params, id) => answerCodexRequest(turn, method, params, items, id), "request")
788
- });
1194
+ }, { name: "Codex" });
789
1195
  const gone = /* @__PURE__ */ __name((error) => {
790
1196
  if (closed) return;
791
1197
  closed = true;
@@ -811,18 +1217,18 @@ function codexDriver(options) {
811
1217
  threadId = resume;
812
1218
  } else {
813
1219
  const started2 = await link.call("thread/start", settings);
814
- threadId = isRecord2(started2.thread) ? text(started2.thread.id) : "";
1220
+ threadId = isRecord(started2.thread) ? text(started2.thread.id) : "";
815
1221
  if (!threadId) throw new Error("Codex started no thread");
816
1222
  turn.state.set(THREAD, threadId);
817
1223
  }
818
1224
  if (turn.signal.aborted) return finish();
819
1225
  const started = await link.call("turn/start", { threadId, input: [
820
- ...guest ? [{ type: "text", text: GUEST, text_elements: [] }] : [],
1226
+ ...guest ? [{ type: "text", text: GUEST2, text_elements: [] }] : [],
821
1227
  { type: "text", text: turn.prompt, text_elements: [] },
822
1228
  // Images go to the model as images; every attachment's path is also in the prompt for Codex to read.
823
1229
  ...turn.files.filter((file) => IMAGE.test(file)).map((path) => ({ type: "localImage", path }))
824
1230
  ] });
825
- nativeTurn = isRecord2(started.turn) ? text(started.turn.id) : "";
1231
+ nativeTurn = isRecord(started.turn) ? text(started.turn.id) : "";
826
1232
  if (!nativeTurn) throw new Error("Codex started no turn");
827
1233
  if (turn.signal.aborted) interrupt();
828
1234
  conclude();
@@ -913,31 +1319,43 @@ function connectorDriver() {
913
1319
  },
914
1320
  async claim(inputId, requestId, options = {}) {
915
1321
  const entry = find(inputId);
916
- if (entry.state === "claiming") {
917
- if (entry.requestId !== requestId) throw new ConnectorRelayError(409, "CONNECTOR_CLAIM_CONFLICT", "This input belongs to another claim");
918
- throw new ConnectorRelayError(503, "CONNECTOR_RELAY_BUSY", "This claim is still being confirmed; try again");
1322
+ if (entry.state === "claiming" && entry.requestId !== requestId) {
1323
+ throw new ConnectorRelayError(409, "CONNECTOR_CLAIM_CONFLICT", "This input belongs to another claim");
919
1324
  }
920
- if (entry.state !== "pending") {
1325
+ if (entry.state !== "pending" && entry.state !== "claiming") {
921
1326
  if (entry.requestId !== void 0 && entry.requestId !== requestId) {
922
1327
  throw new ConnectorRelayError(409, "CONNECTOR_CLAIM_CONFLICT", "This input belongs to another claim");
923
1328
  }
924
1329
  return { inputId, requestId, runAllowed: false, state: entry.state };
925
1330
  }
926
- entry.state = "claiming";
927
- entry.requestId = requestId;
1331
+ if (entry.confirmation) throw new ConnectorRelayError(503, "CONNECTOR_RELAY_BUSY", "This claim is still being confirmed; retry the same request id");
1332
+ if (entry.state === "pending") {
1333
+ entry.state = "claiming";
1334
+ entry.requestId = requestId;
1335
+ }
1336
+ const confirmation = entry.confirm().then(() => "ok", () => {
1337
+ if (entry.state === "claiming") {
1338
+ entry.state = "pending";
1339
+ entry.requestId = void 0;
1340
+ }
1341
+ return "refused";
1342
+ }).finally(() => {
1343
+ entry.confirmation = void 0;
1344
+ });
1345
+ entry.confirmation = confirmation;
928
1346
  const deadline = options.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
929
1347
  let timer;
930
1348
  const late = new Promise((resolve3) => {
931
1349
  timer = setTimeout(resolve3, Math.max(0, deadline - Date.now()), "late");
932
1350
  });
933
- const confirmed = await Promise.race([entry.confirm().then(() => "ok", () => "refused"), late]).finally(() => clearTimeout(timer));
934
- if (entry.state === "claiming" && confirmed === "ok") {
1351
+ const confirmed = await Promise.race([confirmation, late]).finally(() => clearTimeout(timer));
1352
+ const state = entry.state;
1353
+ if (state === "claiming" && confirmed === "ok") {
935
1354
  entry.state = "claimed";
936
1355
  return { inputId, requestId, runAllowed: true, state: "claimed", input: entry.input };
937
1356
  }
938
- if (entry.state !== "claiming") throw withdrawn();
939
- entry.state = "pending";
940
- entry.requestId = void 0;
1357
+ if (state === "claimed") return { inputId, requestId, runAllowed: false, state: "claimed" };
1358
+ if (state === "withdrawn" || state === "answered") throw withdrawn();
941
1359
  throw new ConnectorRelayError(503, "CONNECTOR_RELAY_BUSY", "Canon could not confirm this claim now; try again");
942
1360
  },
943
1361
  reply: /* @__PURE__ */ __name((inputId, requestId, text3) => answer(inputId, requestId, { outcome: "reply", text: text3 }), "reply"),
@@ -952,7 +1370,10 @@ function connectorDriver() {
952
1370
  activity: false,
953
1371
  tools: false,
954
1372
  outboundFiles: false,
955
- remote: true
1373
+ remote: true,
1374
+ // A turn here is an input waiting for (or with) the assistant, not work on this host: one per room, so a room
1375
+ // whose assistant is away never keeps another room's input from being offered.
1376
+ maxConcurrentTurns: Number.POSITIVE_INFINITY
956
1377
  },
957
1378
  inbox,
958
1379
  runTurn(turn) {
@@ -995,117 +1416,10 @@ function connectorDriver() {
995
1416
  }
996
1417
  __name(connectorDriver, "connectorDriver");
997
1418
 
998
- // src/runner/home.ts
999
- import { randomBytes } from "node:crypto";
1000
- import { closeSync, constants as constants2, fstatSync, fsyncSync, linkSync, mkdirSync, openSync, readSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
1001
- import { lstat, mkdir, realpath as realpath2 } from "node:fs/promises";
1002
- import { dirname as dirname3 } from "node:path";
1003
- var runnerLockName = /* @__PURE__ */ __name((environmentId, agentId) => `runner-${environmentId}-${agentId}`.replace(/[^\w.-]/g, "_"), "runnerLockName");
1004
- function writeDurable(file, text3) {
1005
- const directory = dirname3(file);
1006
- const created = mkdirSync(directory, { recursive: true, mode: 448 });
1007
- const temporary = `${file}.${process.pid}.tmp`;
1008
- const fd = openSync(temporary, "w", 384);
1009
- try {
1010
- writeFileSync(fd, text3);
1011
- fsyncSync(fd);
1012
- } finally {
1013
- closeSync(fd);
1014
- }
1015
- renameSync(temporary, file);
1016
- if (process.platform === "win32") return;
1017
- const last = created ? dirname3(created) : directory;
1018
- for (let path = directory; ; path = dirname3(path)) {
1019
- const dir = openSync(path, "r");
1020
- try {
1021
- fsyncSync(dir);
1022
- } finally {
1023
- closeSync(dir);
1024
- }
1025
- if (path === last) break;
1026
- }
1027
- }
1028
- __name(writeDurable, "writeDurable");
1029
- function privateKey(file, bytes = 32) {
1030
- const refuse = /* @__PURE__ */ __name(() => {
1031
- throw new Error(`Refusing to start: ${file} must be a regular file of ${bytes} bytes, owned by this user and private to it (0600).`);
1032
- }, "refuse");
1033
- const read = /* @__PURE__ */ __name(() => {
1034
- let fd2;
1035
- try {
1036
- fd2 = openSync(file, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
1037
- } catch (error) {
1038
- return error.code === "ENOENT" ? void 0 : refuse();
1039
- }
1040
- try {
1041
- const info = fstatSync(fd2);
1042
- const uid = process.getuid?.();
1043
- if (!info.isFile() || info.size !== bytes || uid !== void 0 && (info.uid !== uid || (info.mode & 63) !== 0)) refuse();
1044
- const key = Buffer.alloc(bytes);
1045
- if (readSync(fd2, key, 0, bytes, 0) !== bytes) refuse();
1046
- return key;
1047
- } finally {
1048
- closeSync(fd2);
1049
- }
1050
- }, "read");
1051
- const existing = read();
1052
- if (existing) return existing;
1053
- const directory = dirname3(file);
1054
- mkdirSync(directory, { recursive: true, mode: 448 });
1055
- const temporary = `${file}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
1056
- const fd = openSync(temporary, "wx", 384);
1057
- try {
1058
- writeSync(fd, randomBytes(bytes));
1059
- fsyncSync(fd);
1060
- } finally {
1061
- closeSync(fd);
1062
- }
1063
- try {
1064
- linkSync(temporary, file);
1065
- } catch (error) {
1066
- if (error.code !== "EEXIST") throw error;
1067
- } finally {
1068
- rmSync(temporary, { force: true });
1069
- }
1070
- const dir = openSync(directory, "r");
1071
- try {
1072
- fsyncSync(dir);
1073
- } finally {
1074
- closeSync(dir);
1075
- }
1076
- return read() ?? refuse();
1077
- }
1078
- __name(privateKey, "privateKey");
1079
- async function assertPrivateHome(home) {
1080
- const uid = process.getuid?.();
1081
- if (uid === void 0) return;
1082
- const fail2 = /* @__PURE__ */ __name((path, problem) => {
1083
- throw new Error(`Refusing to start: ${path} ${problem}. CANON_HOME must be a private 0700 directory whose ancestors only its owner or root can modify.`);
1084
- }, "fail");
1085
- await mkdir(home, { recursive: true, mode: 448 }).catch(() => void 0);
1086
- let resolved;
1087
- try {
1088
- resolved = await realpath2(home);
1089
- } catch {
1090
- return fail2(home, "does not exist");
1091
- }
1092
- if (resolved !== home) fail2(home, `is not canonical (it resolves to ${resolved})`);
1093
- const stat3 = await lstat(home);
1094
- if (!stat3.isDirectory() || stat3.uid !== uid) fail2(home, "is not a directory owned by this user");
1095
- if (stat3.mode & 63) fail2(home, `is accessible to other users (mode ${(stat3.mode & 511).toString(8)})`);
1096
- for (let path = dirname3(home); ; path = dirname3(path)) {
1097
- const ancestor = await lstat(path);
1098
- if (ancestor.uid !== uid && ancestor.uid !== 0) fail2(path, "is owned by another user");
1099
- if (ancestor.mode & 18) fail2(path, "is writable by other users");
1100
- if (path === dirname3(path)) return;
1101
- }
1102
- }
1103
- __name(assertPrivateHome, "assertPrivateHome");
1104
-
1105
1419
  // src/runner/errands.ts
1106
1420
  import { randomBytes as randomBytes2 } from "node:crypto";
1107
- import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, lstatSync, mkdirSync as mkdirSync2, openSync as openSync2, readdirSync, readFileSync, unlinkSync, watch } from "node:fs";
1108
- import { join } from "node:path";
1421
+ import { closeSync, constants as constants2, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync as readFileSync2, unlinkSync, watch } from "node:fs";
1422
+ import { join as join2 } from "node:path";
1109
1423
  var ERRAND_KINDS = ["tell", "post", "errand", "cancel"];
1110
1424
  var ERRAND_TEXT_MAX_BYTES = MLS_DEV_TEXT_MAX_BYTES;
1111
1425
  var ERRAND_WHY_MAX_BYTES = 4 * 1024;
@@ -1123,7 +1437,7 @@ function errandsDirectory(home, environmentId, agentId) {
1123
1437
  for (const segment of [environmentId, agentId]) {
1124
1438
  if (!/^[\w.:-]{1,200}$/.test(segment) || /^\.+$/.test(segment)) throw new Error(`Not an id: ${JSON.stringify(segment)}`);
1125
1439
  }
1126
- return join(home, "runner", environmentId, agentId, "errands");
1440
+ return join2(home, "runner", environmentId, agentId, "errands");
1127
1441
  }
1128
1442
  __name(errandsDirectory, "errandsDirectory");
1129
1443
  function custody(stat3, kind) {
@@ -1139,8 +1453,8 @@ function custody(stat3, kind) {
1139
1453
  }
1140
1454
  __name(custody, "custody");
1141
1455
  function errandInbox(directory) {
1142
- const inbox = join(directory, "inbox");
1143
- mkdirSync2(inbox, { recursive: true, mode: 448 });
1456
+ const inbox = join2(directory, "inbox");
1457
+ mkdirSync(inbox, { recursive: true, mode: 448 });
1144
1458
  const problem = custody(lstatSync(inbox), "directory");
1145
1459
  if (problem) throw new Error(`The errand inbox ${inbox} ${problem}`);
1146
1460
  return inbox;
@@ -1173,16 +1487,16 @@ function queueErrand(directory, input) {
1173
1487
  const command = { version: 1, id: randomBytes2(16).toString("hex"), ...fields, createdAt: now.toISOString() };
1174
1488
  const problem = commandProblem(command, command.id);
1175
1489
  if (problem) throw new Error(`Not a valid ${input.kind}: ${problem}.`);
1176
- writeDurable(join(errandInbox(directory), `${now.getTime()}-${command.id}.json`), JSON.stringify(command));
1490
+ writeDurable(join2(errandInbox(directory), `${now.getTime()}-${command.id}.json`), JSON.stringify(command));
1177
1491
  return command;
1178
1492
  }
1179
1493
  __name(queueErrand, "queueErrand");
1180
1494
  function errandJournal(directory) {
1181
- const folder = join(directory, "journal");
1182
- const file = /* @__PURE__ */ __name((id) => join(folder, `${id}.json`), "file");
1495
+ const folder = join2(directory, "journal");
1496
+ const file = /* @__PURE__ */ __name((id) => join2(folder, `${id}.json`), "file");
1183
1497
  const read = /* @__PURE__ */ __name((path) => {
1184
1498
  try {
1185
- return JSON.parse(readFileSync(path, "utf8"));
1499
+ return JSON.parse(readFileSync2(path, "utf8"));
1186
1500
  } catch {
1187
1501
  return void 0;
1188
1502
  }
@@ -1204,7 +1518,7 @@ function errandJournal(directory) {
1204
1518
  names = readdirSync(folder);
1205
1519
  } catch {
1206
1520
  }
1207
- return names.filter((name) => /^[0-9a-f]{32}\.json$/.test(name)).flatMap((name) => read(join(folder, name)) ?? []);
1521
+ return names.filter((name) => /^[0-9a-f]{32}\.json$/.test(name)).flatMap((name) => read(join2(folder, name)) ?? []);
1208
1522
  }
1209
1523
  };
1210
1524
  }
@@ -1229,24 +1543,24 @@ __name(awaitErrand, "awaitErrand");
1229
1543
  function readCommand(path, id) {
1230
1544
  let fd;
1231
1545
  try {
1232
- fd = openSync2(path, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
1546
+ fd = openSync(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
1233
1547
  } catch (error) {
1234
1548
  const code = error.code;
1235
1549
  return { problem: code === "ELOOP" || code === "EMLINK" ? "is a symbolic link" : `cannot be opened (${code})` };
1236
1550
  }
1237
1551
  try {
1238
- const problem = custody(fstatSync2(fd), "file");
1552
+ const problem = custody(fstatSync(fd), "file");
1239
1553
  if (problem) return { problem };
1240
1554
  let value;
1241
1555
  try {
1242
- value = JSON.parse(readFileSync(fd, "utf8"));
1556
+ value = JSON.parse(readFileSync2(fd, "utf8"));
1243
1557
  } catch {
1244
1558
  return { problem: "is not JSON" };
1245
1559
  }
1246
1560
  const invalid2 = commandProblem(value, id);
1247
1561
  return invalid2 ? { problem: `is not a valid command: ${invalid2}` } : value;
1248
1562
  } finally {
1249
- closeSync2(fd);
1563
+ closeSync(fd);
1250
1564
  }
1251
1565
  }
1252
1566
  __name(readCommand, "readCommand");
@@ -1271,7 +1585,7 @@ function watchErrandInbox(directory, handler, options) {
1271
1585
  ${name}`, `Errands: ${name} in the inbox is not a command file; it is left alone.`);
1272
1586
  continue;
1273
1587
  }
1274
- const path = join(inbox, name);
1588
+ const path = join2(inbox, name);
1275
1589
  try {
1276
1590
  if (handler.taken(id) || await handler.take(id, readCommand(path, id))) unlinkSync(path);
1277
1591
  } catch (error) {
@@ -1321,8 +1635,8 @@ __name(watchErrandInbox, "watchErrandInbox");
1321
1635
 
1322
1636
  // src/runner/mls.ts
1323
1637
  import { createHash as createHash4, createHmac } from "node:crypto";
1324
- import { constants as constants4 } from "node:fs";
1325
- import { open as open2, realpath as realpath3 } from "node:fs/promises";
1638
+ import { constants as constants3 } from "node:fs";
1639
+ import { open as open2, realpath as realpath2 } from "node:fs/promises";
1326
1640
  import { isAbsolute, resolve } from "node:path";
1327
1641
  var fail = /* @__PURE__ */ __name(() => {
1328
1642
  throw new Error("MLS_AUTHORITY_REQUIRED");
@@ -1438,8 +1752,8 @@ function object(value, required, optional = []) {
1438
1752
  __name(object, "object");
1439
1753
  async function loadMlsDevConfiguration(filePath, input) {
1440
1754
  if (!isMlsEnvironmentId(input.environmentId)) invalid();
1441
- if (typeof filePath !== "string" || !isAbsolute(filePath) || filePath.includes("\0") || resolve(filePath) !== filePath || await realpath3(filePath).catch(() => "") !== filePath) invalid();
1442
- const file = await open2(filePath, constants4.O_RDONLY | constants4.O_NOFOLLOW).catch(() => invalid());
1755
+ if (typeof filePath !== "string" || !isAbsolute(filePath) || filePath.includes("\0") || resolve(filePath) !== filePath || await realpath2(filePath).catch(() => "") !== filePath) invalid();
1756
+ const file = await open2(filePath, constants3.O_RDONLY | constants3.O_NOFOLLOW).catch(() => invalid());
1443
1757
  let value;
1444
1758
  try {
1445
1759
  const before = await file.stat();
@@ -1447,7 +1761,7 @@ async function loadMlsDevConfiguration(filePath, input) {
1447
1761
  const bytes = Buffer.alloc(MAX_CONFIGURATION_BYTES + 1);
1448
1762
  const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
1449
1763
  const after = await file.stat();
1450
- if (bytesRead !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || await realpath3(filePath) !== filePath) invalid();
1764
+ if (bytesRead !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || await realpath2(filePath) !== filePath) invalid();
1451
1765
  value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, bytesRead)));
1452
1766
  } catch {
1453
1767
  invalid();
@@ -1503,11 +1817,18 @@ function mlsDevRoomPackageId(identity, room) {
1503
1817
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
1504
1818
  }
1505
1819
  __name(mlsDevRoomPackageId, "mlsDevRoomPackageId");
1820
+ async function assertMlsVaultInHome(endpoint, provisioned) {
1821
+ const { status, binding } = await endpoint.mlsDevDevice.read();
1822
+ if (status === "absent" || status === "revoked" || provisioned({ ...binding, revision: 0 })) return;
1823
+ throw new Error("This agent's MLS device has no vault in this home. A device set up with an operator's configuration keeps needing it: run the agent with that --mls-dev-config <path> again (in createRunner, mls: await loadMlsDevConfiguration(path, { environmentId })). A vault that was lost cannot be recovered.");
1824
+ }
1825
+ __name(assertMlsVaultInHome, "assertMlsVaultInHome");
1506
1826
  async function runMlsDevSetup(endpoint, action, room, provision, provisioned) {
1507
1827
  const identity = await endpoint.ready();
1508
1828
  const { environmentId } = identity, devices = endpoint.mlsDevDevice;
1509
1829
  if (!isMlsEnvironmentId(environmentId) || !devices || !endpoint.mlsDevKeyPackages || !endpoint.mlsDevGroups) throw new Error("MLS_DEV_SETUP_UNAVAILABLE");
1510
1830
  if ((action === "publish" || action === "refresh") !== !!room) throw new Error("MLS_DEV_SETUP_ARGUMENTS_INVALID");
1831
+ if (provisioned && action !== "status") await assertMlsVaultInHome(endpoint, provisioned);
1511
1832
  const enroll = /* @__PURE__ */ __name(async () => {
1512
1833
  await devices.enroll();
1513
1834
  if ((await devices.read()).status !== "credential_committed") await devices.renew();
@@ -1560,14 +1881,14 @@ __name(runMlsDevSetup, "runMlsDevSetup");
1560
1881
 
1561
1882
  // src/runner/mls-bundled.ts
1562
1883
  import { createHash as createHash5 } from "node:crypto";
1563
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1564
- import { dirname as dirname4, join as join2 } from "node:path";
1884
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
1885
+ import { dirname as dirname4, join as join3 } from "node:path";
1565
1886
  import { fileURLToPath } from "node:url";
1566
1887
 
1567
1888
  // mls-custody.json
1568
1889
  var mls_custody_default = {
1569
1890
  schemaVersion: 1,
1570
- release: "mls-custody-cadb937436ce",
1891
+ release: "mls-custody-5ce777037493",
1571
1892
  protocolVersion: 1,
1572
1893
  rustVersion: "rustc 1.91.0 (f8297e351 2025-10-28)",
1573
1894
  sourceHashes: {
@@ -1579,15 +1900,15 @@ var mls_custody_default = {
1579
1900
  "mls/native/src/portable/json.rs": "d4e68556a051ffe9008686036597b1fd8239ae66fad67bd848c3f0e16bf8ccaf",
1580
1901
  "mls/native/src/portable/vault.rs": "14b9ff1e10ec4058e39a46511a489c7e82a9836e4175a768ac5d4b69c1dba8e5",
1581
1902
  "mls/native/src/portable/tests.rs": "7eadba2a49ed044470bb567d9b93fba44b64dc117b1da50a22187cff3dccb64e",
1582
- "mls/native/portable/build.mjs": "84c422a8a8aa6a7ec112a358bdeef080201aa4ea9a91f6d5726b05f46fa2a10c",
1903
+ "mls/native/portable/build.mjs": "ec81bfe27238b3117c4e855fa58b323e18a935ad7aa8ac18807376fb0bddc99b",
1583
1904
  "mls/core/open.rs": "8cdc03882f3b84dca72ab06afd437f162de01a0fe21327b019f17867dd59f5d0",
1584
1905
  "mls/core/protocol.rs": "7f3e021d6e4f1cc130b5dd3c98e0b0fa23edf813c72802c3fa3f8fb9e431c947",
1585
1906
  "mls/core/fault_storage.rs": "fdd6672a21f6ffee657142755637824541a5090b4f8a5fb4902f482556fb2793",
1586
1907
  "mls/core/mls_environment.rs": "390d11ee82ef8b108a7fffc7bb193ac7f3e470aa14e9643d029007aaaa29c4e4",
1587
- "mls/core/dev_identity.rs": "97df6bdb9daab66f0e3d4ca3b49bf9954fa06ac032c927d9ae6c83509b0166bd",
1588
- "mls/core/dev_key_package.rs": "3e7011fe8ab3044d80d84d04944068dbcf683141fcaba8a7a00bd2cfd103de72",
1908
+ "mls/core/dev_identity.rs": "df7282f0daa8babb3a9b9f397ad65d621784f9f54f5ca7a45fb3c8e82d58bbb1",
1909
+ "mls/core/dev_key_package.rs": "e8956f8dd4e365c22ac2356fb520759cdb613e344e1d44510ceee54b1cf9a418",
1589
1910
  "mls/core/dev_application.rs": "fafe245ace38e384da265bab2c12fef6b77c86b2c20477d00563ab891a54f94b",
1590
- "mls/core/dev_group.rs": "1d519a5d15394bbc163aef00e0ddacb9bb87631beaf8a13e52c76d628749152b",
1911
+ "mls/core/dev_group.rs": "d43f9ecdae79effdb9f177e76108487f81618dfc5b72b63618c41eff5896b2e0",
1591
1912
  "packages/test-fixtures/src/mlsDevEnrollmentProofV1.json": "50d7896e0a3a25b420412e84484e64f1e49480a3f298ee2d125f2f9188382810",
1592
1913
  "packages/test-fixtures/src/mlsDevCredentialV1.json": "24cd23af794b8a6e35caf050995020c6584b806de060969db4536f44b1188780",
1593
1914
  "packages/test-fixtures/src/mlsDevCredentialReconciliationV1.json": "bd4bbb3a2e5dc7921867a2e5936d1da77d536740b4360012c1f741f9caed2826",
@@ -1598,10 +1919,10 @@ var mls_custody_default = {
1598
1919
  platforms: {
1599
1920
  "darwin-arm64": {
1600
1921
  target: "aarch64-apple-darwin",
1601
- sha256: "918ae350f1d7bd339c4eee548f6668943217f5fa66022bb93f41a02992dd296c",
1922
+ sha256: "b09c82209cc7884c4521a310c727a8faaf387e9d0603443b1c1d9a55d87b7103",
1602
1923
  bytes: 2932976,
1603
- unitTestsPassed: 95,
1604
- commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1924
+ unitTestsPassed: 96,
1925
+ commit: "b9d1a4008749d1f144fd8db6ca4066de20422184",
1605
1926
  builder: {
1606
1927
  macos: "26.3.1",
1607
1928
  sdk: "26.4",
@@ -1610,10 +1931,10 @@ var mls_custody_default = {
1610
1931
  },
1611
1932
  "linux-x64": {
1612
1933
  target: "x86_64-unknown-linux-gnu",
1613
- sha256: "1c5a12d0213c53bdf59b5f36a7390a4b4c17f8a53c95e760ad8cb4905297d256",
1614
- bytes: 3389816,
1615
- unitTestsPassed: 95,
1616
- commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1934
+ sha256: "0b20495694534d9792b6920f1e3f375a2182b237a44cb2d384080dd17e7f83bb",
1935
+ bytes: 3389880,
1936
+ unitTestsPassed: 96,
1937
+ commit: "da679f4c60e2271c20b1f1958d0e5f6bfce6ef2d",
1617
1938
  builder: {
1618
1939
  image: "quay.io/pypa/manylinux_2_28_x86_64@sha256:e0adf18521ad35192df733a6c62d35df7801957d24e0470238aacecfab2188a3",
1619
1940
  dockerfileSha256: "fe8d4895b8bd304954441fd27c6273c8c00baa10a1ad912d3c3851b193b8ad75"
@@ -1621,15 +1942,28 @@ var mls_custody_default = {
1621
1942
  },
1622
1943
  "linux-arm64": {
1623
1944
  target: "aarch64-unknown-linux-gnu",
1624
- sha256: "371c5754cf0c437f561e8c49b3ea654c037311b951e11b1c5a29fb6b91656619",
1945
+ sha256: "649d90ba74a42b34025292bd942068fc08979e35f66c9d6ad358469f5022fac7",
1625
1946
  bytes: 3234232,
1626
- unitTestsPassed: 95,
1627
- commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1947
+ unitTestsPassed: 96,
1948
+ commit: "b9d1a4008749d1f144fd8db6ca4066de20422184",
1628
1949
  builder: {
1629
1950
  image: "quay.io/pypa/manylinux_2_28_aarch64@sha256:a3e02308c822fb6ce0d12248b0c941817ba5e1a1bc9e7bb5929f29c50eaa40a9",
1630
1951
  dockerfileSha256: "fe8d4895b8bd304954441fd27c6273c8c00baa10a1ad912d3c3851b193b8ad75"
1631
1952
  }
1632
1953
  }
1954
+ },
1955
+ wasm: {
1956
+ release: "mls-wasm-ddfdcc6039d7",
1957
+ commit: "ba57ec95af11e8b560fd7d71431c0ec14542d71e",
1958
+ rustVersion: "rustc 1.91.0 (f8297e351 2025-10-28)",
1959
+ wasm: {
1960
+ sha256: "137454981d2b6236dffa3dae0a7253e5f6d30578bce2cf50476adc8ca3d9aa02",
1961
+ bytes: 1794989
1962
+ },
1963
+ glue: {
1964
+ sha256: "744eb2267560f22aa54540fe9aa988588a7c27641610aae729b2ed75232e2a24",
1965
+ bytes: 13579
1966
+ }
1633
1967
  }
1634
1968
  };
1635
1969
 
@@ -1665,34 +1999,77 @@ var MLS_ENVIRONMENT_PINS = {
1665
1999
  };
1666
2000
  function packageRoot() {
1667
2001
  for (let directory = dirname4(fileURLToPath(import.meta.url)); ; directory = dirname4(directory)) {
1668
- const manifest = join2(directory, "package.json");
1669
- if (existsSync(manifest) && JSON.parse(readFileSync2(manifest, "utf8")).name === "@canonmsg/agent-sdk") return directory;
2002
+ const manifest = join3(directory, "package.json");
2003
+ if (existsSync2(manifest) && JSON.parse(readFileSync3(manifest, "utf8")).name === "@canonmsg/agent-sdk") return directory;
1670
2004
  if (dirname4(directory) === directory) return void 0;
1671
2005
  }
1672
2006
  }
1673
2007
  __name(packageRoot, "packageRoot");
2008
+ function helperRunsOnLibc() {
2009
+ if (process.platform !== "linux") return true;
2010
+ const report = process.report;
2011
+ const { excludeNetwork } = report;
2012
+ report.excludeNetwork = true;
2013
+ try {
2014
+ const version = report.getReport().header?.glibcVersionRuntime;
2015
+ const [major = 0, minor = 0] = (version ?? "").split(".").map(Number);
2016
+ return major > 2 || major === 2 && minor >= 28;
2017
+ } finally {
2018
+ report.excludeNetwork = excludeNetwork;
2019
+ }
2020
+ }
2021
+ __name(helperRunsOnLibc, "helperRunsOnLibc");
1674
2022
  function bundledMlsCustodyHelper() {
1675
2023
  const platform = `${process.platform}-${process.arch}`;
1676
2024
  const pin = mls_custody_default.platforms[platform];
1677
- const root = pin && packageRoot();
1678
- const path = root && join2(root, "dist", "mls", platform, "canon-mls-custody");
1679
- if (!pin || !path || !existsSync(path)) return void 0;
1680
- if (createHash5("sha256").update(readFileSync2(path)).digest("hex") !== pin.sha256) {
2025
+ const root = pin && helperRunsOnLibc() && packageRoot();
2026
+ const path = root && join3(root, "dist", "mls", platform, "canon-mls-custody");
2027
+ if (!pin || !path || !existsSync2(path)) return void 0;
2028
+ if (createHash5("sha256").update(readFileSync3(path)).digest("hex") !== pin.sha256) {
1681
2029
  throw new Error(`${path} is not the custody helper this package pins; reinstall @canonmsg/agent-sdk.`);
1682
2030
  }
1683
2031
  return { path, sha256: pin.sha256 };
1684
2032
  }
1685
2033
  __name(bundledMlsCustodyHelper, "bundledMlsCustodyHelper");
2034
+ function bundledMlsWasmCore() {
2035
+ const root = packageRoot();
2036
+ if (!root) return void 0;
2037
+ const directory = join3(root, "dist", "mls", "wasm");
2038
+ const hostPath = join3(root, "dist", "mls-wasm-host.js"), wasmPath = join3(directory, "canon_mls_wasm_bg.wasm"), gluePath = join3(directory, "canon_mls_wasm.js");
2039
+ if (![hostPath, wasmPath, gluePath].every((path) => existsSync2(path))) return void 0;
2040
+ for (const [path, pin] of [[wasmPath, mls_custody_default.wasm.wasm.sha256], [gluePath, mls_custody_default.wasm.glue.sha256]]) {
2041
+ if (createHash5("sha256").update(readFileSync3(path)).digest("hex") !== pin) throw new Error(`${path} is not the MLS core this package pins; reinstall @canonmsg/agent-sdk.`);
2042
+ }
2043
+ return { hostPath, wasmPath, wasmSha256: mls_custody_default.wasm.wasm.sha256, gluePath, glueSha256: mls_custody_default.wasm.glue.sha256 };
2044
+ }
2045
+ __name(bundledMlsWasmCore, "bundledMlsWasmCore");
1686
2046
 
1687
2047
  // src/runner/mls-managed.ts
1688
2048
  import { createHash as createHash6 } from "node:crypto";
1689
- import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2 } from "node:fs";
1690
- import { dirname as dirname5, join as join3 } from "node:path";
2049
+ import { chmodSync, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, renameSync } from "node:fs";
2050
+ import { dirname as dirname5, join as join4 } from "node:path";
1691
2051
  var sha256 = /* @__PURE__ */ __name((bytes) => createHash6("sha256").update(bytes).digest("hex"), "sha256");
2052
+ function custodyFor(home, environmentId) {
2053
+ const root = join4(home, "mls", environmentId);
2054
+ const keptOnCore = existsSync3(root) && readdirSync2(root).some((name) => existsSync3(join4(root, name, "wrapping.key")));
2055
+ const kept = keptOnCore ? bundledMlsWasmCore() : void 0;
2056
+ if (kept) return { wasm: kept };
2057
+ const helper = bundledMlsCustodyHelper();
2058
+ if (helper) return { helper };
2059
+ const wasm = bundledMlsWasmCore();
2060
+ return wasm ? { wasm } : {};
2061
+ }
2062
+ __name(custodyFor, "custodyFor");
2063
+ function defaultMlsDevice(home, environmentId) {
2064
+ if (!isMlsEnvironmentId(environmentId)) return void 0;
2065
+ const custody2 = custodyFor(home, environmentId);
2066
+ return custody2.helper || custody2.wasm ? managedMlsDevice({ home, environmentId, ...custody2 }) : void 0;
2067
+ }
2068
+ __name(defaultMlsDevice, "defaultMlsDevice");
1692
2069
  function managedMlsDevice(options) {
1693
2070
  const { environmentId } = options;
1694
- const helper = options.helper ?? bundledMlsCustodyHelper();
1695
- if (!helper) {
2071
+ const { helper, wasm } = options.helper ? { helper: options.helper, wasm: void 0 } : options.wasm ? options : custodyFor(options.home, environmentId);
2072
+ if (!helper && !wasm) {
1696
2073
  throw new Error(`@canonmsg/agent-sdk ships no MLS custody helper for ${process.platform}-${process.arch}: build one from mls/native in the Canon repository and give the runner --mls-dev-config <path> (docs/design/mls-dev-agent-host.md).`);
1697
2074
  }
1698
2075
  const pins = options.pins ?? MLS_ENVIRONMENT_PINS[environmentId];
@@ -1703,24 +2080,27 @@ function managedMlsDevice(options) {
1703
2080
  issuerKeys: pins.issuerKeys,
1704
2081
  receiptKeys: pins.receiptKeys
1705
2082
  });
1706
- const root = join3(options.home, "mls", environmentId);
1707
- const directory = /* @__PURE__ */ __name((context) => join3(root, sha256(Buffer.from(JSON.stringify([
2083
+ const root = join4(options.home, "mls", environmentId);
2084
+ const directory = /* @__PURE__ */ __name((context) => join4(root, sha256(Buffer.from(JSON.stringify([
1708
2085
  context.installationId,
1709
2086
  context.principal.type,
1710
2087
  context.principal.id,
1711
2088
  "ownershipVersion" in context.principal ? context.principal.ownershipVersion : null
1712
2089
  ]))).slice(0, 32)), "directory");
1713
- const vault = /* @__PURE__ */ __name((context) => join3(directory(context), "vault", "vault.bin"), "vault");
2090
+ const vault = /* @__PURE__ */ __name((context) => join4(directory(context), "vault", "vault.bin"), "vault");
2091
+ const wrappingKey = /* @__PURE__ */ __name((context) => join4(directory(context), "wrapping.key"), "wrappingKey");
2092
+ const made = /* @__PURE__ */ __name((context) => wasm ? wrappingKey(context) : vault(context), "made");
2093
+ const stranded = /* @__PURE__ */ __name((context) => new Error(wasm ? `This agent's MLS device was made with the native custody helper, which does not run on this host (${process.platform}-${process.arch}). It is still in ${directory(context)}: run the agent with this home where the helper runs (macOS on Apple silicon, or Linux with glibc 2.28 or later), with the architecture it was set up on. A device cannot move to the WASM core, and a new home is refused, since Canon keeps an agent to one installation.` : `This agent's MLS device was made with the WASM core, which this install of @canonmsg/agent-sdk does not have. It is still in ${directory(context)}: reinstall the package. A device cannot move to the native helper, and a new home is refused, since Canon keeps an agent to one installation.`), "stranded");
1714
2094
  let executable;
1715
2095
  function install() {
1716
- const target = join3(root, "bin", helper.sha256, "canon-mls-custody");
1717
- if (existsSync2(target) && sha256(readFileSync3(target)) === helper.sha256) return target;
1718
- mkdirSync3(dirname5(target), { recursive: true, mode: 448 });
2096
+ const target = join4(root, "bin", helper.sha256, "canon-mls-custody");
2097
+ if (existsSync3(target) && sha256(readFileSync4(target)) === helper.sha256) return target;
2098
+ mkdirSync2(dirname5(target), { recursive: true, mode: 448 });
1719
2099
  const temporary = `${target}.${process.pid}.tmp`;
1720
2100
  copyFileSync(helper.path, temporary);
1721
2101
  chmodSync(temporary, 448);
1722
- if (sha256(readFileSync3(temporary)) !== helper.sha256) throw new Error(`${helper.path} is not the pinned custody helper`);
1723
- renameSync2(temporary, target);
2102
+ if (sha256(readFileSync4(temporary)) !== helper.sha256) throw new Error(`${helper.path} is not the pinned custody helper`);
2103
+ renameSync(temporary, target);
1724
2104
  return target;
1725
2105
  }
1726
2106
  __name(install, "install");
@@ -1730,9 +2110,24 @@ function managedMlsDevice(options) {
1730
2110
  let found = devices.get(key);
1731
2111
  if (!found) {
1732
2112
  found = (async () => {
1733
- if (!provisioning && !existsSync2(vault(context))) throw new NodeMlsDevCustodyError();
1734
- mkdirSync3(dirname5(vault(context)), { recursive: true, mode: 448 });
1735
- const unlock = join3(key, "unlock");
2113
+ if (!provisioning && !existsSync3(made(context))) throw new NodeMlsDevCustodyError();
2114
+ mkdirSync2(dirname5(made(context)), { recursive: true, mode: 448 });
2115
+ if (wasm) {
2116
+ if (process.platform === "win32") {
2117
+ if (provisioning) secureWindowsDirectory(key);
2118
+ else assertPrivateWindowsFile(wrappingKey(context));
2119
+ }
2120
+ const { revision: _revision, ...identity } = context;
2121
+ return wasmMlsCustody({
2122
+ core: wasm,
2123
+ context: identity,
2124
+ keyPath: wrappingKey(context),
2125
+ issuerKeys: configuration.issuerKeys,
2126
+ receiptKeys: configuration.receiptKeys,
2127
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
2128
+ });
2129
+ }
2130
+ const unlock = join4(key, "unlock");
1736
2131
  if (provisioning) privateKey(unlock, 32);
1737
2132
  const text3 = `${JSON.stringify({
1738
2133
  version: 1,
@@ -1748,8 +2143,8 @@ function managedMlsDevice(options) {
1748
2143
  receiptKeys: configuration.receiptKeys
1749
2144
  }, null, 2)}
1750
2145
  `;
1751
- const file = join3(key, "native.json");
1752
- if (!existsSync2(file) || readFileSync3(file, "utf8") !== text3) writeDurable(file, text3);
2146
+ const file = join4(key, "native.json");
2147
+ if (!existsSync3(file) || readFileSync4(file, "utf8") !== text3) writeDurable(file, text3);
1753
2148
  return createPortableNodeMlsDevCustody({
1754
2149
  executablePath: executable ??= install(),
1755
2150
  executableSha256: helper.sha256,
@@ -1775,8 +2170,18 @@ function managedMlsDevice(options) {
1775
2170
  return {
1776
2171
  configuration,
1777
2172
  custody: custody2,
1778
- provision: /* @__PURE__ */ __name((context) => device(context, true).then((open4) => open4.provision(context)), "provision"),
1779
- provisioned: /* @__PURE__ */ __name((context) => existsSync2(vault(context)), "provisioned"),
2173
+ async provision(context) {
2174
+ await (await device(context, true)).provision(context);
2175
+ if (wasm && process.platform === "win32") {
2176
+ restrictWindowsFile(wrappingKey(context));
2177
+ assertPrivateWindowsFile(wrappingKey(context));
2178
+ }
2179
+ },
2180
+ provisioned(context) {
2181
+ if (existsSync3(made(context))) return true;
2182
+ if (existsSync3(wasm ? vault(context) : wrappingKey(context))) throw stranded(context);
2183
+ return false;
2184
+ },
1780
2185
  async close() {
1781
2186
  await Promise.allSettled([...devices.values()].map((open4) => open4.then((custody3) => custody3.close())));
1782
2187
  }
@@ -1786,10 +2191,10 @@ __name(managedMlsDevice, "managedMlsDevice");
1786
2191
 
1787
2192
  // src/runner/relay.ts
1788
2193
  import { createHmac as createHmac2, timingSafeEqual } from "node:crypto";
1789
- import { closeSync as closeSync3, constants as constants5, fsyncSync as fsyncSync2, mkdirSync as mkdirSync4, openSync as openSync3, renameSync as renameSync3, writeSync as writeSync2 } from "node:fs";
1790
- import { lstat as lstat2, open as open3, realpath as realpath4 } from "node:fs/promises";
2194
+ import { closeSync as closeSync2, constants as constants4, fsyncSync, mkdirSync as mkdirSync3, openSync as openSync2, renameSync as renameSync2, writeSync } from "node:fs";
2195
+ import { lstat, open as open3, realpath as realpath3 } from "node:fs/promises";
1791
2196
  import { createServer } from "node:http";
1792
- import { isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
2197
+ import { isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "node:path";
1793
2198
  var TOLERANCE_S = 300;
1794
2199
  function relaySecretKey(secret) {
1795
2200
  if (typeof secret !== "string" || !/^whsec_[A-Za-z0-9+/]+={0,2}$/.test(secret)) throw new Error("The relay channel secret must be a whsec_ value");
@@ -1849,6 +2254,8 @@ async function startRelayChannel(options) {
1849
2254
  return served.runner.relayRead(request.args.conversationId, { limit: request.args.limit, before: request.args.before, deadline });
1850
2255
  case "send":
1851
2256
  return served.runner.relaySend(request.args.conversationId, request.args.messageId, request.args.text, { deadline });
2257
+ case "start":
2258
+ return served.runner.relayStart(request.args.targetUserId, request.args.messageId, request.args.text, request.args.selection, { deadline });
1852
2259
  }
1853
2260
  }
1854
2261
  __name(dispatch, "dispatch");
@@ -1920,8 +2327,8 @@ async function readPrivateFile(path, maximum, what) {
1920
2327
  const fail2 = /* @__PURE__ */ __name(() => {
1921
2328
  throw new Error(`${what} must be a private file (0600, owned by this user, not a link): ${path}`);
1922
2329
  }, "fail");
1923
- if (typeof path !== "string" || !isAbsolute2(path) || path.includes("\0") || resolve2(path) !== path || await realpath4(path).catch(() => "") !== path) fail2();
1924
- const file = await open3(path, constants5.O_RDONLY | constants5.O_NOFOLLOW).catch(() => fail2());
2330
+ if (typeof path !== "string" || !isAbsolute2(path) || path.includes("\0") || resolve2(path) !== path || await realpath3(path).catch(() => "") !== path) fail2();
2331
+ const file = await open3(path, constants4.O_RDONLY | constants4.O_NOFOLLOW).catch(() => fail2());
1925
2332
  try {
1926
2333
  const info = await file.stat();
1927
2334
  if (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid?.() || (info.mode & 63) !== 0 || info.size < 1 || info.size > maximum) fail2();
@@ -1958,11 +2365,11 @@ async function loadRelayConfiguration(path, input) {
1958
2365
  return { environmentId: value.environmentId, agentId: value.agentId, listen: { host: listen.host, port: listen.port }, secret };
1959
2366
  }
1960
2367
  __name(loadRelayConfiguration, "loadRelayConfiguration");
1961
- var relayDirectory = /* @__PURE__ */ __name((home) => join4(home, "relay"), "relayDirectory");
1962
- var credentialFile = /* @__PURE__ */ __name((home) => join4(relayDirectory(home), "credential.json"), "credentialFile");
2368
+ var relayDirectory = /* @__PURE__ */ __name((home) => join5(home, "relay"), "relayDirectory");
2369
+ var credentialFile = /* @__PURE__ */ __name((home) => join5(relayDirectory(home), "credential.json"), "credentialFile");
1963
2370
  async function readRelayCredential(home, relay) {
1964
2371
  const path = credentialFile(home);
1965
- if (!await lstat2(path).then(() => true, () => false)) return void 0;
2372
+ if (!await lstat(path).then(() => true, () => false)) return void 0;
1966
2373
  let value;
1967
2374
  try {
1968
2375
  value = JSON.parse(await readPrivateFile(path, 4 * 1024, "The relay credential"));
@@ -1985,33 +2392,33 @@ function writeRelayCredential(home, relay, delivery) {
1985
2392
  generation: delivery.generation,
1986
2393
  credential: delivery.credential
1987
2394
  };
1988
- mkdirSync4(relayDirectory(home), { recursive: true, mode: 448 });
2395
+ mkdirSync3(relayDirectory(home), { recursive: true, mode: 448 });
1989
2396
  const file = credentialFile(home);
1990
2397
  const temporary = `${file}.${process.pid}.tmp`;
1991
- const fd = openSync3(temporary, "w", 384);
2398
+ const fd = openSync2(temporary, "w", 384);
1992
2399
  try {
1993
- writeSync2(fd, JSON.stringify(value));
1994
- fsyncSync2(fd);
2400
+ writeSync(fd, JSON.stringify(value));
2401
+ fsyncSync(fd);
1995
2402
  } finally {
1996
- closeSync3(fd);
2403
+ closeSync2(fd);
1997
2404
  }
1998
- renameSync3(temporary, file);
2405
+ renameSync2(temporary, file);
1999
2406
  return value;
2000
2407
  }
2001
2408
  __name(writeRelayCredential, "writeRelayCredential");
2002
2409
  function relayRunnerHome(home, installationId) {
2003
2410
  if (!CONNECTOR_RELAY_INSTALLATION_PATTERN.test(installationId)) throw new Error("Invalid relay installation");
2004
- const directory = join4(relayDirectory(home), installationId.slice(CONNECTOR_RELAY_INSTALLATION_PREFIX.length));
2005
- mkdirSync4(directory, { recursive: true, mode: 448 });
2411
+ const directory = join5(relayDirectory(home), installationId.slice(CONNECTOR_RELAY_INSTALLATION_PREFIX.length));
2412
+ mkdirSync3(directory, { recursive: true, mode: 448 });
2006
2413
  return directory;
2007
2414
  }
2008
2415
  __name(relayRunnerHome, "relayRunnerHome");
2009
2416
 
2010
2417
  // src/runner/runner.ts
2011
2418
  import { createHash as createHash8 } from "node:crypto";
2012
- import { readFileSync as readFileSync5, rmSync as rmSync3 } from "node:fs";
2013
- import { realpath as realpath5, stat as stat2 } from "node:fs/promises";
2014
- import { basename as basename3, isAbsolute as isAbsolute3, join as join6, sep } from "node:path";
2419
+ import { readFileSync as readFileSync6, rmSync as rmSync2 } from "node:fs";
2420
+ import { realpath as realpath4, stat as stat2 } from "node:fs/promises";
2421
+ import { basename as basename4, isAbsolute as isAbsolute3, join as join7, sep } from "node:path";
2015
2422
 
2016
2423
  // ../agent-tools/src/communication-tool.ts
2017
2424
  var CANON_COMMUNICATE_TOOL_NAME = "communicate";
@@ -2506,8 +2913,8 @@ __name(createLivePublisher, "createLivePublisher");
2506
2913
 
2507
2914
  // src/runner/mls-ask.ts
2508
2915
  import { createHash as createHash7 } from "node:crypto";
2509
- import { readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
2510
- import { join as join5 } from "node:path";
2916
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync } from "node:fs";
2917
+ import { join as join6 } from "node:path";
2511
2918
  var sha2562 = /* @__PURE__ */ __name((value) => createHash7("sha256").update(value).digest("hex"), "sha256");
2512
2919
  var pause = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
2513
2920
  setTimeout(resolve3, ms).unref?.();
@@ -2562,9 +2969,9 @@ function createMlsAsks(options, approvalScope2) {
2562
2969
  });
2563
2970
  }
2564
2971
  });
2565
- const file = /* @__PURE__ */ __name((conversationId, requestId) => join5(options.directory, `${sha2562(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2972
+ const file = /* @__PURE__ */ __name((conversationId, requestId) => join6(options.directory, `${sha2562(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2566
2973
  const save = /* @__PURE__ */ __name((journal) => writeDurable(file(journal.conversationId, journal.requestId), JSON.stringify(journal)), "save");
2567
- const forget = /* @__PURE__ */ __name((journal) => rmSync2(file(journal.conversationId, journal.requestId), { force: true }), "forget");
2974
+ const forget = /* @__PURE__ */ __name((journal) => rmSync(file(journal.conversationId, journal.requestId), { force: true }), "forget");
2568
2975
  const operationOf = /* @__PURE__ */ __name((conversationId, messageId) => `runner-mls-ask:${sha2562(JSON.stringify([conversationId, messageId])).slice(0, 48)}`, "operationOf");
2569
2976
  const withdrawalOf = /* @__PURE__ */ __name((journal) => operationOf(journal.conversationId, `${journal.requestId}:withdraw`), "withdrawalOf");
2570
2977
  const audienceChanged = /* @__PURE__ */ __name((error) => codeOf(error) === "MLS_DEV_AUDIENCE_CHANGED", "audienceChanged");
@@ -2899,18 +3306,18 @@ function createMlsAsks(options, approvalScope2) {
2899
3306
  async recover() {
2900
3307
  let names;
2901
3308
  try {
2902
- names = readdirSync2(options.directory).filter((name) => name.endsWith(".json"));
3309
+ names = readdirSync3(options.directory).filter((name) => name.endsWith(".json"));
2903
3310
  } catch {
2904
3311
  return;
2905
3312
  }
2906
3313
  for (const name of names) {
2907
3314
  let journal;
2908
3315
  try {
2909
- journal = JSON.parse(readFileSync4(join5(options.directory, name), "utf8"));
3316
+ journal = JSON.parse(readFileSync5(join6(options.directory, name), "utf8"));
2910
3317
  if (journal.version !== 1 || typeof journal.conversationId !== "string" || typeof journal.requestId !== "string" || !Array.isArray(journal.sends) || journal.sends.some((sent) => typeof sent?.operationId !== "string" || typeof sent.messageId !== "string")) throw new Error();
2911
3318
  for (const sent of journal.sends) sent.audience = snapshotMlsAudience(sent.audience);
2912
3319
  } catch {
2913
- rmSync2(join5(options.directory, name), { force: true });
3320
+ rmSync(join6(options.directory, name), { force: true });
2914
3321
  continue;
2915
3322
  }
2916
3323
  try {
@@ -3014,7 +3421,7 @@ var STATE_LIMIT_BYTES = 64 * 1024;
3014
3421
  var FAILURE_NOTICE = "Sorry, something went wrong and I could not finish this reply.";
3015
3422
  var STOPS_SHORT_NOTICE = "Sorry, my reply could not be delivered in full.";
3016
3423
  var RESTART_NOTICE = "Sorry, I was restarted before I could finish replying to this. Please send it again if you still need an answer.";
3017
- var CLIENT_TYPES = { code: "generic", claude: "claude-code", codex: "codex", connector: "generic" };
3424
+ var CLIENT_TYPES = { code: "generic", claude: "claude-code", codex: "codex", acp: "generic", connector: "generic" };
3018
3425
  var objectSchema = /* @__PURE__ */ __name((properties, required = []) => ({ type: "object", properties, required, additionalProperties: false }), "objectSchema");
3019
3426
  var MAX_QUESTION = 4e3;
3020
3427
  var MAX_OPTION = 120;
@@ -3214,6 +3621,7 @@ __name(runtimeDescriptor, "runtimeDescriptor");
3214
3621
  async function createRunner(options) {
3215
3622
  const { driver, home } = options;
3216
3623
  await assertPrivateHome(home);
3624
+ const mls = options.mls === false ? void 0 : options.mls ?? (options.endpoint ? void 0 : defaultMlsDevice(home, options.environmentId));
3217
3625
  const log = options.log ?? ((line) => console.error(`[canon-runner] ${line}`));
3218
3626
  const warn = /* @__PURE__ */ __name((what) => (error) => log(`${what}: ${error instanceof Error ? error.message : String(error)}`), "warn");
3219
3627
  const abortGraceMs = options.abortGraceMs ?? 1e4;
@@ -3313,9 +3721,9 @@ async function createRunner(options) {
3313
3721
  current.lane = current.lane.then(publish2).catch(() => void 0);
3314
3722
  }, "enqueue")
3315
3723
  }), "livePublisher");
3316
- const stateFile = /* @__PURE__ */ __name((session) => join6(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2563(JSON.stringify([scope, session]))}.json`), "stateFile");
3724
+ const stateFile = /* @__PURE__ */ __name((session) => join7(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2563(JSON.stringify([scope, session]))}.json`), "stateFile");
3317
3725
  const resetSession = /* @__PURE__ */ __name(async (session) => {
3318
- rmSync3(stateFile(session), { force: true });
3726
+ rmSync2(stateFile(session), { force: true });
3319
3727
  mlsAsks?.endSession(session);
3320
3728
  await driver.reset?.(session);
3321
3729
  }, "resetSession");
@@ -3385,6 +3793,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3385
3793
  if (input.mls) target.mls = true;
3386
3794
  target.queue.push(input);
3387
3795
  target.turn?.live.refresh();
3796
+ requeue(target);
3388
3797
  pump();
3389
3798
  }
3390
3799
  __name(offer, "offer");
@@ -3492,7 +3901,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3492
3901
  const inputs = candidate.queue.splice(0, Math.min(MAX_INPUTS_PER_TURN, other < 0 ? MAX_INPUTS_PER_TURN : other));
3493
3902
  const turn = newTurn(inputs, turnId(candidate.id, inputs), candidate.id);
3494
3903
  candidate.turn = turn;
3495
- (turn.live = livePublisher(candidate, turn, encrypted ? "presence" : "status")).open();
3904
+ (turn.live = driver.capabilities.remote ? SILENT : livePublisher(candidate, turn, encrypted ? "presence" : "status")).open();
3496
3905
  const done = execute(candidate, turn).catch((error) => {
3497
3906
  warn("turn failed")(error);
3498
3907
  return dispose(queued(turn), "release", "turn-failed");
@@ -3522,14 +3931,28 @@ ${error instanceof Error ? error.message : String(error)}`;
3522
3931
  if (turn.inputs[0].mls && !await beforeTurn(current, turn)) return;
3523
3932
  turn.trusted = await confirmTrusted(turn, conversation.memberIds);
3524
3933
  turn.notes = pendingNotes(current.id);
3525
- turn.contexts = await Promise.all(openErrands(current.id).map(async (record) => ({ record, encryptedOrigin: !!record.origin && await endpoint.getConversationContentMode(record.origin).then((mode) => mode === "mls-dev-text", () => false) })));
3526
- turn.live.setMode(turn.mls ? "presence" : driver.capabilities.streaming && resolveTurnVerbosity({ conversationType: kind }) === "verbose" ? "full" : "status");
3934
+ turn.contexts = await Promise.all(openErrands(current.id).map(async (record) => ({ record, encryptedOrigin: !!record.origin && await endpoint.getConversationContentMode(record.origin).then((mode2) => mode2 === "mls-dev-text", () => false) })));
3935
+ const mode = turn.mls ? "presence" : driver.capabilities.streaming && resolveTurnVerbosity({ conversationType: kind }) === "verbose" ? "full" : "status";
3936
+ turn.live.setMode(mode);
3527
3937
  const history = driver.capabilities.sessionMemory === "none" ? await recentHistory(current, turn) : [];
3528
3938
  await materialize(current, turn);
3529
3939
  const bound = bindTools(current, turn);
3530
3940
  try {
3531
3941
  let work;
3532
- for (let live = queued(turn), waited = 0; live.length && !turn.controller.signal.aborted; ) {
3942
+ if (driver.capabilities.remote) {
3943
+ const live = queued(turn);
3944
+ if (live.length && !turn.controller.signal.aborted) {
3945
+ turn.inputs = live;
3946
+ turn.id = turnId(current.id, live);
3947
+ turn.pickUp = () => pickUp(current, turn, mode);
3948
+ try {
3949
+ work = Promise.resolve(driver.runTurn(buildTurn(current.id, turn, kind, history, bound.tools)));
3950
+ } catch (error) {
3951
+ work = Promise.reject(error);
3952
+ }
3953
+ requeue(current);
3954
+ }
3955
+ } else for (let live = queued(turn), waited = 0; live.length && !turn.controller.signal.aborted; ) {
3533
3956
  turn.inputs = live;
3534
3957
  turn.id = turnId(current.id, live);
3535
3958
  let driverTurn = buildTurn(current.id, turn, kind, history, bound.tools);
@@ -3552,15 +3975,17 @@ ${error instanceof Error ? error.message : String(error)}`;
3552
3975
  live = next;
3553
3976
  }
3554
3977
  if (!work) return;
3555
- turn.submitted = true;
3556
- progress(turn);
3557
- if (!turn.mls) void client.markAsRead(current.id).catch(warn("mark read"));
3558
- for (const { payload } of turn.mls ? [] : turn.inputs) {
3559
- if (payload.message.metadata?.inboundDisposition === "queued") {
3560
- void client.updateMessageDisposition(current.id, payload.message.id, "accepted_now").catch(warn("accepted_now"));
3561
- }
3562
- }
3978
+ if (!driver.capabilities.remote) started(current, turn);
3563
3979
  const outcome = await outcomeOf(turn, work);
3980
+ if (!turn.submitted) await turn.claiming?.catch(() => void 0);
3981
+ if (!turn.submitted) {
3982
+ turn.pickUp = void 0;
3983
+ if (turn.reason === "requeue") return void current.queue.unshift(...queued(turn));
3984
+ if (turn.reason) return;
3985
+ const claim = await endpoint.startInbound(queued(turn).map((input) => input.handle), () => void 0);
3986
+ if (claim.status !== "submitted") return await dispose(queued(turn), "release", "claim-deferred");
3987
+ started(current, turn);
3988
+ }
3564
3989
  turn.open = false;
3565
3990
  turn.ended.abort();
3566
3991
  bound.release();
@@ -3569,10 +3994,50 @@ ${error instanceof Error ? error.message : String(error)}`;
3569
3994
  } finally {
3570
3995
  turn.ended.abort();
3571
3996
  bound.release();
3572
- rmSync3(conversationMediaCacheDir(inboundMedia(current.id)), { recursive: true, force: true });
3997
+ rmSync2(conversationMediaCacheDir(inboundMedia(current.id)), { recursive: true, force: true });
3573
3998
  }
3574
3999
  }
3575
4000
  __name(execute, "execute");
4001
+ function started(current, turn) {
4002
+ turn.submitted = true;
4003
+ progress(turn);
4004
+ if (!turn.mls) void client.markAsRead(current.id).catch(warn("mark read"));
4005
+ for (const { payload } of turn.mls ? [] : turn.inputs) {
4006
+ if (payload.message.metadata?.inboundDisposition === "queued") {
4007
+ void client.updateMessageDisposition(current.id, payload.message.id, "accepted_now").catch(warn("accepted_now"));
4008
+ }
4009
+ }
4010
+ }
4011
+ __name(started, "started");
4012
+ async function pickUp(current, turn, mode) {
4013
+ const live = queued(turn);
4014
+ if (live.length !== turn.inputs.length) {
4015
+ abort(turn, "requeue");
4016
+ throw turnEnded2();
4017
+ }
4018
+ const claiming = endpoint.startInbound(live.map((input) => input.handle), () => void 0).then((result) => {
4019
+ if (result.status === "submitted") {
4020
+ turn.pickUp = void 0;
4021
+ started(current, turn);
4022
+ if (!turn.controller.signal.aborted) (turn.live = livePublisher(current, turn, mode)).open();
4023
+ }
4024
+ return result;
4025
+ });
4026
+ turn.claiming = claiming;
4027
+ const claim = await claiming;
4028
+ if (claim.status !== "submitted") {
4029
+ if (claim.status !== "deferred") abort(turn, "requeue");
4030
+ throw new Error("The input could not be claimed now");
4031
+ }
4032
+ }
4033
+ __name(pickUp, "pickUp");
4034
+ function requeue(current) {
4035
+ const turn = current.turn, next = current.queue[0];
4036
+ if (!turn?.pickUp || turn.pickingUp || turn.controller.signal.aborted || !next) return;
4037
+ if (!!next.mls !== !!turn.inputs[0].mls || turn.inputs.length >= MAX_INPUTS_PER_TURN) return;
4038
+ abort(turn, "requeue");
4039
+ }
4040
+ __name(requeue, "requeue");
3576
4041
  async function confirmTrusted(turn, memberIds) {
3577
4042
  if (!trustedAgents.size || driver.capabilities.remote || options.relay) return /* @__PURE__ */ new Set();
3578
4043
  const candidates = turn.inputs.map(({ payload }) => payload.message).filter((message2) => message2.senderType === "ai_agent" && message2.senderId !== agentId && trustedAgents.has(message2.senderId) && (turn.mls ? true : message2.metadata?.[ACTED_FOR_OWNER] === true && message2.metadata.turnSemantics === "turn_complete" && message2.forwarded !== true));
@@ -3644,7 +4109,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3644
4109
  return check;
3645
4110
  }
3646
4111
  __name(reaudience, "reaudience");
3647
- const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join6(home, "runner", options.environmentId, agentId) }), "inboundMedia");
4112
+ const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join7(home, "runner", options.environmentId, agentId) }), "inboundMedia");
3648
4113
  async function materialize(current, turn) {
3649
4114
  if (driver.capabilities.remote) return;
3650
4115
  for (const { payload: { message: message2 } } of turn.inputs) {
@@ -3687,7 +4152,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3687
4152
  const file = await admitFile(path);
3688
4153
  if (typeof file === "string") return { result: toolText(file, true) };
3689
4154
  turn.outbox.push(file.path);
3690
- return { result: toolText(`${basename3(file.path)} will be sent with your reply.`) };
4155
+ return { result: toolText(`${basename4(file.path)} will be sent with your reply.`) };
3691
4156
  }, "send_file"),
3692
4157
  communicate: /* @__PURE__ */ __name(async (args) => ({ result: await communicate(current, turn, args) }), "communicate"),
3693
4158
  // RUN-25 through a tool: the same `ask`, so the same responder, card, expiry, waiting_input and paused deadline;
@@ -3805,7 +4270,7 @@ ${JSON.stringify(result, null, 2)}`);
3805
4270
  let sending = turn.said.get(messageId);
3806
4271
  if (!sending) {
3807
4272
  turn.said.set(messageId, sending = (async () => {
3808
- const mls = audience ?? (await currentAudience(roomId)).audience;
4273
+ const mls2 = audience ?? (await currentAudience(roomId)).audience;
3809
4274
  const record = {
3810
4275
  version: 1,
3811
4276
  turnId: messageId,
@@ -3816,7 +4281,7 @@ ${JSON.stringify(result, null, 2)}`);
3816
4281
  text: text3,
3817
4282
  options: { messageId },
3818
4283
  notice: false,
3819
- mls
4284
+ mls: mls2
3820
4285
  };
3821
4286
  journal.save(record);
3822
4287
  const sent2 = await publish(record);
@@ -3834,7 +4299,7 @@ ${JSON.stringify(result, null, 2)}`);
3834
4299
  async function admitFile(path) {
3835
4300
  if (typeof path !== "string" || !isAbsolute3(path)) return "send_file needs an absolute file path";
3836
4301
  try {
3837
- const real = await realpath5(path);
4302
+ const real = await realpath4(path);
3838
4303
  if (!roots.some((root) => real.startsWith(root.endsWith(sep) ? root : `${root}${sep}`))) return `${path} is outside the directories this agent may share from`;
3839
4304
  const info = await stat2(real);
3840
4305
  if (!info.isFile() || info.size < 1 || info.size > MAX_CANON_MEDIA_BYTES) return `${path} must be a file from 1 byte to 100 MiB`;
@@ -3852,13 +4317,13 @@ ${JSON.stringify(result, null, 2)}`);
3852
4317
  for (const candidate of candidates) {
3853
4318
  const file = await admitFile(candidate);
3854
4319
  if (typeof file === "string") {
3855
- refused2.push(typeof candidate === "string" ? basename3(candidate) : "a file");
4320
+ refused2.push(typeof candidate === "string" ? basename4(candidate) : "a file");
3856
4321
  continue;
3857
4322
  }
3858
4323
  if (seen.has(file.path)) continue;
3859
4324
  seen.add(file.path);
3860
4325
  if (attachments.length >= MAX_GROUP_PARTS - 1) {
3861
- refused2.push(basename3(file.path));
4326
+ refused2.push(basename4(file.path));
3862
4327
  continue;
3863
4328
  }
3864
4329
  try {
@@ -3866,14 +4331,14 @@ ${JSON.stringify(result, null, 2)}`);
3866
4331
  const attachmentId = `${turn.id}-${seen.size}`;
3867
4332
  const [prepared] = await media.prepare({ conversationId, attachmentId, files: [{
3868
4333
  source: file.path,
3869
- descriptor: { mimeType: inferUploadMimeType(file.path), sizeBytes: file.size, fileName: basename3(file.path) }
4334
+ descriptor: { mimeType: inferUploadMimeType(file.path), sizeBytes: file.size, fileName: basename4(file.path) }
3870
4335
  }] }, { signal: turn.retire.signal });
3871
4336
  attachments.push(prepared);
3872
4337
  flows.push(`${conversationId}:upload:${attachmentId}`);
3873
4338
  } catch (error) {
3874
4339
  if (turn.retire.signal.aborted) break;
3875
4340
  warn("file upload")(error);
3876
- refused2.push(basename3(file.path));
4341
+ refused2.push(basename4(file.path));
3877
4342
  }
3878
4343
  }
3879
4344
  return { attachments, flows, refused: refused2 };
@@ -3905,7 +4370,9 @@ ${JSON.stringify(result, null, 2)}`);
3905
4370
  const skip = new Set([...turn.inputs, ...current.queue].map(({ payload }) => payload.message.id));
3906
4371
  const at = Math.max(...turn.inputs.map(({ payload }) => recent.findIndex((message2) => message2.id === payload.message.id)));
3907
4372
  const before = /* @__PURE__ */ __name((message2, index) => message2.senderId === agentId || (at >= 0 ? index > at : !first.createdAt || String(message2.createdAt ?? "") < first.createdAt), "before");
3908
- const window = recent.filter((message2, index) => before(message2, index) && !message2.deleted && !skip.has(message2.id)).slice(0, HISTORY_WINDOW);
4373
+ const ownerId = driver.capabilities.ownerHistoryOnly ? (await client.getAgentMe()).ownerId : void 0;
4374
+ const allowed = /* @__PURE__ */ __name((message2) => ownerId === void 0 || message2.senderId === agentId || message2.senderId === ownerId, "allowed");
4375
+ const window = recent.filter((message2, index) => before(message2, index) && !message2.deleted && !skip.has(message2.id) && allowed(message2)).slice(0, HISTORY_WINDOW);
3909
4376
  const sameAudience = turn.mls ? await Promise.all(window.map((message2) => endpoint.mlsDevGroups.readVerifiedMessageBinding({ conversationId: current.id, messageId: message2.id }).then((binding) => !!binding && JSON.stringify(binding.head) === JSON.stringify(turn.mls.head), () => false))) : void 0;
3910
4377
  return window.filter((_, index) => !sameAudience || sameAudience[index]).reverse();
3911
4378
  } catch (error) {
@@ -3949,7 +4416,7 @@ ${JSON.stringify(result, null, 2)}`);
3949
4416
  } else if (answered2 && turn.mls && resultFiles.length) {
3950
4417
  text3 = `${text3}
3951
4418
 
3952
- (${ENCRYPTED_FILES}: ${resultFiles.map((file) => typeof file === "string" ? basename3(file) : "a file").join(", ")}.)`;
4419
+ (${ENCRYPTED_FILES}: ${resultFiles.map((file) => typeof file === "string" ? basename4(file) : "a file").join(", ")}.)`;
3953
4420
  } else if (answered2 && !turn.mls) {
3954
4421
  const files = await attach(current.id, turn, [...turn.outbox, ...resultFiles]);
3955
4422
  ({ attachments, flows } = files);
@@ -4052,21 +4519,21 @@ ${JSON.stringify(result, null, 2)}`);
4052
4519
  return await send(record.conversationId, STOPS_SHORT_NOTICE, notice, retire) === "pending" ? "pending" : "refused";
4053
4520
  }
4054
4521
  __name(publish, "publish");
4055
- async function send(conversationId, text3, options2, retire, record, mls = record?.mls) {
4522
+ async function send(conversationId, text3, options2, retire, record, mls2 = record?.mls) {
4056
4523
  const sent = await (async () => {
4057
4524
  for (let attempt = 0; ; attempt += 1) {
4058
4525
  const files = record?.attachments?.length ?? 0;
4059
- const result = await untilHalt((mls ? sendEncryptedItems(mls, record?.items ?? [], retire).then(() => sendEncrypted(mls, options2.messageId, text3, retire)) : files ? sendWithFiles(record, fitGroup(text3, files), options2, retire) : sendMessageWithRetryChunked(client, conversationId, fitGroup(text3, 0), options2, { signal: retire })).then(() => "delivered", (error) => error));
4526
+ const result = await untilHalt((mls2 ? sendEncryptedItems(mls2, record?.items ?? [], retire).then(() => sendEncrypted(mls2, options2.messageId, text3, retire)) : files ? sendWithFiles(record, fitGroup(text3, files), options2, retire) : sendMessageWithRetryChunked(client, conversationId, fitGroup(text3, 0), options2, { signal: retire })).then(() => "delivered", (error) => error));
4060
4527
  if (result === "delivered" || result === "pending") return result;
4061
4528
  if (retire?.aborted || isPendingCanonOperation(result)) return "pending";
4062
- if (mls ? result.retryable === false : !isRetryableCanonDeliveryError(result)) {
4529
+ if (mls2 ? result.retryable === false : !isRetryableCanonDeliveryError(result)) {
4063
4530
  warn("Canon refused a reply")(result);
4064
4531
  return "refused";
4065
4532
  }
4066
4533
  if (await untilHalt(pause2(Math.min(3e4, 1e3 * 2 ** attempt))) === "pending") return "pending";
4067
4534
  }
4068
4535
  })();
4069
- if (mls) forgetGuard(mls, options2.messageId, sent);
4536
+ if (mls2) forgetGuard(mls2, options2.messageId, sent);
4070
4537
  return sent;
4071
4538
  }
4072
4539
  __name(send, "send");
@@ -4115,20 +4582,20 @@ ${JSON.stringify(result, null, 2)}`);
4115
4582
  for (const candidate of candidates) {
4116
4583
  const file = await admitFile(candidate);
4117
4584
  if (typeof file === "string") {
4118
- refused2.push(typeof candidate === "string" ? basename3(candidate) : "a file");
4585
+ refused2.push(typeof candidate === "string" ? basename4(candidate) : "a file");
4119
4586
  continue;
4120
4587
  }
4121
4588
  if (seen.has(file.path)) continue;
4122
4589
  seen.add(file.path);
4123
4590
  const mimeType = inferUploadMimeType(file.path);
4124
- if (items.length >= MAX_GROUP_PARTS - 1 || !resolveAllowedUploadMimeType({ mimeType, fileName: basename3(file.path) })) {
4125
- refused2.push(basename3(file.path));
4591
+ if (items.length >= MAX_GROUP_PARTS - 1 || !resolveAllowedUploadMimeType({ mimeType, fileName: basename4(file.path) })) {
4592
+ refused2.push(basename4(file.path));
4126
4593
  continue;
4127
4594
  }
4128
4595
  items.push({
4129
4596
  path: file.path,
4130
4597
  messageId: keyed().text("item", roomId, turn.id, String(items.length)),
4131
- fileName: basename3(file.path),
4598
+ fileName: basename4(file.path),
4132
4599
  mimeType,
4133
4600
  sizeBytes: file.size
4134
4601
  });
@@ -4200,21 +4667,28 @@ ${JSON.stringify(result, null, 2)}`);
4200
4667
  __name(buildTurn, "buildTurn");
4201
4668
  async function confirmPickup(roomId, turn, open4) {
4202
4669
  if (!open4()) throw turnEnded2();
4203
- const current = await client.getConversation(roomId);
4204
- if (!open4()) throw turnEnded2();
4205
- if (!current?.memberIds?.includes(agentId)) {
4206
- loseMembership(room(roomId));
4207
- throw new Error("The agent is no longer a member of this conversation");
4208
- }
4209
- if (turn.mls && await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4210
- if (!turn.controller.signal.aborted) {
4211
- void dispose(queued(turn), "settle", "mls-obsolete-audience");
4212
- abort(turn, "audience");
4670
+ turn.pickingUp = true;
4671
+ try {
4672
+ const current = await client.getConversation(roomId);
4673
+ if (!open4()) throw turnEnded2();
4674
+ if (!current?.memberIds?.includes(agentId)) {
4675
+ loseMembership(room(roomId));
4676
+ throw new Error("The agent is no longer a member of this conversation");
4677
+ }
4678
+ if (turn.mls && await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4679
+ if (!turn.controller.signal.aborted) {
4680
+ void dispose(queued(turn), "settle", "mls-obsolete-audience");
4681
+ abort(turn, "audience");
4682
+ }
4683
+ throw new Error("Who can read this encrypted conversation changed");
4213
4684
  }
4214
- throw new Error("Who can read this encrypted conversation changed");
4685
+ if (!open4()) throw turnEnded2();
4686
+ if (turn.pickUp) await turn.pickUp();
4687
+ progress(turn);
4688
+ } finally {
4689
+ turn.pickingUp = false;
4690
+ requeue(room(roomId));
4215
4691
  }
4216
- if (!open4()) throw turnEnded2();
4217
- progress(turn);
4218
4692
  }
4219
4693
  __name(confirmPickup, "confirmPickup");
4220
4694
  function ask(roomId, turn, sender, request) {
@@ -4306,7 +4780,7 @@ ${JSON.stringify(result, null, 2)}`);
4306
4780
  const file = stateFile(session);
4307
4781
  let data = {};
4308
4782
  try {
4309
- data = JSON.parse(readFileSync5(file, "utf8"));
4783
+ data = JSON.parse(readFileSync6(file, "utf8"));
4310
4784
  } catch {
4311
4785
  }
4312
4786
  const save = /* @__PURE__ */ __name((next) => {
@@ -4396,11 +4870,11 @@ ${JSON.stringify(result, null, 2)}`);
4396
4870
  const mode = await endpoint.getConversationContentMode(roomId);
4397
4871
  if (mode !== "plaintext") target.mls = true;
4398
4872
  if (mode !== "plaintext" && (!mlsOn || mode !== "mls-dev-text")) refuse(roomId, mlsOn ? "protocol" : "no-device");
4399
- const mls = mode === "mls-dev-text" && mlsOn ? (await currentAudience(roomId)).audience : void 0;
4873
+ const mls2 = mode === "mls-dev-text" && mlsOn ? (await currentAudience(roomId)).audience : void 0;
4400
4874
  const legacy = `${id}:restart`;
4401
- const messageId = mls && !await endpoint.getOperation(encryptedOperation(mls, legacy)) ? keyed().text("restart", roomId, id) : legacy;
4402
- if (mode !== "plaintext" && !mls) await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4403
- else if (await send(roomId, RESTART_NOTICE, outputOptions(id, trigger, "restart", false, messageId), void 0, void 0, mls) !== "pending") {
4875
+ const messageId = mls2 && !await endpoint.getOperation(encryptedOperation(mls2, legacy)) ? keyed().text("restart", roomId, id) : legacy;
4876
+ if (mode !== "plaintext" && !mls2) await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4877
+ else if (await send(roomId, RESTART_NOTICE, outputOptions(id, trigger, "restart", false, messageId), void 0, void 0, mls2) !== "pending") {
4404
4878
  await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4405
4879
  }
4406
4880
  }
@@ -4759,7 +5233,7 @@ ${JSON.stringify(result, null, 2)}`);
4759
5233
  let text3 = answered2 ? result.reply : failed ? FAILURE_NOTICE : null;
4760
5234
  if (answered2 && result.files?.length) text3 = `${text3}
4761
5235
 
4762
- (Files cannot go with an errand's report: ${result.files.map((file) => basename3(String(file))).join(", ")}.)`;
5236
+ (Files cannot go with an errand's report: ${result.files.map((file) => basename4(String(file))).join(", ")}.)`;
4763
5237
  const known = latest(record.id) ?? record;
4764
5238
  const cancelled = known.status === "cancelled" || stopped();
4765
5239
  const status = cancelled ? "cancelled" : ended(known, failed);
@@ -4928,12 +5402,12 @@ ${JSON.stringify(result, null, 2)}`);
4928
5402
  client = new CanonClient(options.apiKey, connection.apiBaseUrl, {
4929
5403
  environmentId: connection.environmentId,
4930
5404
  streamUrl: connection.streamUrl,
4931
- stateDirectory: join6(home, "endpoint"),
5405
+ stateDirectory: join7(home, "endpoint"),
4932
5406
  ...options.client ? { client: options.client } : {},
4933
5407
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
4934
5408
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
4935
5409
  ...options.installationId ? { installationId: options.installationId } : {},
4936
- ...options.mls ? { mlsDev: { configuration: options.mls.configuration, custody: options.mls.custody } } : {},
5410
+ ...mls ? { mlsDev: { configuration: mls.configuration, custody: mls.custody } } : {},
4937
5411
  ...options.endpoint ? { endpoint: options.endpoint } : {},
4938
5412
  ...options.fetch ? { fetch: options.fetch } : {}
4939
5413
  });
@@ -4949,14 +5423,17 @@ ${JSON.stringify(result, null, 2)}`);
4949
5423
  if (options.wake !== void 0) await client.configureWake(options.wake);
4950
5424
  mlsOn = !!(endpoint.mlsDevDevice && endpoint.mlsDevGroups);
4951
5425
  if (!mlsOn && (await endpoint.capabilities().catch(() => null))?.mls?.roomMode === "default") {
4952
- throw new Error("Canon encrypts every new conversation here, and this runner has no MLS device: give it mls (managedMlsDevice, or an operator's configuration: --mls-dev-config <path>).");
5426
+ throw new Error(`Canon encrypts every new conversation here, and this runner has no MLS device${options.mls === void 0 && !options.endpoint ? ` (@canonmsg/agent-sdk ships no custody helper for ${process.platform}-${process.arch})` : ""}: give it an operator's configuration (--mls-dev-config <path>, or mls: await loadMlsDevConfiguration(path, { environmentId })).`);
4953
5427
  }
4954
- if (mlsOn && options.mls?.provisioned && (await endpoint.mlsDevDevice.read()).status !== "credential_committed") {
4955
- log("Setting up this agent's MLS device");
4956
- const device = await runMlsDevSetup(endpoint, "setup", void 0, options.mls.provision, options.mls.provisioned).catch((error) => {
4957
- throw new Error(`Could not set up this agent's MLS device: ${error instanceof Error ? error.message : String(error)}`);
4958
- });
4959
- log(`MLS device ${device.deviceId} is ${device.status}`);
5428
+ if (mlsOn && mls?.provisioned) {
5429
+ await assertMlsVaultInHome(endpoint, mls.provisioned);
5430
+ if ((await endpoint.mlsDevDevice.read()).status !== "credential_committed") {
5431
+ log("Setting up this agent's MLS device");
5432
+ const device = await runMlsDevSetup(endpoint, "setup", void 0, mls.provision, mls.provisioned).catch((error) => {
5433
+ throw new Error(`Could not set up this agent's MLS device: ${error instanceof Error ? error.message : String(error)}`);
5434
+ });
5435
+ log(`MLS device ${device.deviceId} is ${device.status}`);
5436
+ }
4960
5437
  }
4961
5438
  if (mlsOn) await endpoint.mlsDevDevice.ensureReady().catch(warn("MLS device not ready; encrypted rooms wait for it"));
4962
5439
  const keepPool = /* @__PURE__ */ __name(() => endpoint.mlsDevDevice.read().then(async (view) => {
@@ -4973,17 +5450,17 @@ ${JSON.stringify(result, null, 2)}`);
4973
5450
  }
4974
5451
  scope = JSON.stringify([identity.environmentId, agentId, identity.ownershipVersion ?? 0, identity.installationId]);
4975
5452
  if (mlsOn) {
4976
- mlsIds = createMlsIds(privateKey(join6(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2563(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
5453
+ mlsIds = createMlsIds(privateKey(join7(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2563(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
4977
5454
  mlsAsks = createMlsAsks({
4978
5455
  endpoint,
4979
5456
  agentId,
4980
- directory: join6(home, "runner", options.environmentId, agentId, "asks", sha2563(scope).slice(0, 32)),
5457
+ directory: join7(home, "runner", options.environmentId, agentId, "asks", sha2563(scope).slice(0, 32)),
4981
5458
  assertAudience,
4982
5459
  sessionForDestructive: options.sessionApprovalForDestructive === true,
4983
5460
  log
4984
5461
  }, approvalScope);
4985
5462
  }
4986
- journal = createCompletedOutputJournal(join6(home, "runner", options.environmentId, agentId, "finals"), scope, {
5463
+ journal = createCompletedOutputJournal(join7(home, "runner", options.environmentId, agentId, "finals"), scope, {
4987
5464
  label: "agent runner",
4988
5465
  validate: /* @__PURE__ */ __name((record) => typeof record.text === "string" && typeof record.options?.messageId === "string" && Array.isArray(record.inputs) && typeof record.notice === "boolean" && (record.attachments === void 0 || Array.isArray(record.attachments)) && (record.flows === void 0 || Array.isArray(record.flows) && record.flows.length === record.attachments?.length) && (record.mls === void 0 || !!record.mls && typeof record.mls === "object") && (record.errand === void 0 || typeof record.errand === "string"), "validate")
4989
5466
  });
@@ -5008,13 +5485,11 @@ ${JSON.stringify(result, null, 2)}`);
5008
5485
  }
5009
5486
  }
5010
5487
  const outbound = driver.capabilities.outboundFiles;
5011
- roots = outbound ? (await Promise.all(outbound.roots.map((root) => realpath5(root).catch(() => "")))).filter(Boolean) : [];
5488
+ roots = outbound ? (await Promise.all(outbound.roots.map((root) => realpath4(root).catch(() => "")))).filter(Boolean) : [];
5012
5489
  if (driver.capabilities.tools) tools = await startToolServer();
5013
5490
  await recover();
5014
- if (!options.relay) {
5015
- const publisher = createRuntimeStatePublisher({ agentId, clientType: CLIENT_TYPES[driver.name], hostMode: true, rtdb: initRTDBAuth(client) });
5016
- heartbeat = createRuntimeHeartbeat({ publisher, getRuntime: /* @__PURE__ */ __name(() => ({ runtimeDescriptor: descriptor }), "getRuntime"), onError: warn("runtime heartbeat") });
5017
- }
5491
+ const publisher = createRuntimeStatePublisher({ agentId, clientType: CLIENT_TYPES[driver.name], hostMode: true, rtdb: initRTDBAuth(client) });
5492
+ heartbeat = createRuntimeHeartbeat({ publisher, getRuntime: /* @__PURE__ */ __name(() => ({ runtimeDescriptor: descriptor }), "getRuntime"), onError: warn("runtime heartbeat") });
5018
5493
  inbound = endpoint.acceptInbound({ kind: "message.created", offer, onError: warn("inbox"), ...mlsOn ? { intervalMs: 1e3 } : {} });
5019
5494
  inbound.start();
5020
5495
  let reconnect = false;
@@ -5079,7 +5554,7 @@ ${JSON.stringify(result, null, 2)}`);
5079
5554
  for (const unregister of guards.values()) unregister();
5080
5555
  guards.clear();
5081
5556
  await client?.close();
5082
- await options.mls?.close?.().catch(warn("MLS custody close"));
5557
+ await mls?.close?.().catch(warn("MLS custody close"));
5083
5558
  })().finally(() => lock?.release());
5084
5559
  return stopping;
5085
5560
  },
@@ -5118,6 +5593,54 @@ ${JSON.stringify(result, null, 2)}`);
5118
5593
  const sent = await within(entry.sent, Math.max(0, deadline - Date.now()));
5119
5594
  if (sent === "refused") throw new ConnectorRelayError(409, "CONNECTOR_RELAY_SEND_REFUSED", "Canon did not accept the message; the conversation's members may have changed");
5120
5595
  return { conversationId, messageId, historyMessageId: id, status: sent === "delivered" ? "sent" : "pending" };
5596
+ },
5597
+ async relayStart(targetUserId, messageId, text3, selection, startOptions = {}) {
5598
+ if (typeof targetUserId !== "string" || !targetUserId || targetUserId.length > 200 || targetUserId.includes("/") || typeof messageId !== "string" || !ENDPOINT_ID_PATTERN.test(messageId) || messageId.length > 120 || typeof text3 !== "string" || !text3.trim() || Buffer.byteLength(text3) > MLS_DEV_TEXT_MAX_BYTES) {
5599
+ throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A start needs a person or agent, a messageId and 1 byte to 32 KiB of text");
5600
+ }
5601
+ if (!options.relay || !client) throw new ConnectorRelayError(403, "CONNECTOR_RELAY_UNAVAILABLE", "This runner is not a relay");
5602
+ if (!mlsOn) throw new ConnectorRelayError(503, "CONNECTOR_RELAY_NO_DEVICE", "The relay has no MLS device");
5603
+ const deadline = startOptions.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
5604
+ const identity = { messageId, historyMessageId: `relay-start-${sha2563(messageId).slice(0, 32)}` };
5605
+ const answer = await within(
5606
+ endpoint.startWithheldOpener({
5607
+ targetUserId,
5608
+ text: text3,
5609
+ messageId: identity.historyMessageId,
5610
+ ...selection ? { selection } : {}
5611
+ }).then((result2) => ({ result: result2 }), (error) => ({ error })),
5612
+ Math.max(0, deadline - Date.now())
5613
+ );
5614
+ if (!answer || "error" in answer && answer.error?.retryable === true) {
5615
+ return { ...identity, status: "starting" };
5616
+ }
5617
+ if ("error" in answer) {
5618
+ const { status, code, message: message2 } = answer.error;
5619
+ if (code === "OPERATION_ID_COLLISION") {
5620
+ throw new ConnectorRelayError(409, "CONNECTOR_RELAY_MESSAGE_CONFLICT", "This messageId already started a conversation with other text or another person");
5621
+ }
5622
+ if ((status === 400 || status === 403 || status === 404 || status === 409) && typeof code === "string") {
5623
+ throw new ConnectorRelayError(status, code, typeof message2 === "string" ? message2 : "Canon refused the start");
5624
+ }
5625
+ throw answer.error;
5626
+ }
5627
+ const result = answer.result;
5628
+ if ("reason" in result) {
5629
+ if (result.reason === "opening-text-required") {
5630
+ throw new ConnectorRelayError(403, "CONNECTOR_RELAY_NOT_ENCRYPTED", "The relay serves only encrypted conversations its agent is in");
5631
+ }
5632
+ return { ...identity, status: result.status, reason: result.reason };
5633
+ }
5634
+ if ("requestId" in result) {
5635
+ return { ...identity, status: result.status, requestId: result.requestId, ...result.conversationId ? { conversationId: result.conversationId } : {} };
5636
+ }
5637
+ return {
5638
+ ...identity,
5639
+ status: "messaged",
5640
+ conversationId: result.conversationId,
5641
+ ...result.created !== void 0 ? { created: result.created } : {},
5642
+ ...result.reused !== void 0 ? { reused: result.reused } : {}
5643
+ };
5121
5644
  }
5122
5645
  };
5123
5646
  }
@@ -5139,14 +5662,14 @@ export {
5139
5662
  DriverSessionError,
5140
5663
  ConnectorRelayError,
5141
5664
  loadInstructions,
5665
+ checkAcp,
5666
+ acpDriver,
5142
5667
  loadClaudeQuery,
5143
5668
  claudeDriver,
5144
5669
  codeDriver,
5145
5670
  checkCodex,
5146
5671
  codexDriver,
5147
5672
  connectorDriver,
5148
- runnerLockName,
5149
- assertPrivateHome,
5150
5673
  errandsDirectory,
5151
5674
  queueErrand,
5152
5675
  errandJournal,
@@ -5157,6 +5680,8 @@ export {
5157
5680
  runMlsDevSetup,
5158
5681
  MLS_ENVIRONMENT_PINS,
5159
5682
  bundledMlsCustodyHelper,
5683
+ bundledMlsWasmCore,
5684
+ defaultMlsDevice,
5160
5685
  managedMlsDevice,
5161
5686
  startRelayChannel,
5162
5687
  loadRelayConfiguration,