@canonmsg/agent-sdk 12.5.1 → 13.0.2

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 (46) hide show
  1. package/README.md +57 -24
  2. package/dist/chunks/{chunk-C2WJT3VX.js → chunk-5WPAB5MB.js} +1047 -856
  3. package/dist/chunks/{chunk-3KFVBOVI.js → chunk-DNHJGAEL.js} +1115 -1020
  4. package/dist/chunks/chunk-DTAWEQCM.js +406 -0
  5. package/dist/chunks/{chunk-TFSURFF6.js → chunk-TYZT346O.js} +1720 -4006
  6. package/dist/chunks/chunk-YHDDOADK.js +118 -0
  7. package/dist/chunks/chunk-YJUPKU47.js +1490 -0
  8. package/dist/chunks/{mls-crypto-Y44BOSEM.js → mls-crypto-NZVMSUFU.js} +2 -1
  9. package/dist/cli.js +86 -48
  10. package/dist/drivers/acp.d.ts +56 -0
  11. package/dist/drivers/codex.d.ts +1 -2
  12. package/dist/drivers/subprocess.d.ts +38 -0
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +12 -3
  15. package/dist/internal/backend-contracts/endpointContentPolicy.d.ts +2 -2
  16. package/dist/internal/backend-contracts/index.d.ts +1 -0
  17. package/dist/internal/backend-contracts/loggedOutSessions.d.ts +7 -0
  18. package/dist/internal/backend-contracts/mlsDevCredential.d.ts +3 -0
  19. package/dist/internal/backend-contracts/mlsDevGroup.d.ts +17 -0
  20. package/dist/internal/core/completed-output-journal.d.ts +0 -5
  21. package/dist/internal/core/index.d.ts +3 -9
  22. package/dist/internal/core/retry.d.ts +2 -89
  23. package/dist/internal/core/silent-turn.d.ts +4 -33
  24. package/dist/internal/endpoint/engine.d.ts +6 -0
  25. package/dist/internal/endpoint/mls-dev-client.d.ts +2 -0
  26. package/dist/internal/endpoint/mls-dev-groups.d.ts +12 -0
  27. package/dist/internal/endpoint/mls-dev-text.d.ts +32 -0
  28. package/dist/internal/endpoint/node-mls-custody-protocol.d.ts +5 -0
  29. package/dist/internal/endpoint/node.d.ts +1 -0
  30. package/dist/internal/endpoint/transport.d.ts +1 -1
  31. package/dist/media.js +4 -2
  32. package/dist/mls/darwin-arm64/canon-mls-custody +0 -0
  33. package/dist/mls/linux-arm64/canon-mls-custody +0 -0
  34. package/dist/mls/linux-x64/canon-mls-custody +0 -0
  35. package/dist/mls/wasm/canon_mls_wasm.js +363 -0
  36. package/dist/mls/wasm/canon_mls_wasm_bg.wasm +0 -0
  37. package/dist/mls-wasm-host.js +258 -0
  38. package/dist/runner/mls-bundled.d.ts +15 -2
  39. package/dist/runner/mls-managed.d.ts +6 -3
  40. package/dist/runner/mls.d.ts +4 -7
  41. package/dist/runner/runner.d.ts +9 -12
  42. package/dist/runner/types.d.ts +37 -1
  43. package/package.json +1 -1
  44. package/dist/internal/core/approval-manager.d.ts +0 -43
  45. package/dist/internal/core/runtime-request-manager.d.ts +0 -283
  46. package/dist/internal/core/turn-verbosity.d.ts +0 -79
@@ -1,36 +1,45 @@
1
1
  import {
2
- ApprovalManager,
2
+ assertPrivateHome,
3
+ assertPrivateWindowsFile,
4
+ privateKey,
5
+ restrictWindowsFile,
6
+ runnerLockName,
7
+ secureWindowsDirectory,
8
+ wasmMlsCustody,
9
+ windowsAclProblems,
10
+ writeDurable
11
+ } from "./chunk-DTAWEQCM.js";
12
+ import {
3
13
  BoundedFileReadError,
4
14
  CanonClient,
5
15
  MAX_CANON_MEDIA_BYTES,
6
- NodeMlsDevCustodyError,
7
16
  RUNTIME_NEW_SESSION_ACTION,
8
17
  RUNTIME_STOP_ACTION,
9
18
  RUNTIME_STOP_AND_DROP_ACTION,
10
19
  acquireLock,
11
- assertCompletedOutputAudience,
12
- buildChunkedMessagePartMetadata,
13
20
  conversationMediaCacheDir,
14
21
  createCompletedOutputJournal,
15
- createNodeMlsDevCustody,
16
- createPortableNodeMlsDevCustody,
17
22
  createRuntimeHeartbeat,
18
23
  createRuntimeStatePublisher,
19
24
  inferUploadMimeType,
20
25
  initRTDBAuth,
21
26
  isPendingCanonOperation,
22
- isRetryableCanonDeliveryError,
23
27
  materializeAttachment,
24
28
  normalizeRuntimeCommandDescriptors,
25
29
  readBoundedRegularFileHandle,
26
30
  renderCanonHostInboundContent,
27
- reportNoReplyOutcome,
28
31
  resolveCanonRuntimeConnection,
29
- resolveTurnVerbosity,
30
- sendMessageWithRetryChunked,
31
- splitTextByUtf8Bytes,
32
+ splitTextWithinBytes,
32
33
  verifyCanonRuntimeConnection
33
- } from "./chunk-TFSURFF6.js";
34
+ } from "./chunk-TYZT346O.js";
35
+ import {
36
+ NodeMlsDevCustodyError,
37
+ createNodeMlsDevCustody,
38
+ createPortableNodeMlsDevCustody
39
+ } from "./chunk-YJUPKU47.js";
40
+ import {
41
+ parseMlsDevClientConfiguration
42
+ } from "./chunk-YHDDOADK.js";
34
43
  import {
35
44
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
36
45
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
@@ -44,9 +53,11 @@ import {
44
53
  MLS_DEV_TEXT_MAX_BYTES,
45
54
  MLS_DEV_TEXT_PROTOCOL_V3,
46
55
  NO_REPLY_ACK_NOTE,
47
- VERB_LIMITS,
56
+ PARTICIPATION_HISTORY_FETCH_LIMIT,
48
57
  __name,
58
+ buildParticipationHistorySnapshot,
49
59
  connectorRelayResponseBytes,
60
+ evaluateParticipationPolicy,
50
61
  isConnectorRelayCredential,
51
62
  isConnectorRelayInstallation,
52
63
  isMlsDevTextProtocol,
@@ -54,12 +65,12 @@ import {
54
65
  mlsDevProtocolCarriesCards,
55
66
  parseConnectorRelayRequest,
56
67
  parseDirectConversationSelection,
57
- parseMlsDevClientConfiguration,
58
68
  parseMlsDevCredentialToken,
59
69
  parseMlsDevGroupHead,
60
70
  parseMlsDevGroupRoster,
61
- resolveAllowedUploadMimeType
62
- } from "./chunk-C2WJT3VX.js";
71
+ resolveAllowedUploadMimeType,
72
+ resolveConversationPolicyScope
73
+ } from "./chunk-5WPAB5MB.js";
63
74
 
64
75
  // src/runner/types.ts
65
76
  var DriverSessionError = class extends Error {
@@ -106,11 +117,13 @@ async function loadInstructions(path) {
106
117
  if (real !== path) refuse(`must be given by its real path (${real}), with no symbolic links`);
107
118
  const parent = await stat(dirname(path)).catch(() => refuse("cannot be read"));
108
119
  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");
120
+ const windows = process.platform === "win32" ? windowsAclProblems([[dirname(path), "unwritable"], [path, "unwritable"]]) : void 0;
121
+ if (windows?.length) refuse(`must not be writable by others: ${windows[0]}`);
122
+ if (!windows && (parent.uid !== uid && parent.uid !== 0 || (parent.mode & 18) !== 0)) refuse("its directory must not be writable by group or others");
110
123
  const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(() => refuse("cannot be read"));
111
124
  try {
112
125
  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");
126
+ if (!windows && (info.uid !== uid || (info.mode & 18) !== 0)) refuse("must be owned by you and not writable by group or others");
114
127
  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
128
  let text3;
116
129
  try {
@@ -126,11 +139,13 @@ async function loadInstructions(path) {
126
139
  }
127
140
  __name(loadInstructions, "loadInstructions");
128
141
 
129
- // src/drivers/claude.ts
130
- import { randomUUID } from "node:crypto";
142
+ // src/drivers/acp.ts
143
+ import { spawn as spawn2 } from "node:child_process";
144
+ import { randomBytes } from "node:crypto";
131
145
  import { realpathSync } from "node:fs";
132
146
  import { tmpdir } from "node:os";
133
- import { basename, dirname as dirname2 } from "node:path";
147
+ import { basename } from "node:path";
148
+ import { pathToFileURL } from "node:url";
134
149
 
135
150
  // src/drivers/call-scope.ts
136
151
  import { createHash } from "node:crypto";
@@ -141,17 +156,466 @@ function callScope(name, call) {
141
156
  }
142
157
  __name(callScope, "callScope");
143
158
 
159
+ // src/drivers/installed-cli.ts
160
+ import { spawn } from "node:child_process";
161
+ import { existsSync, readFileSync } from "node:fs";
162
+ import { dirname as dirname2, extname, join } from "node:path";
163
+ function shimTarget(shim) {
164
+ const last = [...shim.matchAll(/"([^"]+)"\s+%\*/g)].at(-1)?.[1];
165
+ const variable = /^%(\w+)%$/.exec(last ?? "")?.[1];
166
+ const target = variable ? new RegExp(`SET "${variable}=([^"]+)"`, "i").exec(shim)?.[1] : last;
167
+ return /^%~?dp0%?\\(.+)$/.exec(target ?? "")?.[1];
168
+ }
169
+ __name(shimTarget, "shimTarget");
170
+ function installedCli(path) {
171
+ const given = { file: path, args: [] };
172
+ if (process.platform !== "win32") return given;
173
+ const names = extname(path) ? [path] : [`${path}.exe`, `${path}.cmd`];
174
+ const directories = /[\\/]/.test(path) ? [""] : (process.env.PATH ?? "").split(";").filter(Boolean);
175
+ for (const directory of directories) {
176
+ for (const name of names) {
177
+ const file = join(directory, name);
178
+ if (!existsSync(file)) continue;
179
+ if (!/\.(?:cmd|bat)$/i.test(file)) return { file, args: [] };
180
+ const target = shimTarget(readFileSync(file, "utf8"));
181
+ if (!target) throw new Error(`${file} is not an npm shim: give the path of the program it runs.`);
182
+ const script = join(dirname2(file), target);
183
+ return /\.[cm]?js$/i.test(script) ? { file: process.execPath, args: [script] } : { file: script, args: [] };
184
+ }
185
+ }
186
+ return given;
187
+ }
188
+ __name(installedCli, "installedCli");
189
+ function stopCli(child) {
190
+ if (process.platform !== "win32") {
191
+ child.kill("SIGTERM");
192
+ return;
193
+ }
194
+ if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
195
+ spawn(
196
+ join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe"),
197
+ ["/PID", String(child.pid), "/T", "/F"],
198
+ { stdio: "ignore", windowsHide: true }
199
+ ).once("error", () => {
200
+ child.kill();
201
+ });
202
+ }
203
+ __name(stopCli, "stopCli");
204
+
205
+ // src/drivers/subprocess.ts
206
+ import { createHash as createHash2 } from "node:crypto";
207
+ import { createInterface } from "node:readline";
208
+ var isRecord = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
209
+ var text = /* @__PURE__ */ __name((value) => typeof value === "string" ? value : "", "text");
210
+ var oneLine = /* @__PURE__ */ __name((value, max) => {
211
+ const line = value.replace(/\s+/g, " ").trim();
212
+ return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
213
+ }, "oneLine");
214
+ 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;
215
+ var guestTurn = /* @__PURE__ */ __name((turn) => !turn.input.messages.every((message2) => message2.isOwner || message2.trustedAgent === true), "guestTurn");
216
+ async function approve(turn, request) {
217
+ const asked = createHash2("sha256").update(JSON.stringify([request.scopeKey, request.title, request.detail])).digest("hex").slice(0, 12);
218
+ const answer = await turn.ask({ kind: "approve", ...request, id: `${request.id}#${asked}` }).catch(() => void 0);
219
+ if (!answer) return "stopped";
220
+ if (answer.status !== "answered" || !answer.allow) return "denied";
221
+ return answer.scope === "session" ? "session" : "once";
222
+ }
223
+ __name(approve, "approve");
224
+ var PIPE_GRACE_MS = 1e3;
225
+ function stdioRpc(child, handlers, { name, envelope }) {
226
+ let next = 0;
227
+ const pending = /* @__PURE__ */ new Map();
228
+ const write = /* @__PURE__ */ __name((message2) => {
229
+ if (child.stdin?.writable) child.stdin.write(`${JSON.stringify({ ...envelope, ...message2 })}
230
+ `);
231
+ }, "write");
232
+ child.stdin?.on("error", () => void 0);
233
+ createInterface({ input: child.stdout }).on("error", () => void 0).on("line", (line) => {
234
+ let message2;
235
+ try {
236
+ message2 = JSON.parse(line);
237
+ } catch {
238
+ return;
239
+ }
240
+ if (!isRecord(message2)) return;
241
+ const params = isRecord(message2.params) ? message2.params : {};
242
+ if (typeof message2.method !== "string") {
243
+ const waiting = pending.get(Number(message2.id));
244
+ pending.delete(Number(message2.id));
245
+ if (isRecord(message2.error)) waiting?.reject(Object.assign(new Error(text(message2.error.message) || `${name} refused the request`), { code: message2.error.code }));
246
+ else waiting?.resolve(message2.result);
247
+ } else if (message2.id !== void 0) {
248
+ handlers.request(message2.method, params, message2.id).then(
249
+ (result) => write({ id: message2.id, result }),
250
+ (error) => write({ id: message2.id, error: {
251
+ code: typeof error?.code === "number" ? error.code : -32e3,
252
+ message: error instanceof Error ? error.message : String(error)
253
+ } })
254
+ );
255
+ } else handlers.notify(message2.method, params);
256
+ });
257
+ child.stderr?.resume();
258
+ child.once("exit", () => {
259
+ setTimeout(() => {
260
+ child.stdout?.destroy();
261
+ child.stderr?.destroy();
262
+ }, PIPE_GRACE_MS).unref();
263
+ });
264
+ return {
265
+ call: /* @__PURE__ */ __name((method, params) => new Promise((resolve3, reject) => {
266
+ next += 1;
267
+ pending.set(next, { resolve: /* @__PURE__ */ __name((value) => resolve3(isRecord(value) ? value : {}), "resolve"), reject });
268
+ write({ id: next, method, params });
269
+ }), "call"),
270
+ notify: /* @__PURE__ */ __name((method, params) => write({ method, ...params ? { params } : {} }), "notify"),
271
+ fail(error) {
272
+ for (const waiting of pending.values()) waiting.reject(error);
273
+ pending.clear();
274
+ }
275
+ };
276
+ }
277
+ __name(stdioRpc, "stdioRpc");
278
+
279
+ // src/drivers/acp.ts
280
+ var PROTOCOL = 1;
281
+ var SESSION = "sessionId";
282
+ var OWNER_MODE = "ownerMode";
283
+ var CANON = "canon";
284
+ var METHOD_NOT_FOUND = -32601;
285
+ var AUTH_REQUIRED = -32e3;
286
+ 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.";
287
+ 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.";
288
+ var OWNER_ONLY = "This agent only takes messages from its owner.";
289
+ var KILL_AFTER_MS = 2e3;
290
+ var MACHINE_KINDS = /* @__PURE__ */ new Set(["read", "edit", "delete", "move", "search", "execute", "fetch"]);
291
+ var BRIDGE = `
292
+ const { request } = require('node:http');
293
+ const url = new URL(process.env.TURN_TOOLS_URL);
294
+ const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' };
295
+ require('node:readline').createInterface({ input: process.stdin }).on('line', (line) => {
296
+ const post = request(url, { method: 'POST', headers }, (response) => {
297
+ let body = '';
298
+ response.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
299
+ response.on('end', () => { if (body.trim()) process.stdout.write(body.trim() + '\\n'); });
300
+ });
301
+ post.on('error', () => undefined);
302
+ post.end(line);
303
+ });`;
304
+ function startAgent(command, args, cwd) {
305
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("CANON_")));
306
+ const cli = installedCli(command);
307
+ return spawn2(cli.file, [...cli.args, ...args], { cwd, env, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32", windowsHide: true });
308
+ }
309
+ __name(startAgent, "startAgent");
310
+ function signalAgent(child, signal) {
311
+ if (process.platform === "win32") {
312
+ stopCli(child);
313
+ return;
314
+ }
315
+ try {
316
+ if (child.pid) process.kill(-child.pid, signal);
317
+ else child.kill(signal);
318
+ } catch {
319
+ }
320
+ }
321
+ __name(signalAgent, "signalAgent");
322
+ var refuseMethod = /* @__PURE__ */ __name((method) => Promise.reject(Object.assign(new Error(`Canon does not offer ${method}`), { code: METHOD_NOT_FOUND })), "refuseMethod");
323
+ var restoreMethod = /* @__PURE__ */ __name((capabilities) => {
324
+ if (isRecord(capabilities.sessionCapabilities) && isRecord(capabilities.sessionCapabilities.resume)) return "session/resume";
325
+ return capabilities.loadSession === true ? "session/load" : void 0;
326
+ }, "restoreMethod");
327
+ async function initialize(link, agent) {
328
+ const result = await link.call("initialize", {
329
+ protocolVersion: PROTOCOL,
330
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
331
+ clientInfo: { name: "canon-agent", title: "Canon", version: "1" }
332
+ });
333
+ if (result.protocolVersion !== PROTOCOL) {
334
+ throw new Error(`${agent} speaks Agent Client Protocol version ${String(result.protocolVersion)}; the acp driver speaks version ${PROTOCOL}`);
335
+ }
336
+ return { capabilities: isRecord(result.agentCapabilities) ? result.agentCapabilities : {}, info: isRecord(result.agentInfo) ? result.agentInfo : {} };
337
+ }
338
+ __name(initialize, "initialize");
339
+ async function checkAcp({ command, args = [], cwd, timeoutMs = 3e4 }) {
340
+ const child = startAgent(command, args, realpathSync(cwd));
341
+ const link = stdioRpc(child, { notify: /* @__PURE__ */ __name(() => void 0, "notify"), request: /* @__PURE__ */ __name((method) => refuseMethod(method), "request") }, { name: basename(command), envelope: { jsonrpc: "2.0" } });
342
+ let timer;
343
+ try {
344
+ return await new Promise((resolve3, reject) => {
345
+ timer = setTimeout(() => reject(new Error(`${command} did not answer the Agent Client Protocol's initialize within ${timeoutMs / 1e3} s`)), timeoutMs);
346
+ child.once("error", (error) => reject(new Error(`Could not run ${command}: ${error.message}`)));
347
+ child.once("close", () => reject(new Error(`${command} exited before answering the Agent Client Protocol's initialize`)));
348
+ initialize(link, command).then(({ capabilities, info }) => resolve3({
349
+ name: text(info.title) || text(info.name) || basename(command),
350
+ version: text(info.version),
351
+ sessionMemory: restoreMethod(capabilities) ? "native" : "none"
352
+ }), reject);
353
+ });
354
+ } finally {
355
+ clearTimeout(timer);
356
+ child.stdin?.end();
357
+ signalAgent(child, "SIGTERM");
358
+ setTimeout(() => signalAgent(child, "SIGKILL"), KILL_AFTER_MS).unref();
359
+ }
360
+ }
361
+ __name(checkAcp, "checkAcp");
362
+ var present = /* @__PURE__ */ __name((fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== null && value !== void 0)), "present");
363
+ var toolIdentity = /* @__PURE__ */ __name((call) => text(call.name) || text(call.title), "toolIdentity");
364
+ function canonTool(call, turn, server, guest) {
365
+ if (!["", "other"].includes(text(call.kind)) || guest && !text(call.name)) return false;
366
+ const name = toolIdentity(call).replace(/\W+/g, "_").replace(/^mcp_+/, "");
367
+ return turn.tools.definitions.some((tool) => name === `${server}_${tool.name}` || name === `${server}__${tool.name}`);
368
+ }
369
+ __name(canonTool, "canonTool");
370
+ function acpDriver(options) {
371
+ const { command, args = [] } = options;
372
+ if (options.guestMode !== void 0 && (options.unguardedGuests || !options.guestMode.trim())) {
373
+ throw new Error("guestMode must name a mode, and cannot go with unguardedGuests");
374
+ }
375
+ const cwd = realpathSync(options.cwd);
376
+ const agent = basename(command);
377
+ const interruptGraceMs = options.interruptGraceMs ?? 5e3;
378
+ const framing = withInstructions(SYSTEM, options.instructions);
379
+ const running = /* @__PURE__ */ new Map();
380
+ const end = /* @__PURE__ */ __name(async (session) => {
381
+ const live = running.get(session);
382
+ if (!live) return;
383
+ live.close();
384
+ await live.exited;
385
+ }, "end");
386
+ return {
387
+ name: "acp",
388
+ capabilities: {
389
+ sessionMemory: options.sessionMemory ?? "none",
390
+ ask: ["approve", "choose", "text"],
391
+ streaming: true,
392
+ activity: true,
393
+ tools: true,
394
+ outboundFiles: { roots: [cwd, realpathSync(tmpdir())] },
395
+ ownerHistoryOnly: options.guestMode === void 0 && !options.unguardedGuests
396
+ },
397
+ async runTurn(turn) {
398
+ await running.get(turn.session)?.exited;
399
+ if (turn.signal.aborted) throw turn.signal.reason;
400
+ const guest = guestTurn(turn);
401
+ if (guest && options.guestMode === void 0 && !options.unguardedGuests) return { reply: OWNER_ONLY };
402
+ const child = startAgent(command, args, cwd);
403
+ let exit = /* @__PURE__ */ __name(() => void 0, "exit");
404
+ const exited = new Promise((resolve3) => {
405
+ exit = resolve3;
406
+ });
407
+ let killing;
408
+ let stopping = false;
409
+ const entry = { close: /* @__PURE__ */ __name(() => {
410
+ stopping = true;
411
+ signalAgent(child, "SIGTERM");
412
+ killing ??= setTimeout(() => signalAgent(child, "SIGKILL"), KILL_AFTER_MS);
413
+ }, "close"), exited };
414
+ running.set(turn.session, entry);
415
+ const server = `${CANON}_${randomBytes(4).toString("hex")}`;
416
+ return new Promise((resolve3, reject) => {
417
+ const messages = [];
418
+ const calls = /* @__PURE__ */ new Map();
419
+ let sessionId = "";
420
+ let answering = false;
421
+ let continues = false;
422
+ let closed = false;
423
+ let ended = Promise.resolve();
424
+ let closing;
425
+ const visible = /* @__PURE__ */ __name(() => messages.map((message2) => message2.text).filter((part) => part.trim()).join("\n\n"), "visible");
426
+ const closeLater = /* @__PURE__ */ __name(() => {
427
+ if (!closed) closing ??= setTimeout(entry.close, interruptGraceMs);
428
+ }, "closeLater");
429
+ const finish = /* @__PURE__ */ __name(() => {
430
+ if (!closed) child.stdin?.end();
431
+ closeLater();
432
+ }, "finish");
433
+ const interrupt = /* @__PURE__ */ __name(() => {
434
+ if (sessionId && answering) link.notify("session/cancel", { sessionId });
435
+ closeLater();
436
+ }, "interrupt");
437
+ const settle = /* @__PURE__ */ __name((outcome) => {
438
+ answering = false;
439
+ finish();
440
+ if (turn.signal.aborted) reject(turn.signal.reason);
441
+ else if (outcome instanceof Error) reject(outcome);
442
+ else resolve3(outcome);
443
+ }, "settle");
444
+ const stopGuest = /* @__PURE__ */ __name((what) => {
445
+ link.notify("session/cancel", { sessionId });
446
+ entry.close();
447
+ settle(new DriverSessionError(`${agent} ${what} in a turn holding someone other than the owner's message`));
448
+ }, "stopGuest");
449
+ const guard = /* @__PURE__ */ __name((call) => {
450
+ if (guest && MACHINE_KINDS.has(text(call.kind)) && ["in_progress", "completed"].includes(text(call.status))) stopGuest(`ran a ${text(call.kind)} tool`);
451
+ }, "guard");
452
+ const known = /* @__PURE__ */ __name((update2, session) => {
453
+ const key = JSON.stringify([text(session), text(update2.toolCallId)]);
454
+ const call = { ...calls.get(key), ...present(update2) };
455
+ calls.set(key, call);
456
+ return call;
457
+ }, "known");
458
+ const update = /* @__PURE__ */ __name((params) => {
459
+ const change = isRecord(params.update) ? params.update : {};
460
+ if (!answering) return;
461
+ const tool = change.sessionUpdate === "tool_call" || change.sessionUpdate === "tool_call_update";
462
+ const call = tool ? known(change, params.sessionId) : void 0;
463
+ if (call) guard(call);
464
+ if (guest && options.guestMode !== void 0 && change.sessionUpdate === "current_mode_update" && change.currentModeId !== options.guestMode) stopGuest("left its guest mode");
465
+ if (text(params.sessionId) !== sessionId) return;
466
+ if (change.sessionUpdate === "agent_message_chunk") {
467
+ const chunk = isRecord(change.content) && change.content.type === "text" ? text(change.content.text) : "";
468
+ if (!chunk) return;
469
+ const id = text(change.messageId);
470
+ const message2 = id ? messages.find((known2) => known2.id === id) : continues ? messages.at(-1) : void 0;
471
+ if (message2) message2.text += chunk;
472
+ else messages.push({ id, text: chunk });
473
+ continues = true;
474
+ turn.onText(visible());
475
+ } else if (call) {
476
+ if (change.sessionUpdate === "tool_call") continues = false;
477
+ turn.onActivity({
478
+ id: text(call.toolCallId),
479
+ kind: "tool",
480
+ title: oneLine(text(call.title) || "Use a tool", 120),
481
+ state: call.status === "completed" ? "done" : call.status === "failed" ? "failed" : "running"
482
+ });
483
+ }
484
+ }, "update");
485
+ const permission = /* @__PURE__ */ __name(async (params) => {
486
+ const offered = (Array.isArray(params.options) ? params.options : []).filter(isRecord);
487
+ const pick = /* @__PURE__ */ __name((kind) => offered.find((option) => option.kind === kind), "pick");
488
+ const select = /* @__PURE__ */ __name((option) => ({ outcome: option ? { outcome: "selected", optionId: text(option.optionId) } : { outcome: "cancelled" } }), "select");
489
+ if (turn.signal.aborted) return select(void 0);
490
+ const allow = pick("allow_once");
491
+ const refuse = select(pick("reject_once") ?? pick("reject_always"));
492
+ const call = known(isRecord(params.toolCall) ? params.toolCall : {}, params.sessionId);
493
+ if (allow && canonTool(call, turn, server, guest)) return select(allow);
494
+ if (guest || !allow) return refuse;
495
+ const input = call.rawInput === void 0 || ["{}", "[]", '""', "null"].includes(JSON.stringify(call.rawInput)) ? "" : JSON.stringify(call.rawInput);
496
+ const title = oneLine(text(call.title) || "Use a tool", 200);
497
+ const paths = (Array.isArray(call.locations) ? call.locations : []).filter(isRecord).map((location) => text(location.path));
498
+ const edits = (Array.isArray(call.content) ? call.content : []).filter(isRecord).filter((item) => item.type === "diff").map((item) => `${text(item.path)}
499
+ ${text(item.newText)}`);
500
+ const id = `${text(params.sessionId) === sessionId ? "" : `${text(params.sessionId)}:`}${text(call.toolCallId) || "tool"}`;
501
+ const verdict = await approve(turn, {
502
+ id,
503
+ title,
504
+ detail: [input, ...paths, ...edits].filter(Boolean).join("\n"),
505
+ scopeKey: callScope(text(call.kind) || "tool", [toolIdentity(call) || title, input, call.locations ?? null]),
506
+ allowSession: input !== "" && text(call.name) !== "",
507
+ // JSON punctuation goes first, so a command given as an argument list reads as one.
508
+ ...call.kind === "delete" || RISKY.test(input.replace(/[",[\]{}]+/g, " ")) ? { risk: "destructive" } : {}
509
+ });
510
+ if (verdict === "stopped" || turn.signal.aborted) return select(void 0);
511
+ return verdict === "denied" ? refuse : select(allow);
512
+ }, "permission");
513
+ const link = stdioRpc(child, {
514
+ notify: /* @__PURE__ */ __name((method, params) => {
515
+ if (method === "session/update") update(params);
516
+ }, "notify"),
517
+ request: /* @__PURE__ */ __name((method, params) => method === "session/request_permission" ? permission(params) : refuseMethod(method), "request")
518
+ }, { name: agent, envelope: { jsonrpc: "2.0" } });
519
+ const gone = /* @__PURE__ */ __name((error) => {
520
+ if (closed) return;
521
+ closed = true;
522
+ clearTimeout(closing);
523
+ clearTimeout(killing);
524
+ turn.signal.removeEventListener("abort", interrupt);
525
+ link.fail(error);
526
+ settle(error);
527
+ void ended.then(() => {
528
+ if (running.get(turn.session) === entry) running.delete(turn.session);
529
+ exit();
530
+ });
531
+ }, "gone");
532
+ child.once("exit", () => {
533
+ if (stopping || guest || turn.signal.aborted) {
534
+ signalAgent(child, "SIGTERM");
535
+ ended = new Promise((resolve4) => {
536
+ setTimeout(() => {
537
+ signalAgent(child, "SIGKILL");
538
+ resolve4();
539
+ }, KILL_AFTER_MS);
540
+ });
541
+ }
542
+ });
543
+ child.once("error", (error) => gone(new Error(`Could not run ${command}: ${error.message}`)));
544
+ child.once("close", () => gone(new Error(`${agent} exited before finishing the turn`)));
545
+ turn.signal.addEventListener("abort", interrupt, { once: true });
546
+ 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");
547
+ async function openSession(capabilities) {
548
+ const mcp = isRecord(capabilities.mcpCapabilities) ? capabilities.mcpCapabilities : {};
549
+ const { url } = turn.tools;
550
+ 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 }] }];
551
+ const restore = options.sessionMemory === "native" ? restoreMethod(capabilities) : void 0;
552
+ const saved = restore && turn.state.get(SESSION);
553
+ if (saved) {
554
+ const restored = await link.call(restore, { sessionId: saved, cwd, mcpServers }).catch((error) => {
555
+ throw signInError(error) ?? (turn.signal.aborted ? error : new DriverSessionError(`${agent} could not restore session ${saved}`, { cause: error }));
556
+ });
557
+ return { id: saved, fresh: false, keep: false, modes: restored.modes };
558
+ }
559
+ const opened = await link.call("session/new", { cwd, mcpServers }).catch((error) => {
560
+ throw signInError(error) ?? error;
561
+ });
562
+ if (!text(opened.sessionId)) throw new Error(`${agent} opened no session`);
563
+ return { id: text(opened.sessionId), fresh: true, keep: Boolean(restore), modes: opened.modes };
564
+ }
565
+ __name(openSession, "openSession");
566
+ async function applyMode(modes) {
567
+ if (!options.guestMode) return;
568
+ const current = isRecord(modes) ? text(modes.currentModeId) : "";
569
+ if (!turn.state.get(OWNER_MODE) && current) turn.state.set(OWNER_MODE, current);
570
+ const wanted = guest ? options.guestMode : turn.state.get(OWNER_MODE) ?? current;
571
+ if (wanted && wanted !== current) {
572
+ await link.call("session/set_mode", { sessionId, modeId: wanted }).catch((error) => {
573
+ throw new Error(`${agent} could not enter mode "${wanted}": ${oneLine(error.message, 200)}`);
574
+ });
575
+ }
576
+ }
577
+ __name(applyMode, "applyMode");
578
+ void (async () => {
579
+ const { capabilities } = await initialize(link, agent);
580
+ const session = await openSession(capabilities);
581
+ sessionId = session.id;
582
+ await applyMode(session.modes);
583
+ if (turn.signal.aborted) return finish();
584
+ if (session.keep) turn.state.set(SESSION, sessionId);
585
+ answering = true;
586
+ const { stopReason } = await link.call("session/prompt", { sessionId, prompt: [
587
+ { type: "text", text: [...session.fresh ? [framing] : [], ...guest ? [GUEST] : [], turn.prompt].join("\n\n") },
588
+ ...turn.files.map((file) => ({ type: "resource_link", name: basename(file), uri: pathToFileURL(file).href }))
589
+ ] });
590
+ if (stopReason === "end_turn") return settle({ reply: messages.filter((message2) => message2.text.trim()).at(-1)?.text ?? null });
591
+ settle(new Error(`${agent} ended the turn: ${String(stopReason)}`));
592
+ })().catch((error) => settle(error instanceof DriverSessionError ? error : new Error(oneLine(error instanceof Error ? error.message : String(error), 300))));
593
+ });
594
+ },
595
+ // New session (and the runner's reset after a stuck turn): the room's process goes; the runner drops the state.
596
+ reset: end,
597
+ async close() {
598
+ await Promise.all([...running.keys()].map(end));
599
+ }
600
+ };
601
+ }
602
+ __name(acpDriver, "acpDriver");
603
+
144
604
  // src/drivers/claude.ts
605
+ import { randomUUID } from "node:crypto";
606
+ import { realpathSync as realpathSync2 } from "node:fs";
607
+ import { tmpdir as tmpdir2 } from "node:os";
608
+ import { basename as basename2, dirname as dirname3 } from "node:path";
145
609
  var SDK = "@anthropic-ai/claude-agent-sdk";
146
- var SESSION = "sessionId";
610
+ var SESSION2 = "sessionId";
147
611
  var ASK_USER_DIALOG = "permission_ask_user_question";
148
612
  var CANON_TOOLS = "mcp__canon__";
149
613
  var NOT_OWNER = "Only the agent's owner can let it use tools in this conversation.";
150
614
  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");
615
+ 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.";
616
+ var isRecord2 = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
153
617
  var forOwner = /* @__PURE__ */ __name((message2) => message2.isOwner || message2.trustedAgent === true, "forOwner");
154
- var oneLine = /* @__PURE__ */ __name((text3, max) => {
618
+ var oneLine2 = /* @__PURE__ */ __name((text3, max) => {
155
619
  const line = text3.replace(/\s+/g, " ").trim();
156
620
  return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
157
621
  }, "oneLine");
@@ -165,10 +629,10 @@ async function loadClaudeQuery() {
165
629
  __name(loadClaudeQuery, "loadClaudeQuery");
166
630
  function describeTool(name, input) {
167
631
  const label = name.startsWith("mcp__") ? name.split("__").slice(2).join("__") || name : name;
168
- const fields = isRecord(input) ? input : {};
632
+ const fields = isRecord2(input) ? input : {};
169
633
  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);
634
+ const detail = key && (key.endsWith("path") ? basename2(fields[key]) : fields[key]);
635
+ return oneLine2(detail ? `${label}: ${detail}` : label, 120);
172
636
  }
173
637
  __name(describeTool, "describeTool");
174
638
  function liveReader(turn) {
@@ -194,14 +658,14 @@ function liveReader(turn) {
194
658
  else if (event.type === "content_block_delta" && event.delta?.type === "text_delta") append(event.index ?? 0, String(event.delta.text ?? ""));
195
659
  } else if (message2.type === "assistant") {
196
660
  for (const block of message2.message.content) {
197
- if (!isRecord(block) || block.type !== "tool_use" || typeof block.id !== "string") continue;
661
+ if (!isRecord2(block) || block.type !== "tool_use" || typeof block.id !== "string") continue;
198
662
  const title = describeTool(String(block.name), block.input);
199
663
  tools.set(block.id, title);
200
664
  turn.onActivity({ id: block.id, kind: "tool", title, state: "running" });
201
665
  }
202
666
  } else if (message2.type === "user" && Array.isArray(message2.message.content)) {
203
667
  for (const block of message2.message.content) {
204
- if (!isRecord(block) || block.type !== "tool_result" || !tools.has(String(block.tool_use_id))) continue;
668
+ if (!isRecord2(block) || block.type !== "tool_result" || !tools.has(String(block.tool_use_id))) continue;
205
669
  const id = String(block.tool_use_id);
206
670
  turn.onActivity({ id, kind: "tool", title: tools.get(id), state: block.is_error === true ? "failed" : "done" });
207
671
  }
@@ -219,12 +683,12 @@ function toolScope(toolName, input) {
219
683
  return callScope(toolName, toolName.startsWith("mcp__") ? input : Object.fromEntries(Object.entries(input).filter(([key]) => key !== "description")));
220
684
  }
221
685
  __name(toolScope, "toolScope");
222
- async function approve(turn, toolName, input, prompt) {
686
+ async function approve2(turn, toolName, input, prompt) {
223
687
  const detail = typeof input.command === "string" ? input.command : JSON.stringify(input);
224
688
  const answer = await turn.ask({
225
689
  kind: "approve",
226
690
  id: prompt.toolUseID,
227
- title: oneLine(prompt.title || describeTool(toolName, input), 200),
691
+ title: oneLine2(prompt.title || describeTool(toolName, input), 200),
228
692
  scopeKey: toolScope(toolName, input),
229
693
  allowSession: prompt.suppressAlwaysAllowRule !== true,
230
694
  ...detail && detail !== "{}" ? { detail } : {},
@@ -234,14 +698,14 @@ async function approve(turn, toolName, input, prompt) {
234
698
  if (answer.status !== "answered") return { behavior: "deny", message: DECLINED[answer.status] };
235
699
  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
700
  }
237
- __name(approve, "approve");
701
+ __name(approve2, "approve");
238
702
  async function answerQuestions(turn, id, questions, input) {
239
- const list = (Array.isArray(questions) ? questions : []).filter(isRecord).slice(0, 4);
703
+ const list = (Array.isArray(questions) ? questions : []).filter(isRecord2).slice(0, 4);
240
704
  if (!list.length) return { behavior: "deny", message: "There was no question to ask." };
241
705
  const answers = {};
242
706
  for (const [index, question] of list.entries()) {
243
707
  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() !== ""))];
708
+ const labels = [...new Set((Array.isArray(question.options) ? question.options : []).filter(isRecord2).map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== ""))];
245
709
  const askId = list.length > 1 ? `${id}#${index + 1}` : id;
246
710
  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
711
  const answer = await turn.ask(request).catch(() => void 0);
@@ -253,10 +717,10 @@ async function answerQuestions(turn, id, questions, input) {
253
717
  }
254
718
  __name(answerQuestions, "answerQuestions");
255
719
  function claudeDriver(options) {
256
- const cwd = realpathSync(options.cwd);
720
+ const cwd = realpathSync2(options.cwd);
257
721
  const permissionMode = options.permissionMode ?? "default";
258
722
  const interruptGraceMs = options.interruptGraceMs ?? 5e3;
259
- const system = withInstructions(SYSTEM, options.instructions);
723
+ const system = withInstructions(SYSTEM2, options.instructions);
260
724
  let loaded;
261
725
  const running = /* @__PURE__ */ new Map();
262
726
  const end = /* @__PURE__ */ __name(async (session) => {
@@ -274,11 +738,11 @@ function claudeDriver(options) {
274
738
  const canUseTool = /* @__PURE__ */ __name(async (toolName, input, prompt) => {
275
739
  if (toolName.startsWith(CANON_TOOLS)) return { behavior: "allow", updatedInput: input };
276
740
  if (toolName === "AskUserQuestion") return answerQuestions(turn, prompt.toolUseID, input.questions, input);
277
- return approve(turn, toolName, input, prompt);
741
+ return approve2(turn, toolName, input, prompt);
278
742
  }, "canUseTool");
279
743
  const onUserDialog = /* @__PURE__ */ __name(async (request, { signal }) => {
280
744
  if (request.dialogKind !== ASK_USER_DIALOG) return { behavior: "cancelled" };
281
- const input = isRecord(request.payload.input) ? request.payload.input : { questions: request.payload.questions };
745
+ const input = isRecord2(request.payload.input) ? request.payload.input : { questions: request.payload.questions };
282
746
  const id = request.toolUseID || typeof request.payload.requestId === "string" && request.payload.requestId || randomUUID();
283
747
  const result = await answerQuestions(turn, id, input.questions, input);
284
748
  return signal.aborted ? { behavior: "cancelled" } : { behavior: "completed", result };
@@ -296,7 +760,7 @@ function claudeDriver(options) {
296
760
  ...resume ? { resume } : {},
297
761
  includePartialMessages: true,
298
762
  ...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)))],
763
+ additionalDirectories: [...new Set(turn.files.map((file) => dirname3(file)))],
300
764
  canUseTool,
301
765
  onUserDialog,
302
766
  supportedDialogKinds: [ASK_USER_DIALOG],
@@ -316,13 +780,13 @@ function claudeDriver(options) {
316
780
  streaming: true,
317
781
  activity: true,
318
782
  tools: true,
319
- outboundFiles: { roots: [cwd, realpathSync(tmpdir())] }
783
+ outboundFiles: { roots: [cwd, realpathSync2(tmpdir2())] }
320
784
  },
321
785
  async runTurn(turn) {
322
786
  await running.get(turn.session)?.exited;
323
787
  if (turn.signal.aborted) throw turn.signal.reason;
324
788
  const query = await (options.query ? Promise.resolve(options.query) : loaded ??= loadClaudeQuery());
325
- const resume = turn.state.get(SESSION);
789
+ const resume = turn.state.get(SESSION2);
326
790
  const native = query({ prompt: turn.prompt, options: queryOptions(turn, resume) });
327
791
  let exit = /* @__PURE__ */ __name(() => void 0, "exit");
328
792
  const entry = { close: /* @__PURE__ */ __name(() => native.close(), "close"), exited: new Promise((resolve3) => {
@@ -349,7 +813,7 @@ function claudeDriver(options) {
349
813
  const message2 = raw;
350
814
  if (message2.type === "system" && message2.subtype === "init") started = true;
351
815
  const id = message2.session_id;
352
- if (typeof id === "string" && id && id !== session) turn.state.set(SESSION, session = id);
816
+ if (typeof id === "string" && id && id !== session) turn.state.set(SESSION2, session = id);
353
817
  reader.read(message2);
354
818
  if (message2.type !== "result") continue;
355
819
  finished = true;
@@ -359,7 +823,7 @@ function claudeDriver(options) {
359
823
  resolve3({ reply: reply.trim() ? reply : null });
360
824
  } else {
361
825
  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)}` : ""}`)));
826
+ reject(failed(new Error(`Claude Code ended the turn with ${message2.subtype}${detail ? `: ${oneLine2(detail, 300)}` : ""}`)));
363
827
  }
364
828
  }
365
829
  if (!finished) throw new Error("Claude Code exited without finishing the turn");
@@ -384,8 +848,8 @@ function claudeDriver(options) {
384
848
  __name(claudeDriver, "claudeDriver");
385
849
 
386
850
  // src/drivers/code.ts
387
- import { realpathSync as realpathSync2 } from "node:fs";
388
- import { tmpdir as tmpdir2 } from "node:os";
851
+ import { realpathSync as realpathSync3 } from "node:fs";
852
+ import { tmpdir as tmpdir3 } from "node:os";
389
853
  function codeDriver(handler, capabilities = {}) {
390
854
  return {
391
855
  name: "code",
@@ -395,7 +859,7 @@ function codeDriver(handler, capabilities = {}) {
395
859
  streaming: true,
396
860
  activity: true,
397
861
  tools: true,
398
- outboundFiles: { roots: [realpathSync2(tmpdir2())] },
862
+ outboundFiles: { roots: [realpathSync3(tmpdir3())] },
399
863
  ...capabilities
400
864
  },
401
865
  async runTurn(turn) {
@@ -420,16 +884,14 @@ function codeDriver(handler, capabilities = {}) {
420
884
  __name(codeDriver, "codeDriver");
421
885
 
422
886
  // 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";
887
+ import { spawn as spawn3, spawnSync } from "node:child_process";
888
+ import { realpathSync as realpathSync4 } from "node:fs";
889
+ import { tmpdir as tmpdir4 } from "node:os";
890
+ import { basename as basename3 } from "node:path";
429
891
  var THREAD = "threadId";
430
- var CANON = "canon";
892
+ var CANON2 = "canon";
431
893
  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.";
894
+ 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
895
  var CANON_TOOL_TIMEOUT_SEC = 2 * 60 * 60;
434
896
  var MACHINE_FEATURES = [
435
897
  "shell_tool",
@@ -466,18 +928,11 @@ var GUEST_ITEMS = /* @__PURE__ */ new Set([
466
928
  "sleep"
467
929
  ]);
468
930
  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;
931
+ 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
932
  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
933
  function checkCodex(codexPath = "codex") {
480
- const result = spawnSync(codexPath, ["--version"], { encoding: "utf8" });
934
+ const cli = installedCli(codexPath);
935
+ const result = spawnSync(cli.file, [...cli.args, "--version"], { encoding: "utf8", windowsHide: true });
481
936
  if (result.status !== 0) {
482
937
  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
938
  }
@@ -485,20 +940,20 @@ function checkCodex(codexPath = "codex") {
485
940
  }
486
941
  __name(checkCodex, "checkCodex");
487
942
  function describe(item) {
488
- const paths = (Array.isArray(item.changes) ? item.changes : []).filter(isRecord2).map((change) => basename2(text(change.path)));
943
+ const paths = (Array.isArray(item.changes) ? item.changes : []).filter(isRecord).map((change) => basename3(text(change.path)));
489
944
  switch (item.type) {
490
945
  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);
946
+ const actions = (Array.isArray(item.commandActions) ? item.commandActions : []).filter(isRecord).map((action) => text(action.command));
947
+ return oneLine(actions.filter(Boolean).join(" && ") || text(item.command) || "Run a command", 120);
493
948
  }
494
949
  case "fileChange":
495
- return oneLine2(paths.length ? `Edit ${paths.join(", ")}` : "Edit files", 120);
950
+ return oneLine(paths.length ? `Edit ${paths.join(", ")}` : "Edit files", 120);
496
951
  case "mcpToolCall":
497
952
  return `${text(item.server)}: ${text(item.tool)}`;
498
953
  case "webSearch":
499
- return oneLine2(`Search the web${text(item.query) ? `: ${text(item.query)}` : ""}`, 120);
954
+ return oneLine(`Search the web${text(item.query) ? `: ${text(item.query)}` : ""}`, 120);
500
955
  case "imageView":
501
- return `View ${basename2(text(item.path))}`;
956
+ return `View ${basename3(text(item.path))}`;
502
957
  case "dynamicToolCall":
503
958
  return text(item.tool) || "Tool";
504
959
  default:
@@ -506,27 +961,19 @@ function describe(item) {
506
961
  }
507
962
  }
508
963
  __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
964
  var DECISION = { session: "acceptForSession", once: "accept", denied: "decline", stopped: "cancel" };
518
965
  async function answerCodexRequest(turn, method, params, items, rpcId) {
519
966
  const id = text(params.approvalId) || text(params.itemId) || text(params.elicitationId) || `request-${String(rpcId)}`;
520
967
  if (method === "item/commandExecution/requestApproval") {
521
968
  const command = text(params.command);
522
969
  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);
970
+ const network = isRecord(params.networkApprovalContext) ? params.networkApprovalContext : void 0;
971
+ const parsed = (Array.isArray(params.commandActions) ? params.commandActions : []).filter(isRecord).map((action) => text(action.command)).filter(Boolean);
525
972
  const destination = network && `${text(network.host)}${text(network.protocol) ? ` (${text(network.protocol)})` : ""}`;
526
973
  const scope = callScope("exec", [command, text(params.cwd), network ?? null, params.additionalPermissions ?? null]);
527
- const verdict = await approve2(turn, {
974
+ const verdict = await approve(turn, {
528
975
  id,
529
- title: oneLine2(text(params.reason) || (command ? `${stdin ? "Send input to" : "Run"}: ${parsed.join(" && ") || command}` : `Allow network access to ${destination || "the network"}`), 200),
976
+ title: oneLine(text(params.reason) || (command ? `${stdin ? "Send input to" : "Run"}: ${parsed.join(" && ") || command}` : `Allow network access to ${destination || "the network"}`), 200),
530
977
  detail: [
531
978
  command,
532
979
  text(params.cwd) && `in ${text(params.cwd)}`,
@@ -541,24 +988,24 @@ async function answerCodexRequest(turn, method, params, items, rpcId) {
541
988
  return { decision: DECISION[verdict] };
542
989
  }
543
990
  if (method === "item/fileChange/requestApproval") {
544
- const changes = (Array.isArray(items.get(text(params.itemId))?.changes) ? items.get(text(params.itemId)).changes : []).filter(isRecord2);
991
+ const changes = (Array.isArray(items.get(text(params.itemId))?.changes) ? items.get(text(params.itemId)).changes : []).filter(isRecord);
545
992
  const paths = [...new Set(changes.map((change) => text(change.path)))].sort();
546
- const verdict = await approve2(turn, {
993
+ const verdict = await approve(turn, {
547
994
  id,
548
- title: oneLine2(text(params.reason) || (paths.length ? `Edit ${paths.map((path) => basename2(path)).join(", ")}` : "Edit files"), 200),
995
+ title: oneLine(text(params.reason) || (paths.length ? `Edit ${paths.map((path) => basename3(path)).join(", ")}` : "Edit files"), 200),
549
996
  detail: [...changes.map((change) => `${text(change.path)}
550
997
  ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text(params.grantRoot)}`].filter(Boolean).join("\n\n"),
551
998
  scopeKey: callScope("apply_patch", paths),
552
999
  allowSession: paths.length > 0 && !params.grantRoot,
553
- ...changes.some((change) => isRecord2(change.kind) && change.kind.type === "delete") ? { risk: "destructive" } : {}
1000
+ ...changes.some((change) => isRecord(change.kind) && change.kind.type === "delete") ? { risk: "destructive" } : {}
554
1001
  });
555
1002
  return { decision: DECISION[verdict] };
556
1003
  }
557
1004
  if (method === "item/permissions/requestApproval") {
558
- const permissions = isRecord2(params.permissions) ? params.permissions : {};
559
- const verdict = await approve2(turn, {
1005
+ const permissions = isRecord(params.permissions) ? params.permissions : {};
1006
+ const verdict = await approve(turn, {
560
1007
  id,
561
- title: oneLine2(text(params.reason) || "Allow Codex more access", 200),
1008
+ title: oneLine(text(params.reason) || "Allow Codex more access", 200),
562
1009
  detail: JSON.stringify(permissions),
563
1010
  scopeKey: callScope("permissions", permissions),
564
1011
  allowSession: true
@@ -566,16 +1013,16 @@ ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text
566
1013
  return verdict === "session" || verdict === "once" ? { permissions, scope: verdict === "session" ? "session" : "turn" } : { permissions: {}, scope: "turn" };
567
1014
  }
568
1015
  if (method === "mcpServer/elicitation/request") {
569
- const meta = isRecord2(params._meta) ? params._meta : {};
1016
+ const meta = isRecord(params._meta) ? params._meta : {};
570
1017
  if (meta.codex_approval_kind !== "mcp_tool_call") return { action: "decline", content: null, _meta: null };
571
1018
  const server = text(params.serverName);
572
1019
  const named = text(meta.call_id) || text(meta.callId);
573
1020
  const running = [...items.values()].filter((item) => item.type === "mcpToolCall" && item.server === server && item.status === "inProgress");
574
1021
  const call = named ? items.get(named) : running.length === 1 ? running[0] : void 0;
575
1022
  const tool = call && call.server === server ? text(call.tool) : "";
576
- const verdict = await approve2(turn, {
1023
+ const verdict = await approve(turn, {
577
1024
  id: `mcp-${String(rpcId)}`,
578
- title: oneLine2(text(params.message) || `Use ${server}${tool ? `: ${tool}` : ""}`, 200),
1025
+ title: oneLine(text(params.message) || `Use ${server}${tool ? `: ${tool}` : ""}`, 200),
579
1026
  detail: JSON.stringify(meta.tool_params ?? call?.arguments ?? {}),
580
1027
  scopeKey: `codex:mcp:${server}:${tool}`,
581
1028
  allowSession: tool !== ""
@@ -587,10 +1034,10 @@ ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text
587
1034
  };
588
1035
  }
589
1036
  if (method === "item/tool/requestUserInput") {
590
- const questions = (Array.isArray(params.questions) ? params.questions : []).filter(isRecord2);
1037
+ const questions = (Array.isArray(params.questions) ? params.questions : []).filter(isRecord);
591
1038
  const answers = {};
592
1039
  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))];
1040
+ const labels = [...new Set((Array.isArray(question.options) ? question.options : []).filter(isRecord).map((option) => text(option.label)).filter(Boolean))];
594
1041
  const askId = questions.length > 1 ? `${id}#${index + 1}` : id;
595
1042
  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
1043
  const answer = await turn.ask(ask).catch(() => void 0);
@@ -602,55 +1049,11 @@ ${text(change.diff)}`.trim()), text(params.grantRoot) && `Write access to ${text
602
1049
  throw new Error(`The codex driver does not handle ${method}`);
603
1050
  }
604
1051
  __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
1052
  function codexDriver(options) {
650
- const cwd = realpathSync3(options.cwd);
1053
+ const cwd = realpathSync4(options.cwd);
651
1054
  const codexPath = options.codexPath ?? "codex";
652
1055
  const interruptGraceMs = options.interruptGraceMs ?? 5e3;
653
- const developerInstructions = withInstructions(SYSTEM2, options.instructions);
1056
+ const developerInstructions = withInstructions(SYSTEM3, options.instructions);
654
1057
  const running = /* @__PURE__ */ new Map();
655
1058
  const end = /* @__PURE__ */ __name(async (session) => {
656
1059
  const live = running.get(session);
@@ -661,8 +1064,8 @@ function codexDriver(options) {
661
1064
  async function threadSettings(turn, call, env) {
662
1065
  const guest = guestTurn(turn);
663
1066
  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) : [];
1067
+ const settings = isRecord(configured) ? configured : {};
1068
+ const servers = guest && isRecord(settings.mcp_servers) ? Object.keys(settings.mcp_servers).filter((name) => name !== CANON2) : [];
666
1069
  const apiKey = Boolean(env.OPENAI_API_KEY) && [void 0, null, "openai"].includes(settings.model_provider);
667
1070
  return {
668
1071
  cwd,
@@ -675,7 +1078,7 @@ function codexDriver(options) {
675
1078
  config: {
676
1079
  mcp_servers: {
677
1080
  ...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 } } : {}
1081
+ ...turn.tools.url ? { [CANON2]: { url: turn.tools.url, default_tools_approval_mode: "approve", tool_timeout_sec: CANON_TOOL_TIMEOUT_SEC } } : {}
679
1082
  },
680
1083
  // Questions go through Canon's ask_question alone: Codex's own request_user_input outside plan mode (a feature
681
1084
  // still under development) stays off even when the owner's configuration turns it on, so Codex never has two.
@@ -699,20 +1102,21 @@ function codexDriver(options) {
699
1102
  streaming: true,
700
1103
  activity: true,
701
1104
  tools: true,
702
- outboundFiles: { roots: [cwd, realpathSync3(tmpdir3())] }
1105
+ outboundFiles: { roots: [cwd, realpathSync4(tmpdir4())] }
703
1106
  },
704
1107
  async runTurn(turn) {
705
1108
  await running.get(turn.session)?.exited;
706
1109
  if (turn.signal.aborted) throw turn.signal.reason;
707
1110
  const guest = guestTurn(turn);
708
1111
  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"] });
1112
+ const cli = installedCli(codexPath);
1113
+ const child = spawn3(cli.file, [...cli.args, "app-server", "--listen", "stdio://"], { cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
710
1114
  let exit = /* @__PURE__ */ __name(() => void 0, "exit");
711
1115
  const exited = new Promise((resolve3) => {
712
1116
  exit = resolve3;
713
1117
  });
714
1118
  const entry = { close: /* @__PURE__ */ __name(() => {
715
- child.kill("SIGTERM");
1119
+ stopCli(child);
716
1120
  }, "close"), exited };
717
1121
  running.set(turn.session, entry);
718
1122
  return new Promise((resolve3, reject) => {
@@ -749,19 +1153,19 @@ function codexDriver(options) {
749
1153
  const all = [...messages.values()];
750
1154
  const reply = (all.filter((message2) => message2.done && message2.final).at(-1) ?? all.filter((message2) => message2.done).at(-1) ?? all.at(-1))?.text ?? "";
751
1155
  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)}` : ""}`}`));
1156
+ const detail = isRecord(ended.error) ? text(ended.error.message) : failure;
1157
+ settle(new Error(`Codex ended the turn ${ended.status === "interrupted" ? "interrupted" : `with an error${detail ? `: ${oneLine(detail, 300)}` : ""}`}`));
754
1158
  }, "conclude");
755
1159
  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;
1160
+ 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
1161
  interrupt();
758
1162
  entry.close();
759
1163
  const what = item.type === "mcpToolCall" ? `mcpToolCall (${describe(item)})` : text(item.type) || "an unknown item";
760
1164
  settle(new DriverSessionError(`Codex started ${what} in a turn holding someone other than the owner's message`));
761
1165
  }, "guard");
762
- const link = rpc(child, {
1166
+ const link = stdioRpc(child, {
763
1167
  notify(method, params) {
764
- const item = isRecord2(params.item) ? params.item : void 0;
1168
+ const item = isRecord(params.item) ? params.item : void 0;
765
1169
  if (item && method === "item/started") guard(item);
766
1170
  if (item) items.set(text(item.id), item);
767
1171
  if (!ours(params)) return;
@@ -777,15 +1181,15 @@ function codexDriver(options) {
777
1181
  const title = describe(item);
778
1182
  const state = method === "item/started" ? "running" : item.status === "failed" || item.status === "declined" ? "failed" : "done";
779
1183
  if (title) turn.onActivity({ id: text(item.id), kind: "tool", title, state });
780
- } else if (method === "error" && params.willRetry !== true && isRecord2(params.error)) {
1184
+ } else if (method === "error" && params.willRetry !== true && isRecord(params.error)) {
781
1185
  failure = text(params.error.message);
782
- } else if (method === "turn/completed" && isRecord2(params.turn)) {
1186
+ } else if (method === "turn/completed" && isRecord(params.turn)) {
783
1187
  completed.set(text(params.turn.id), params.turn);
784
1188
  conclude();
785
1189
  }
786
1190
  },
787
1191
  request: /* @__PURE__ */ __name((method, params, id) => answerCodexRequest(turn, method, params, items, id), "request")
788
- });
1192
+ }, { name: "Codex" });
789
1193
  const gone = /* @__PURE__ */ __name((error) => {
790
1194
  if (closed) return;
791
1195
  closed = true;
@@ -811,18 +1215,18 @@ function codexDriver(options) {
811
1215
  threadId = resume;
812
1216
  } else {
813
1217
  const started2 = await link.call("thread/start", settings);
814
- threadId = isRecord2(started2.thread) ? text(started2.thread.id) : "";
1218
+ threadId = isRecord(started2.thread) ? text(started2.thread.id) : "";
815
1219
  if (!threadId) throw new Error("Codex started no thread");
816
1220
  turn.state.set(THREAD, threadId);
817
1221
  }
818
1222
  if (turn.signal.aborted) return finish();
819
1223
  const started = await link.call("turn/start", { threadId, input: [
820
- ...guest ? [{ type: "text", text: GUEST, text_elements: [] }] : [],
1224
+ ...guest ? [{ type: "text", text: GUEST2, text_elements: [] }] : [],
821
1225
  { type: "text", text: turn.prompt, text_elements: [] },
822
1226
  // Images go to the model as images; every attachment's path is also in the prompt for Codex to read.
823
1227
  ...turn.files.filter((file) => IMAGE.test(file)).map((path) => ({ type: "localImage", path }))
824
1228
  ] });
825
- nativeTurn = isRecord2(started.turn) ? text(started.turn.id) : "";
1229
+ nativeTurn = isRecord(started.turn) ? text(started.turn.id) : "";
826
1230
  if (!nativeTurn) throw new Error("Codex started no turn");
827
1231
  if (turn.signal.aborted) interrupt();
828
1232
  conclude();
@@ -1010,117 +1414,10 @@ function connectorDriver() {
1010
1414
  }
1011
1415
  __name(connectorDriver, "connectorDriver");
1012
1416
 
1013
- // src/runner/home.ts
1014
- import { randomBytes } from "node:crypto";
1015
- import { closeSync, constants as constants2, fstatSync, fsyncSync, linkSync, mkdirSync, openSync, readSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
1016
- import { lstat, mkdir, realpath as realpath2 } from "node:fs/promises";
1017
- import { dirname as dirname3 } from "node:path";
1018
- var runnerLockName = /* @__PURE__ */ __name((environmentId, agentId) => `runner-${environmentId}-${agentId}`.replace(/[^\w.-]/g, "_"), "runnerLockName");
1019
- function writeDurable(file, text3) {
1020
- const directory = dirname3(file);
1021
- const created = mkdirSync(directory, { recursive: true, mode: 448 });
1022
- const temporary = `${file}.${process.pid}.tmp`;
1023
- const fd = openSync(temporary, "w", 384);
1024
- try {
1025
- writeFileSync(fd, text3);
1026
- fsyncSync(fd);
1027
- } finally {
1028
- closeSync(fd);
1029
- }
1030
- renameSync(temporary, file);
1031
- if (process.platform === "win32") return;
1032
- const last = created ? dirname3(created) : directory;
1033
- for (let path = directory; ; path = dirname3(path)) {
1034
- const dir = openSync(path, "r");
1035
- try {
1036
- fsyncSync(dir);
1037
- } finally {
1038
- closeSync(dir);
1039
- }
1040
- if (path === last) break;
1041
- }
1042
- }
1043
- __name(writeDurable, "writeDurable");
1044
- function privateKey(file, bytes = 32) {
1045
- const refuse = /* @__PURE__ */ __name(() => {
1046
- throw new Error(`Refusing to start: ${file} must be a regular file of ${bytes} bytes, owned by this user and private to it (0600).`);
1047
- }, "refuse");
1048
- const read = /* @__PURE__ */ __name(() => {
1049
- let fd2;
1050
- try {
1051
- fd2 = openSync(file, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
1052
- } catch (error) {
1053
- return error.code === "ENOENT" ? void 0 : refuse();
1054
- }
1055
- try {
1056
- const info = fstatSync(fd2);
1057
- const uid = process.getuid?.();
1058
- if (!info.isFile() || info.size !== bytes || uid !== void 0 && (info.uid !== uid || (info.mode & 63) !== 0)) refuse();
1059
- const key = Buffer.alloc(bytes);
1060
- if (readSync(fd2, key, 0, bytes, 0) !== bytes) refuse();
1061
- return key;
1062
- } finally {
1063
- closeSync(fd2);
1064
- }
1065
- }, "read");
1066
- const existing = read();
1067
- if (existing) return existing;
1068
- const directory = dirname3(file);
1069
- mkdirSync(directory, { recursive: true, mode: 448 });
1070
- const temporary = `${file}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
1071
- const fd = openSync(temporary, "wx", 384);
1072
- try {
1073
- writeSync(fd, randomBytes(bytes));
1074
- fsyncSync(fd);
1075
- } finally {
1076
- closeSync(fd);
1077
- }
1078
- try {
1079
- linkSync(temporary, file);
1080
- } catch (error) {
1081
- if (error.code !== "EEXIST") throw error;
1082
- } finally {
1083
- rmSync(temporary, { force: true });
1084
- }
1085
- const dir = openSync(directory, "r");
1086
- try {
1087
- fsyncSync(dir);
1088
- } finally {
1089
- closeSync(dir);
1090
- }
1091
- return read() ?? refuse();
1092
- }
1093
- __name(privateKey, "privateKey");
1094
- async function assertPrivateHome(home) {
1095
- const uid = process.getuid?.();
1096
- if (uid === void 0) return;
1097
- const fail2 = /* @__PURE__ */ __name((path, problem) => {
1098
- 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.`);
1099
- }, "fail");
1100
- await mkdir(home, { recursive: true, mode: 448 }).catch(() => void 0);
1101
- let resolved;
1102
- try {
1103
- resolved = await realpath2(home);
1104
- } catch {
1105
- return fail2(home, "does not exist");
1106
- }
1107
- if (resolved !== home) fail2(home, `is not canonical (it resolves to ${resolved})`);
1108
- const stat3 = await lstat(home);
1109
- if (!stat3.isDirectory() || stat3.uid !== uid) fail2(home, "is not a directory owned by this user");
1110
- if (stat3.mode & 63) fail2(home, `is accessible to other users (mode ${(stat3.mode & 511).toString(8)})`);
1111
- for (let path = dirname3(home); ; path = dirname3(path)) {
1112
- const ancestor = await lstat(path);
1113
- if (ancestor.uid !== uid && ancestor.uid !== 0) fail2(path, "is owned by another user");
1114
- if (ancestor.mode & 18) fail2(path, "is writable by other users");
1115
- if (path === dirname3(path)) return;
1116
- }
1117
- }
1118
- __name(assertPrivateHome, "assertPrivateHome");
1119
-
1120
1417
  // src/runner/errands.ts
1121
1418
  import { randomBytes as randomBytes2 } from "node:crypto";
1122
- import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, lstatSync, mkdirSync as mkdirSync2, openSync as openSync2, readdirSync, readFileSync, unlinkSync, watch } from "node:fs";
1123
- import { join } from "node:path";
1419
+ import { closeSync, constants as constants2, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync as readFileSync2, unlinkSync, watch } from "node:fs";
1420
+ import { join as join2 } from "node:path";
1124
1421
  var ERRAND_KINDS = ["tell", "post", "errand", "cancel"];
1125
1422
  var ERRAND_TEXT_MAX_BYTES = MLS_DEV_TEXT_MAX_BYTES;
1126
1423
  var ERRAND_WHY_MAX_BYTES = 4 * 1024;
@@ -1138,7 +1435,7 @@ function errandsDirectory(home, environmentId, agentId) {
1138
1435
  for (const segment of [environmentId, agentId]) {
1139
1436
  if (!/^[\w.:-]{1,200}$/.test(segment) || /^\.+$/.test(segment)) throw new Error(`Not an id: ${JSON.stringify(segment)}`);
1140
1437
  }
1141
- return join(home, "runner", environmentId, agentId, "errands");
1438
+ return join2(home, "runner", environmentId, agentId, "errands");
1142
1439
  }
1143
1440
  __name(errandsDirectory, "errandsDirectory");
1144
1441
  function custody(stat3, kind) {
@@ -1154,8 +1451,8 @@ function custody(stat3, kind) {
1154
1451
  }
1155
1452
  __name(custody, "custody");
1156
1453
  function errandInbox(directory) {
1157
- const inbox = join(directory, "inbox");
1158
- mkdirSync2(inbox, { recursive: true, mode: 448 });
1454
+ const inbox = join2(directory, "inbox");
1455
+ mkdirSync(inbox, { recursive: true, mode: 448 });
1159
1456
  const problem = custody(lstatSync(inbox), "directory");
1160
1457
  if (problem) throw new Error(`The errand inbox ${inbox} ${problem}`);
1161
1458
  return inbox;
@@ -1188,16 +1485,16 @@ function queueErrand(directory, input) {
1188
1485
  const command = { version: 1, id: randomBytes2(16).toString("hex"), ...fields, createdAt: now.toISOString() };
1189
1486
  const problem = commandProblem(command, command.id);
1190
1487
  if (problem) throw new Error(`Not a valid ${input.kind}: ${problem}.`);
1191
- writeDurable(join(errandInbox(directory), `${now.getTime()}-${command.id}.json`), JSON.stringify(command));
1488
+ writeDurable(join2(errandInbox(directory), `${now.getTime()}-${command.id}.json`), JSON.stringify(command));
1192
1489
  return command;
1193
1490
  }
1194
1491
  __name(queueErrand, "queueErrand");
1195
1492
  function errandJournal(directory) {
1196
- const folder = join(directory, "journal");
1197
- const file = /* @__PURE__ */ __name((id) => join(folder, `${id}.json`), "file");
1493
+ const folder = join2(directory, "journal");
1494
+ const file = /* @__PURE__ */ __name((id) => join2(folder, `${id}.json`), "file");
1198
1495
  const read = /* @__PURE__ */ __name((path) => {
1199
1496
  try {
1200
- return JSON.parse(readFileSync(path, "utf8"));
1497
+ return JSON.parse(readFileSync2(path, "utf8"));
1201
1498
  } catch {
1202
1499
  return void 0;
1203
1500
  }
@@ -1219,7 +1516,7 @@ function errandJournal(directory) {
1219
1516
  names = readdirSync(folder);
1220
1517
  } catch {
1221
1518
  }
1222
- return names.filter((name) => /^[0-9a-f]{32}\.json$/.test(name)).flatMap((name) => read(join(folder, name)) ?? []);
1519
+ return names.filter((name) => /^[0-9a-f]{32}\.json$/.test(name)).flatMap((name) => read(join2(folder, name)) ?? []);
1223
1520
  }
1224
1521
  };
1225
1522
  }
@@ -1244,24 +1541,24 @@ __name(awaitErrand, "awaitErrand");
1244
1541
  function readCommand(path, id) {
1245
1542
  let fd;
1246
1543
  try {
1247
- fd = openSync2(path, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
1544
+ fd = openSync(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
1248
1545
  } catch (error) {
1249
1546
  const code = error.code;
1250
1547
  return { problem: code === "ELOOP" || code === "EMLINK" ? "is a symbolic link" : `cannot be opened (${code})` };
1251
1548
  }
1252
1549
  try {
1253
- const problem = custody(fstatSync2(fd), "file");
1550
+ const problem = custody(fstatSync(fd), "file");
1254
1551
  if (problem) return { problem };
1255
1552
  let value;
1256
1553
  try {
1257
- value = JSON.parse(readFileSync(fd, "utf8"));
1554
+ value = JSON.parse(readFileSync2(fd, "utf8"));
1258
1555
  } catch {
1259
1556
  return { problem: "is not JSON" };
1260
1557
  }
1261
1558
  const invalid2 = commandProblem(value, id);
1262
1559
  return invalid2 ? { problem: `is not a valid command: ${invalid2}` } : value;
1263
1560
  } finally {
1264
- closeSync2(fd);
1561
+ closeSync(fd);
1265
1562
  }
1266
1563
  }
1267
1564
  __name(readCommand, "readCommand");
@@ -1286,7 +1583,7 @@ function watchErrandInbox(directory, handler, options) {
1286
1583
  ${name}`, `Errands: ${name} in the inbox is not a command file; it is left alone.`);
1287
1584
  continue;
1288
1585
  }
1289
- const path = join(inbox, name);
1586
+ const path = join2(inbox, name);
1290
1587
  try {
1291
1588
  if (handler.taken(id) || await handler.take(id, readCommand(path, id))) unlinkSync(path);
1292
1589
  } catch (error) {
@@ -1336,8 +1633,8 @@ __name(watchErrandInbox, "watchErrandInbox");
1336
1633
 
1337
1634
  // src/runner/mls.ts
1338
1635
  import { createHash as createHash4, createHmac } from "node:crypto";
1339
- import { constants as constants4 } from "node:fs";
1340
- import { open as open2, realpath as realpath3 } from "node:fs/promises";
1636
+ import { constants as constants3 } from "node:fs";
1637
+ import { open as open2, realpath as realpath2 } from "node:fs/promises";
1341
1638
  import { isAbsolute, resolve } from "node:path";
1342
1639
  var fail = /* @__PURE__ */ __name(() => {
1343
1640
  throw new Error("MLS_AUTHORITY_REQUIRED");
@@ -1402,7 +1699,7 @@ function createMlsIds(key) {
1402
1699
  }
1403
1700
  __name(createMlsIds, "createMlsIds");
1404
1701
  function requireMlsSupport(input) {
1405
- if (!isMlsEnvironmentId(input.environmentId) || input.agent.outboundPolicy === "closed") fail();
1702
+ if (!isMlsEnvironmentId(input.environmentId)) fail();
1406
1703
  }
1407
1704
  __name(requireMlsSupport, "requireMlsSupport");
1408
1705
  function mentionsAgent(text3, names) {
@@ -1453,8 +1750,8 @@ function object(value, required, optional = []) {
1453
1750
  __name(object, "object");
1454
1751
  async function loadMlsDevConfiguration(filePath, input) {
1455
1752
  if (!isMlsEnvironmentId(input.environmentId)) invalid();
1456
- if (typeof filePath !== "string" || !isAbsolute(filePath) || filePath.includes("\0") || resolve(filePath) !== filePath || await realpath3(filePath).catch(() => "") !== filePath) invalid();
1457
- const file = await open2(filePath, constants4.O_RDONLY | constants4.O_NOFOLLOW).catch(() => invalid());
1753
+ if (typeof filePath !== "string" || !isAbsolute(filePath) || filePath.includes("\0") || resolve(filePath) !== filePath || await realpath2(filePath).catch(() => "") !== filePath) invalid();
1754
+ const file = await open2(filePath, constants3.O_RDONLY | constants3.O_NOFOLLOW).catch(() => invalid());
1458
1755
  let value;
1459
1756
  try {
1460
1757
  const before = await file.stat();
@@ -1462,7 +1759,7 @@ async function loadMlsDevConfiguration(filePath, input) {
1462
1759
  const bytes = Buffer.alloc(MAX_CONFIGURATION_BYTES + 1);
1463
1760
  const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
1464
1761
  const after = await file.stat();
1465
- if (bytesRead !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || await realpath3(filePath) !== filePath) invalid();
1762
+ if (bytesRead !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || await realpath2(filePath) !== filePath) invalid();
1466
1763
  value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, bytesRead)));
1467
1764
  } catch {
1468
1765
  invalid();
@@ -1582,14 +1879,14 @@ __name(runMlsDevSetup, "runMlsDevSetup");
1582
1879
 
1583
1880
  // src/runner/mls-bundled.ts
1584
1881
  import { createHash as createHash5 } from "node:crypto";
1585
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1586
- import { dirname as dirname4, join as join2 } from "node:path";
1882
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
1883
+ import { dirname as dirname4, join as join3 } from "node:path";
1587
1884
  import { fileURLToPath } from "node:url";
1588
1885
 
1589
1886
  // mls-custody.json
1590
1887
  var mls_custody_default = {
1591
1888
  schemaVersion: 1,
1592
- release: "mls-custody-cadb937436ce",
1889
+ release: "mls-custody-5ce777037493",
1593
1890
  protocolVersion: 1,
1594
1891
  rustVersion: "rustc 1.91.0 (f8297e351 2025-10-28)",
1595
1892
  sourceHashes: {
@@ -1601,15 +1898,15 @@ var mls_custody_default = {
1601
1898
  "mls/native/src/portable/json.rs": "d4e68556a051ffe9008686036597b1fd8239ae66fad67bd848c3f0e16bf8ccaf",
1602
1899
  "mls/native/src/portable/vault.rs": "14b9ff1e10ec4058e39a46511a489c7e82a9836e4175a768ac5d4b69c1dba8e5",
1603
1900
  "mls/native/src/portable/tests.rs": "7eadba2a49ed044470bb567d9b93fba44b64dc117b1da50a22187cff3dccb64e",
1604
- "mls/native/portable/build.mjs": "84c422a8a8aa6a7ec112a358bdeef080201aa4ea9a91f6d5726b05f46fa2a10c",
1901
+ "mls/native/portable/build.mjs": "ec81bfe27238b3117c4e855fa58b323e18a935ad7aa8ac18807376fb0bddc99b",
1605
1902
  "mls/core/open.rs": "8cdc03882f3b84dca72ab06afd437f162de01a0fe21327b019f17867dd59f5d0",
1606
1903
  "mls/core/protocol.rs": "7f3e021d6e4f1cc130b5dd3c98e0b0fa23edf813c72802c3fa3f8fb9e431c947",
1607
1904
  "mls/core/fault_storage.rs": "fdd6672a21f6ffee657142755637824541a5090b4f8a5fb4902f482556fb2793",
1608
1905
  "mls/core/mls_environment.rs": "390d11ee82ef8b108a7fffc7bb193ac7f3e470aa14e9643d029007aaaa29c4e4",
1609
- "mls/core/dev_identity.rs": "97df6bdb9daab66f0e3d4ca3b49bf9954fa06ac032c927d9ae6c83509b0166bd",
1610
- "mls/core/dev_key_package.rs": "3e7011fe8ab3044d80d84d04944068dbcf683141fcaba8a7a00bd2cfd103de72",
1906
+ "mls/core/dev_identity.rs": "df7282f0daa8babb3a9b9f397ad65d621784f9f54f5ca7a45fb3c8e82d58bbb1",
1907
+ "mls/core/dev_key_package.rs": "e8956f8dd4e365c22ac2356fb520759cdb613e344e1d44510ceee54b1cf9a418",
1611
1908
  "mls/core/dev_application.rs": "fafe245ace38e384da265bab2c12fef6b77c86b2c20477d00563ab891a54f94b",
1612
- "mls/core/dev_group.rs": "1d519a5d15394bbc163aef00e0ddacb9bb87631beaf8a13e52c76d628749152b",
1909
+ "mls/core/dev_group.rs": "d43f9ecdae79effdb9f177e76108487f81618dfc5b72b63618c41eff5896b2e0",
1613
1910
  "packages/test-fixtures/src/mlsDevEnrollmentProofV1.json": "50d7896e0a3a25b420412e84484e64f1e49480a3f298ee2d125f2f9188382810",
1614
1911
  "packages/test-fixtures/src/mlsDevCredentialV1.json": "24cd23af794b8a6e35caf050995020c6584b806de060969db4536f44b1188780",
1615
1912
  "packages/test-fixtures/src/mlsDevCredentialReconciliationV1.json": "bd4bbb3a2e5dc7921867a2e5936d1da77d536740b4360012c1f741f9caed2826",
@@ -1620,10 +1917,10 @@ var mls_custody_default = {
1620
1917
  platforms: {
1621
1918
  "darwin-arm64": {
1622
1919
  target: "aarch64-apple-darwin",
1623
- sha256: "918ae350f1d7bd339c4eee548f6668943217f5fa66022bb93f41a02992dd296c",
1920
+ sha256: "b09c82209cc7884c4521a310c727a8faaf387e9d0603443b1c1d9a55d87b7103",
1624
1921
  bytes: 2932976,
1625
- unitTestsPassed: 95,
1626
- commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1922
+ unitTestsPassed: 96,
1923
+ commit: "b9d1a4008749d1f144fd8db6ca4066de20422184",
1627
1924
  builder: {
1628
1925
  macos: "26.3.1",
1629
1926
  sdk: "26.4",
@@ -1632,10 +1929,10 @@ var mls_custody_default = {
1632
1929
  },
1633
1930
  "linux-x64": {
1634
1931
  target: "x86_64-unknown-linux-gnu",
1635
- sha256: "1c5a12d0213c53bdf59b5f36a7390a4b4c17f8a53c95e760ad8cb4905297d256",
1636
- bytes: 3389816,
1637
- unitTestsPassed: 95,
1638
- commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1932
+ sha256: "0b20495694534d9792b6920f1e3f375a2182b237a44cb2d384080dd17e7f83bb",
1933
+ bytes: 3389880,
1934
+ unitTestsPassed: 96,
1935
+ commit: "da679f4c60e2271c20b1f1958d0e5f6bfce6ef2d",
1639
1936
  builder: {
1640
1937
  image: "quay.io/pypa/manylinux_2_28_x86_64@sha256:e0adf18521ad35192df733a6c62d35df7801957d24e0470238aacecfab2188a3",
1641
1938
  dockerfileSha256: "fe8d4895b8bd304954441fd27c6273c8c00baa10a1ad912d3c3851b193b8ad75"
@@ -1643,15 +1940,28 @@ var mls_custody_default = {
1643
1940
  },
1644
1941
  "linux-arm64": {
1645
1942
  target: "aarch64-unknown-linux-gnu",
1646
- sha256: "371c5754cf0c437f561e8c49b3ea654c037311b951e11b1c5a29fb6b91656619",
1943
+ sha256: "649d90ba74a42b34025292bd942068fc08979e35f66c9d6ad358469f5022fac7",
1647
1944
  bytes: 3234232,
1648
- unitTestsPassed: 95,
1649
- commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1945
+ unitTestsPassed: 96,
1946
+ commit: "b9d1a4008749d1f144fd8db6ca4066de20422184",
1650
1947
  builder: {
1651
1948
  image: "quay.io/pypa/manylinux_2_28_aarch64@sha256:a3e02308c822fb6ce0d12248b0c941817ba5e1a1bc9e7bb5929f29c50eaa40a9",
1652
1949
  dockerfileSha256: "fe8d4895b8bd304954441fd27c6273c8c00baa10a1ad912d3c3851b193b8ad75"
1653
1950
  }
1654
1951
  }
1952
+ },
1953
+ wasm: {
1954
+ release: "mls-wasm-ddfdcc6039d7",
1955
+ commit: "ba57ec95af11e8b560fd7d71431c0ec14542d71e",
1956
+ rustVersion: "rustc 1.91.0 (f8297e351 2025-10-28)",
1957
+ wasm: {
1958
+ sha256: "137454981d2b6236dffa3dae0a7253e5f6d30578bce2cf50476adc8ca3d9aa02",
1959
+ bytes: 1794989
1960
+ },
1961
+ glue: {
1962
+ sha256: "744eb2267560f22aa54540fe9aa988588a7c27641610aae729b2ed75232e2a24",
1963
+ bytes: 13579
1964
+ }
1655
1965
  }
1656
1966
  };
1657
1967
 
@@ -1687,40 +1997,77 @@ var MLS_ENVIRONMENT_PINS = {
1687
1997
  };
1688
1998
  function packageRoot() {
1689
1999
  for (let directory = dirname4(fileURLToPath(import.meta.url)); ; directory = dirname4(directory)) {
1690
- const manifest = join2(directory, "package.json");
1691
- if (existsSync(manifest) && JSON.parse(readFileSync2(manifest, "utf8")).name === "@canonmsg/agent-sdk") return directory;
2000
+ const manifest = join3(directory, "package.json");
2001
+ if (existsSync2(manifest) && JSON.parse(readFileSync3(manifest, "utf8")).name === "@canonmsg/agent-sdk") return directory;
1692
2002
  if (dirname4(directory) === directory) return void 0;
1693
2003
  }
1694
2004
  }
1695
2005
  __name(packageRoot, "packageRoot");
2006
+ function helperRunsOnLibc() {
2007
+ if (process.platform !== "linux") return true;
2008
+ const report = process.report;
2009
+ const { excludeNetwork } = report;
2010
+ report.excludeNetwork = true;
2011
+ try {
2012
+ const version = report.getReport().header?.glibcVersionRuntime;
2013
+ const [major = 0, minor = 0] = (version ?? "").split(".").map(Number);
2014
+ return major > 2 || major === 2 && minor >= 28;
2015
+ } finally {
2016
+ report.excludeNetwork = excludeNetwork;
2017
+ }
2018
+ }
2019
+ __name(helperRunsOnLibc, "helperRunsOnLibc");
1696
2020
  function bundledMlsCustodyHelper() {
1697
2021
  const platform = `${process.platform}-${process.arch}`;
1698
2022
  const pin = mls_custody_default.platforms[platform];
1699
- const root = pin && packageRoot();
1700
- const path = root && join2(root, "dist", "mls", platform, "canon-mls-custody");
1701
- if (!pin || !path || !existsSync(path)) return void 0;
1702
- if (createHash5("sha256").update(readFileSync2(path)).digest("hex") !== pin.sha256) {
2023
+ const root = pin && helperRunsOnLibc() && packageRoot();
2024
+ const path = root && join3(root, "dist", "mls", platform, "canon-mls-custody");
2025
+ if (!pin || !path || !existsSync2(path)) return void 0;
2026
+ if (createHash5("sha256").update(readFileSync3(path)).digest("hex") !== pin.sha256) {
1703
2027
  throw new Error(`${path} is not the custody helper this package pins; reinstall @canonmsg/agent-sdk.`);
1704
2028
  }
1705
2029
  return { path, sha256: pin.sha256 };
1706
2030
  }
1707
2031
  __name(bundledMlsCustodyHelper, "bundledMlsCustodyHelper");
2032
+ function bundledMlsWasmCore() {
2033
+ const root = packageRoot();
2034
+ if (!root) return void 0;
2035
+ const directory = join3(root, "dist", "mls", "wasm");
2036
+ const hostPath = join3(root, "dist", "mls-wasm-host.js"), wasmPath = join3(directory, "canon_mls_wasm_bg.wasm"), gluePath = join3(directory, "canon_mls_wasm.js");
2037
+ if (![hostPath, wasmPath, gluePath].every((path) => existsSync2(path))) return void 0;
2038
+ for (const [path, pin] of [[wasmPath, mls_custody_default.wasm.wasm.sha256], [gluePath, mls_custody_default.wasm.glue.sha256]]) {
2039
+ 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.`);
2040
+ }
2041
+ return { hostPath, wasmPath, wasmSha256: mls_custody_default.wasm.wasm.sha256, gluePath, glueSha256: mls_custody_default.wasm.glue.sha256 };
2042
+ }
2043
+ __name(bundledMlsWasmCore, "bundledMlsWasmCore");
1708
2044
 
1709
2045
  // src/runner/mls-managed.ts
1710
2046
  import { createHash as createHash6 } from "node:crypto";
1711
- import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2 } from "node:fs";
1712
- import { dirname as dirname5, join as join3 } from "node:path";
2047
+ import { chmodSync, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, renameSync } from "node:fs";
2048
+ import { dirname as dirname5, join as join4 } from "node:path";
1713
2049
  var sha256 = /* @__PURE__ */ __name((bytes) => createHash6("sha256").update(bytes).digest("hex"), "sha256");
2050
+ function custodyFor(home, environmentId) {
2051
+ const root = join4(home, "mls", environmentId);
2052
+ const keptOnCore = existsSync3(root) && readdirSync2(root).some((name) => existsSync3(join4(root, name, "wrapping.key")));
2053
+ const kept = keptOnCore ? bundledMlsWasmCore() : void 0;
2054
+ if (kept) return { wasm: kept };
2055
+ const helper = bundledMlsCustodyHelper();
2056
+ if (helper) return { helper };
2057
+ const wasm = bundledMlsWasmCore();
2058
+ return wasm ? { wasm } : {};
2059
+ }
2060
+ __name(custodyFor, "custodyFor");
1714
2061
  function defaultMlsDevice(home, environmentId) {
1715
2062
  if (!isMlsEnvironmentId(environmentId)) return void 0;
1716
- const helper = bundledMlsCustodyHelper();
1717
- return helper ? managedMlsDevice({ home, environmentId, helper }) : void 0;
2063
+ const custody2 = custodyFor(home, environmentId);
2064
+ return custody2.helper || custody2.wasm ? managedMlsDevice({ home, environmentId, ...custody2 }) : void 0;
1718
2065
  }
1719
2066
  __name(defaultMlsDevice, "defaultMlsDevice");
1720
2067
  function managedMlsDevice(options) {
1721
2068
  const { environmentId } = options;
1722
- const helper = options.helper ?? bundledMlsCustodyHelper();
1723
- if (!helper) {
2069
+ const { helper, wasm } = options.helper ? { helper: options.helper, wasm: void 0 } : options.wasm ? options : custodyFor(options.home, environmentId);
2070
+ if (!helper && !wasm) {
1724
2071
  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).`);
1725
2072
  }
1726
2073
  const pins = options.pins ?? MLS_ENVIRONMENT_PINS[environmentId];
@@ -1731,24 +2078,27 @@ function managedMlsDevice(options) {
1731
2078
  issuerKeys: pins.issuerKeys,
1732
2079
  receiptKeys: pins.receiptKeys
1733
2080
  });
1734
- const root = join3(options.home, "mls", environmentId);
1735
- const directory = /* @__PURE__ */ __name((context) => join3(root, sha256(Buffer.from(JSON.stringify([
2081
+ const root = join4(options.home, "mls", environmentId);
2082
+ const directory = /* @__PURE__ */ __name((context) => join4(root, sha256(Buffer.from(JSON.stringify([
1736
2083
  context.installationId,
1737
2084
  context.principal.type,
1738
2085
  context.principal.id,
1739
2086
  "ownershipVersion" in context.principal ? context.principal.ownershipVersion : null
1740
2087
  ]))).slice(0, 32)), "directory");
1741
- const vault = /* @__PURE__ */ __name((context) => join3(directory(context), "vault", "vault.bin"), "vault");
2088
+ const vault = /* @__PURE__ */ __name((context) => join4(directory(context), "vault", "vault.bin"), "vault");
2089
+ const wrappingKey = /* @__PURE__ */ __name((context) => join4(directory(context), "wrapping.key"), "wrappingKey");
2090
+ const made = /* @__PURE__ */ __name((context) => wasm ? wrappingKey(context) : vault(context), "made");
2091
+ 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");
1742
2092
  let executable;
1743
2093
  function install() {
1744
- const target = join3(root, "bin", helper.sha256, "canon-mls-custody");
1745
- if (existsSync2(target) && sha256(readFileSync3(target)) === helper.sha256) return target;
1746
- mkdirSync3(dirname5(target), { recursive: true, mode: 448 });
2094
+ const target = join4(root, "bin", helper.sha256, "canon-mls-custody");
2095
+ if (existsSync3(target) && sha256(readFileSync4(target)) === helper.sha256) return target;
2096
+ mkdirSync2(dirname5(target), { recursive: true, mode: 448 });
1747
2097
  const temporary = `${target}.${process.pid}.tmp`;
1748
2098
  copyFileSync(helper.path, temporary);
1749
2099
  chmodSync(temporary, 448);
1750
- if (sha256(readFileSync3(temporary)) !== helper.sha256) throw new Error(`${helper.path} is not the pinned custody helper`);
1751
- renameSync2(temporary, target);
2100
+ if (sha256(readFileSync4(temporary)) !== helper.sha256) throw new Error(`${helper.path} is not the pinned custody helper`);
2101
+ renameSync(temporary, target);
1752
2102
  return target;
1753
2103
  }
1754
2104
  __name(install, "install");
@@ -1758,9 +2108,24 @@ function managedMlsDevice(options) {
1758
2108
  let found = devices.get(key);
1759
2109
  if (!found) {
1760
2110
  found = (async () => {
1761
- if (!provisioning && !existsSync2(vault(context))) throw new NodeMlsDevCustodyError();
1762
- mkdirSync3(dirname5(vault(context)), { recursive: true, mode: 448 });
1763
- const unlock = join3(key, "unlock");
2111
+ if (!provisioning && !existsSync3(made(context))) throw new NodeMlsDevCustodyError();
2112
+ mkdirSync2(dirname5(made(context)), { recursive: true, mode: 448 });
2113
+ if (wasm) {
2114
+ if (process.platform === "win32") {
2115
+ if (provisioning) secureWindowsDirectory(key);
2116
+ else assertPrivateWindowsFile(wrappingKey(context));
2117
+ }
2118
+ const { revision: _revision, ...identity } = context;
2119
+ return wasmMlsCustody({
2120
+ core: wasm,
2121
+ context: identity,
2122
+ keyPath: wrappingKey(context),
2123
+ issuerKeys: configuration.issuerKeys,
2124
+ receiptKeys: configuration.receiptKeys,
2125
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
2126
+ });
2127
+ }
2128
+ const unlock = join4(key, "unlock");
1764
2129
  if (provisioning) privateKey(unlock, 32);
1765
2130
  const text3 = `${JSON.stringify({
1766
2131
  version: 1,
@@ -1776,8 +2141,8 @@ function managedMlsDevice(options) {
1776
2141
  receiptKeys: configuration.receiptKeys
1777
2142
  }, null, 2)}
1778
2143
  `;
1779
- const file = join3(key, "native.json");
1780
- if (!existsSync2(file) || readFileSync3(file, "utf8") !== text3) writeDurable(file, text3);
2144
+ const file = join4(key, "native.json");
2145
+ if (!existsSync3(file) || readFileSync4(file, "utf8") !== text3) writeDurable(file, text3);
1781
2146
  return createPortableNodeMlsDevCustody({
1782
2147
  executablePath: executable ??= install(),
1783
2148
  executableSha256: helper.sha256,
@@ -1803,8 +2168,18 @@ function managedMlsDevice(options) {
1803
2168
  return {
1804
2169
  configuration,
1805
2170
  custody: custody2,
1806
- provision: /* @__PURE__ */ __name((context) => device(context, true).then((open4) => open4.provision(context)), "provision"),
1807
- provisioned: /* @__PURE__ */ __name((context) => existsSync2(vault(context)), "provisioned"),
2171
+ async provision(context) {
2172
+ await (await device(context, true)).provision(context);
2173
+ if (wasm && process.platform === "win32") {
2174
+ restrictWindowsFile(wrappingKey(context));
2175
+ assertPrivateWindowsFile(wrappingKey(context));
2176
+ }
2177
+ },
2178
+ provisioned(context) {
2179
+ if (existsSync3(made(context))) return true;
2180
+ if (existsSync3(wasm ? vault(context) : wrappingKey(context))) throw stranded(context);
2181
+ return false;
2182
+ },
1808
2183
  async close() {
1809
2184
  await Promise.allSettled([...devices.values()].map((open4) => open4.then((custody3) => custody3.close())));
1810
2185
  }
@@ -1814,10 +2189,10 @@ __name(managedMlsDevice, "managedMlsDevice");
1814
2189
 
1815
2190
  // src/runner/relay.ts
1816
2191
  import { createHmac as createHmac2, timingSafeEqual } from "node:crypto";
1817
- 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";
1818
- import { lstat as lstat2, open as open3, realpath as realpath4 } from "node:fs/promises";
2192
+ import { closeSync as closeSync2, constants as constants4, fsyncSync, mkdirSync as mkdirSync3, openSync as openSync2, renameSync as renameSync2, writeSync } from "node:fs";
2193
+ import { lstat, open as open3, realpath as realpath3 } from "node:fs/promises";
1819
2194
  import { createServer } from "node:http";
1820
- import { isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
2195
+ import { isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "node:path";
1821
2196
  var TOLERANCE_S = 300;
1822
2197
  function relaySecretKey(secret) {
1823
2198
  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");
@@ -1950,8 +2325,8 @@ async function readPrivateFile(path, maximum, what) {
1950
2325
  const fail2 = /* @__PURE__ */ __name(() => {
1951
2326
  throw new Error(`${what} must be a private file (0600, owned by this user, not a link): ${path}`);
1952
2327
  }, "fail");
1953
- if (typeof path !== "string" || !isAbsolute2(path) || path.includes("\0") || resolve2(path) !== path || await realpath4(path).catch(() => "") !== path) fail2();
1954
- const file = await open3(path, constants5.O_RDONLY | constants5.O_NOFOLLOW).catch(() => fail2());
2328
+ if (typeof path !== "string" || !isAbsolute2(path) || path.includes("\0") || resolve2(path) !== path || await realpath3(path).catch(() => "") !== path) fail2();
2329
+ const file = await open3(path, constants4.O_RDONLY | constants4.O_NOFOLLOW).catch(() => fail2());
1955
2330
  try {
1956
2331
  const info = await file.stat();
1957
2332
  if (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid?.() || (info.mode & 63) !== 0 || info.size < 1 || info.size > maximum) fail2();
@@ -1988,11 +2363,11 @@ async function loadRelayConfiguration(path, input) {
1988
2363
  return { environmentId: value.environmentId, agentId: value.agentId, listen: { host: listen.host, port: listen.port }, secret };
1989
2364
  }
1990
2365
  __name(loadRelayConfiguration, "loadRelayConfiguration");
1991
- var relayDirectory = /* @__PURE__ */ __name((home) => join4(home, "relay"), "relayDirectory");
1992
- var credentialFile = /* @__PURE__ */ __name((home) => join4(relayDirectory(home), "credential.json"), "credentialFile");
2366
+ var relayDirectory = /* @__PURE__ */ __name((home) => join5(home, "relay"), "relayDirectory");
2367
+ var credentialFile = /* @__PURE__ */ __name((home) => join5(relayDirectory(home), "credential.json"), "credentialFile");
1993
2368
  async function readRelayCredential(home, relay) {
1994
2369
  const path = credentialFile(home);
1995
- if (!await lstat2(path).then(() => true, () => false)) return void 0;
2370
+ if (!await lstat(path).then(() => true, () => false)) return void 0;
1996
2371
  let value;
1997
2372
  try {
1998
2373
  value = JSON.parse(await readPrivateFile(path, 4 * 1024, "The relay credential"));
@@ -2015,182 +2390,36 @@ function writeRelayCredential(home, relay, delivery) {
2015
2390
  generation: delivery.generation,
2016
2391
  credential: delivery.credential
2017
2392
  };
2018
- mkdirSync4(relayDirectory(home), { recursive: true, mode: 448 });
2393
+ mkdirSync3(relayDirectory(home), { recursive: true, mode: 448 });
2019
2394
  const file = credentialFile(home);
2020
2395
  const temporary = `${file}.${process.pid}.tmp`;
2021
- const fd = openSync3(temporary, "w", 384);
2396
+ const fd = openSync2(temporary, "w", 384);
2022
2397
  try {
2023
- writeSync2(fd, JSON.stringify(value));
2024
- fsyncSync2(fd);
2398
+ writeSync(fd, JSON.stringify(value));
2399
+ fsyncSync(fd);
2025
2400
  } finally {
2026
- closeSync3(fd);
2401
+ closeSync2(fd);
2027
2402
  }
2028
- renameSync3(temporary, file);
2403
+ renameSync2(temporary, file);
2029
2404
  return value;
2030
2405
  }
2031
2406
  __name(writeRelayCredential, "writeRelayCredential");
2032
2407
  function relayRunnerHome(home, installationId) {
2033
2408
  if (!CONNECTOR_RELAY_INSTALLATION_PATTERN.test(installationId)) throw new Error("Invalid relay installation");
2034
- const directory = join4(relayDirectory(home), installationId.slice(CONNECTOR_RELAY_INSTALLATION_PREFIX.length));
2035
- mkdirSync4(directory, { recursive: true, mode: 448 });
2409
+ const directory = join5(relayDirectory(home), installationId.slice(CONNECTOR_RELAY_INSTALLATION_PREFIX.length));
2410
+ mkdirSync3(directory, { recursive: true, mode: 448 });
2036
2411
  return directory;
2037
2412
  }
2038
2413
  __name(relayRunnerHome, "relayRunnerHome");
2039
2414
 
2040
2415
  // src/runner/runner.ts
2041
2416
  import { createHash as createHash8 } from "node:crypto";
2042
- import { readFileSync as readFileSync5, rmSync as rmSync3 } from "node:fs";
2043
- import { realpath as realpath5, stat as stat2 } from "node:fs/promises";
2044
- import { basename as basename3, isAbsolute as isAbsolute3, join as join6, sep } from "node:path";
2417
+ import { readFileSync as readFileSync6, rmSync as rmSync2 } from "node:fs";
2418
+ import { realpath as realpath4, stat as stat2 } from "node:fs/promises";
2419
+ import { basename as basename4, isAbsolute as isAbsolute3, join as join7, sep } from "node:path";
2045
2420
 
2046
2421
  // ../agent-tools/src/communication-tool.ts
2047
- var CANON_COMMUNICATE_TOOL_NAME = "communicate";
2048
2422
  var CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
2049
- var NON_EMPTY_ID = { type: "string", minLength: 1, maxLength: 160 };
2050
- var MESSAGE_TEXT = { type: "string", minLength: 1, maxLength: 4096 };
2051
- var OPTIONAL_CAPTION = { type: "string", maxLength: 4096 };
2052
- var DIRECT_SELECTION_SCHEMA = {
2053
- oneOf: [
2054
- {
2055
- type: "object",
2056
- required: ["mode"],
2057
- additionalProperties: false,
2058
- properties: { mode: { const: "latest_or_new" } }
2059
- },
2060
- {
2061
- type: "object",
2062
- required: ["mode"],
2063
- additionalProperties: false,
2064
- properties: { mode: { const: "new" } }
2065
- },
2066
- {
2067
- type: "object",
2068
- required: ["mode", "conversationId"],
2069
- additionalProperties: false,
2070
- properties: {
2071
- mode: { const: "specific" },
2072
- conversationId: NON_EMPTY_ID
2073
- }
2074
- }
2075
- ]
2076
- };
2077
- function canonCommunicateToolDefinition(name = CANON_COMMUNICATE_TOOL_NAME) {
2078
- return {
2079
- name,
2080
- description: "Find agents, list current conversations with their participants, message an explicit conversation, start_conversation with a person or agent (new by default), create_conversation with an optional title, manage_participants, leave, forward a message or share a contact. Check the current participants before selecting an existing conversation. Canon enforces admission and operator permissions.",
2081
- inputSchema: {
2082
- type: "object",
2083
- oneOf: [
2084
- { type: "object", required: ["action"], additionalProperties: false, properties: { action: { const: "list_conversations" }, limit: { type: "integer", minimum: 1, maximum: 100 } } },
2085
- { type: "object", required: ["action", "conversationId"], additionalProperties: false, properties: { action: { const: "leave_conversation" }, conversationId: NON_EMPTY_ID } },
2086
- {
2087
- type: "object",
2088
- required: ["action"],
2089
- additionalProperties: false,
2090
- properties: {
2091
- action: { const: "discover_agents" },
2092
- query: {
2093
- type: "string",
2094
- maxLength: MAX_DISCOVER_AGENTS_QUERY_LENGTH,
2095
- description: "Optional name or description prefix. Empty lists the directory."
2096
- },
2097
- limit: {
2098
- type: "integer",
2099
- minimum: 1,
2100
- maximum: CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
2101
- default: CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS
2102
- },
2103
- cursor: {
2104
- type: "string",
2105
- minLength: 1,
2106
- description: "Opaque cursor from a prior discovery action with the same query."
2107
- }
2108
- }
2109
- },
2110
- {
2111
- type: "object",
2112
- required: ["action", "conversationId", "text"],
2113
- additionalProperties: false,
2114
- properties: {
2115
- action: { const: "message_existing" },
2116
- conversationId: NON_EMPTY_ID,
2117
- text: MESSAGE_TEXT,
2118
- messageId: NON_EMPTY_ID
2119
- }
2120
- },
2121
- {
2122
- type: "object",
2123
- required: ["action", "principalId", "text"],
2124
- additionalProperties: false,
2125
- properties: {
2126
- action: { enum: ["start_conversation", "start_direct"] },
2127
- principalId: NON_EMPTY_ID,
2128
- text: MESSAGE_TEXT,
2129
- selection: DIRECT_SELECTION_SCHEMA,
2130
- messageId: NON_EMPTY_ID
2131
- }
2132
- },
2133
- {
2134
- type: "object",
2135
- required: ["action", "memberIds"],
2136
- additionalProperties: false,
2137
- properties: {
2138
- action: { enum: ["create_conversation", "create_group"] },
2139
- name: { type: "string", minLength: 1, maxLength: VERB_LIMITS.groupNameChars },
2140
- memberIds: {
2141
- type: "array",
2142
- minItems: 1,
2143
- maxItems: VERB_LIMITS.groupMembers - 1,
2144
- uniqueItems: true,
2145
- items: NON_EMPTY_ID
2146
- }
2147
- }
2148
- },
2149
- {
2150
- type: "object",
2151
- required: [
2152
- "action",
2153
- "sourceConversationId",
2154
- "targetConversationId",
2155
- "messageId"
2156
- ],
2157
- additionalProperties: false,
2158
- properties: {
2159
- action: { const: "forward_message" },
2160
- sourceConversationId: NON_EMPTY_ID,
2161
- targetConversationId: NON_EMPTY_ID,
2162
- messageId: NON_EMPTY_ID,
2163
- text: OPTIONAL_CAPTION
2164
- }
2165
- },
2166
- {
2167
- type: "object",
2168
- required: ["action", "conversationId", "contactUserId"],
2169
- additionalProperties: false,
2170
- properties: {
2171
- action: { const: "share_contact" },
2172
- conversationId: NON_EMPTY_ID,
2173
- contactUserId: NON_EMPTY_ID,
2174
- text: OPTIONAL_CAPTION,
2175
- messageId: NON_EMPTY_ID
2176
- }
2177
- },
2178
- {
2179
- type: "object",
2180
- required: ["action", "conversationId", "userId", "operation"],
2181
- additionalProperties: false,
2182
- properties: {
2183
- action: { enum: ["manage_participants", "manage_group_members"] },
2184
- conversationId: NON_EMPTY_ID,
2185
- userId: NON_EMPTY_ID,
2186
- operation: { enum: ["add", "remove"] }
2187
- }
2188
- }
2189
- ]
2190
- }
2191
- };
2192
- }
2193
- __name(canonCommunicateToolDefinition, "canonCommunicateToolDefinition");
2194
2423
  function isRecord3(value) {
2195
2424
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
2196
2425
  }
@@ -2411,124 +2640,41 @@ __name(executeCanonCommunicateTool, "executeCanonCommunicateTool");
2411
2640
 
2412
2641
  // src/runner/live.ts
2413
2642
  import { randomBytes as randomBytes3 } from "node:crypto";
2414
- var SNAPSHOT_GAP_MS = 250;
2415
2643
  var KEEPALIVE_MS = 3500;
2416
2644
  var PROGRESS_REFRESH_MS = 6e4;
2417
- var MAX_ROWS = 30;
2418
2645
  function createLivePublisher(options) {
2419
2646
  const { client, conversationId, enqueue } = options;
2420
- const presence = options.mode === "presence";
2421
2647
  const presenceId = `turn-${randomBytes3(16).toString("hex")}`;
2422
- const publicTurnId = /* @__PURE__ */ __name(() => presence && options.turnId() !== null ? presenceId : options.turnId(), "publicTurnId");
2648
+ const publicTurnId = /* @__PURE__ */ __name(() => options.turnId() !== null ? presenceId : null, "publicTurnId");
2423
2649
  const openedAt = Date.now();
2424
- const rows = /* @__PURE__ */ new Map();
2425
- let mode = options.mode;
2426
- let text3 = "";
2427
2650
  let waiting = false;
2428
2651
  let ended = false;
2429
- let sequence = 0;
2430
- let state;
2431
2652
  let stateAt = 0;
2432
- let snapshotAt = 0;
2433
- let timer;
2434
2653
  let keepalive;
2435
- const current = /* @__PURE__ */ __name(() => waiting ? "waiting_input" : presence ? "thinking" : [...rows.values()].some((row) => row.status === "running") ? "tool" : text3 ? "streaming" : "thinking", "current");
2436
- const typing = /* @__PURE__ */ __name((on) => enqueue(() => client.setTyping(conversationId, on, on ? "thinking" : void 0)), "typing");
2437
- const node = /* @__PURE__ */ __name((blank = false) => enqueue(async () => {
2438
- if (ended && !blank) return;
2439
- const turnId = options.turnId();
2440
- await client.setStreaming({
2441
- conversationId,
2442
- turnId,
2443
- messageId: turnId ?? "",
2444
- text: blank ? "" : text3,
2445
- status: blank ? "thinking" : current(),
2446
- blocks: blank ? [] : [...rows.values()]
2447
- });
2448
- }), "node");
2449
2654
  const publishState = /* @__PURE__ */ __name(() => {
2450
- const next = state = current();
2655
+ const state = waiting ? "waiting_input" : "thinking";
2451
2656
  stateAt = Date.now();
2452
- enqueue(() => client.setRuntimeTurn({
2453
- conversationId,
2454
- turnId: publicTurnId(),
2455
- state: next,
2456
- openedAt,
2457
- turnUpdatedAt: Date.now(),
2458
- ...presence ? {} : options.context()
2459
- }));
2657
+ enqueue(() => client.setRuntimeTurn({ conversationId, turnId: publicTurnId(), state, openedAt, turnUpdatedAt: Date.now() }));
2460
2658
  }, "publishState");
2461
- const progress = /* @__PURE__ */ __name(() => {
2462
- if (current() !== state || Date.now() - stateAt > PROGRESS_REFRESH_MS) publishState();
2463
- if (mode !== "full" || timer) return;
2464
- timer = setTimeout(() => {
2465
- timer = void 0;
2466
- snapshotAt = Date.now();
2467
- node();
2468
- }, Math.max(0, snapshotAt + SNAPSHOT_GAP_MS - Date.now()));
2469
- }, "progress");
2470
2659
  return {
2471
2660
  open() {
2472
2661
  if (ended) return;
2473
- if (!presence) typing(true);
2474
2662
  publishState();
2475
- if (mode === "full") node();
2476
2663
  keepalive = setInterval(() => {
2477
- if (ended) return;
2478
- if (presence) {
2479
- if (Date.now() - stateAt > PROGRESS_REFRESH_MS) publishState();
2480
- return;
2481
- }
2482
- if (waiting) return;
2483
- typing(true);
2484
- if (mode === "full" && !text3 && !rows.size) node();
2664
+ if (!ended && Date.now() - stateAt > PROGRESS_REFRESH_MS) publishState();
2485
2665
  }, KEEPALIVE_MS);
2486
2666
  keepalive.unref?.();
2487
2667
  },
2488
- setMode(next) {
2489
- if (ended || next === mode || presence || next === "presence") return;
2490
- mode = next;
2491
- if (mode === "full") node();
2492
- },
2493
- text(snapshot) {
2494
- if (ended || presence || snapshot === text3) return;
2495
- text3 = snapshot;
2496
- progress();
2497
- },
2498
- activity(event) {
2499
- if (ended || presence) return;
2500
- rows.set(event.id, {
2501
- id: event.id,
2502
- turnId: options.turnId() ?? "",
2503
- kind: event.kind,
2504
- title: event.title,
2505
- status: event.state === "done" ? "completed" : event.state,
2506
- sequence: rows.get(event.id)?.sequence ?? ++sequence
2507
- });
2508
- for (const id of rows.keys()) if (rows.size > MAX_ROWS) rows.delete(id);
2509
- progress();
2510
- },
2511
2668
  waiting(on) {
2512
2669
  if (ended || waiting === on) return;
2513
2670
  waiting = on;
2514
- if (!presence) typing(!on);
2515
2671
  publishState();
2516
- if (mode === "full") node();
2517
- },
2518
- refresh() {
2519
- if (!ended && state && !presence) publishState();
2520
2672
  },
2521
2673
  end(how) {
2522
2674
  if (ended) return;
2523
2675
  ended = true;
2524
- clearTimeout(timer);
2525
2676
  clearInterval(keepalive);
2526
- if (!presence) typing(false);
2527
- if (mode === "full") {
2528
- if (how !== "answered") node(true);
2529
- enqueue(() => client.clearStreaming(conversationId));
2530
- }
2531
- enqueue(() => client.setRuntimeTurn(how === "stopped" ? { conversationId, turnId: publicTurnId(), state: "interrupted", ...presence ? {} : options.context() } : { conversationId, turnId: null, state: "idle", ...presence ? {} : { queueDepth: 0 }, openedAt: null, turnUpdatedAt: null }));
2677
+ enqueue(() => client.setRuntimeTurn(how === "stopped" ? { conversationId, turnId: publicTurnId(), state: "interrupted" } : { conversationId, turnId: null, state: "idle", openedAt: null, turnUpdatedAt: null }));
2532
2678
  }
2533
2679
  };
2534
2680
  }
@@ -2536,8 +2682,8 @@ __name(createLivePublisher, "createLivePublisher");
2536
2682
 
2537
2683
  // src/runner/mls-ask.ts
2538
2684
  import { createHash as createHash7 } from "node:crypto";
2539
- import { readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
2540
- import { join as join5 } from "node:path";
2685
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync } from "node:fs";
2686
+ import { join as join6 } from "node:path";
2541
2687
  var sha2562 = /* @__PURE__ */ __name((value) => createHash7("sha256").update(value).digest("hex"), "sha256");
2542
2688
  var pause = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
2543
2689
  setTimeout(resolve3, ms).unref?.();
@@ -2592,9 +2738,9 @@ function createMlsAsks(options, approvalScope2) {
2592
2738
  });
2593
2739
  }
2594
2740
  });
2595
- const file = /* @__PURE__ */ __name((conversationId, requestId) => join5(options.directory, `${sha2562(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2741
+ const file = /* @__PURE__ */ __name((conversationId, requestId) => join6(options.directory, `${sha2562(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2596
2742
  const save = /* @__PURE__ */ __name((journal) => writeDurable(file(journal.conversationId, journal.requestId), JSON.stringify(journal)), "save");
2597
- const forget = /* @__PURE__ */ __name((journal) => rmSync2(file(journal.conversationId, journal.requestId), { force: true }), "forget");
2743
+ const forget = /* @__PURE__ */ __name((journal) => rmSync(file(journal.conversationId, journal.requestId), { force: true }), "forget");
2598
2744
  const operationOf = /* @__PURE__ */ __name((conversationId, messageId) => `runner-mls-ask:${sha2562(JSON.stringify([conversationId, messageId])).slice(0, 48)}`, "operationOf");
2599
2745
  const withdrawalOf = /* @__PURE__ */ __name((journal) => operationOf(journal.conversationId, `${journal.requestId}:withdraw`), "withdrawalOf");
2600
2746
  const audienceChanged = /* @__PURE__ */ __name((error) => codeOf(error) === "MLS_DEV_AUDIENCE_CHANGED", "audienceChanged");
@@ -2929,18 +3075,18 @@ function createMlsAsks(options, approvalScope2) {
2929
3075
  async recover() {
2930
3076
  let names;
2931
3077
  try {
2932
- names = readdirSync2(options.directory).filter((name) => name.endsWith(".json"));
3078
+ names = readdirSync3(options.directory).filter((name) => name.endsWith(".json"));
2933
3079
  } catch {
2934
3080
  return;
2935
3081
  }
2936
3082
  for (const name of names) {
2937
3083
  let journal;
2938
3084
  try {
2939
- journal = JSON.parse(readFileSync4(join5(options.directory, name), "utf8"));
3085
+ journal = JSON.parse(readFileSync5(join6(options.directory, name), "utf8"));
2940
3086
  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();
2941
3087
  for (const sent of journal.sends) sent.audience = snapshotMlsAudience(sent.audience);
2942
3088
  } catch {
2943
- rmSync2(join5(options.directory, name), { force: true });
3089
+ rmSync(join6(options.directory, name), { force: true });
2944
3090
  continue;
2945
3091
  }
2946
3092
  try {
@@ -3042,9 +3188,8 @@ var CLAIM_PATIENCE_MS = 2e4;
3042
3188
  var HISTORY_WINDOW = 20;
3043
3189
  var STATE_LIMIT_BYTES = 64 * 1024;
3044
3190
  var FAILURE_NOTICE = "Sorry, something went wrong and I could not finish this reply.";
3045
- var STOPS_SHORT_NOTICE = "Sorry, my reply could not be delivered in full.";
3046
3191
  var RESTART_NOTICE = "Sorry, I was restarted before I could finish replying to this. Please send it again if you still need an answer.";
3047
- var CLIENT_TYPES = { code: "generic", claude: "claude-code", codex: "codex", connector: "generic" };
3192
+ var CLIENT_TYPES = { code: "generic", claude: "claude-code", codex: "codex", acp: "generic", connector: "generic" };
3048
3193
  var objectSchema = /* @__PURE__ */ __name((properties, required = []) => ({ type: "object", properties, required, additionalProperties: false }), "objectSchema");
3049
3194
  var MAX_QUESTION = 4e3;
3050
3195
  var MAX_OPTION = 120;
@@ -3112,16 +3257,60 @@ __name(within, "within");
3112
3257
  var pause2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
3113
3258
  setTimeout(resolve3, ms).unref?.();
3114
3259
  }), "pause");
3115
- var audienceOf = /* @__PURE__ */ __name((room) => room?.memberIds ? { memberIds: [...room.memberIds], ...typeof room.membershipRevision === "number" ? { membershipRevision: room.membershipRevision } : {} } : null, "audienceOf");
3260
+ function agentRun(turns) {
3261
+ const history = turns.map(({ senderId, senderType }) => senderType === "ai_agent" ? { senderId, senderType, metadata: { turnSemantics: "turn_complete" } } : { senderId, senderType: "human" });
3262
+ return {
3263
+ consecutiveAgentTurns: buildParticipationHistorySnapshot(history.slice(1)).consecutiveAgentTurns,
3264
+ currentAgentStreakStartedByHuman: buildParticipationHistorySnapshot(history).currentAgentStreakStartedByHuman
3265
+ };
3266
+ }
3267
+ __name(agentRun, "agentRun");
3116
3268
  var inert = /* @__PURE__ */ __name((value) => value.replace(/\s+/g, " ").trim(), "inert");
3117
3269
  var OWNER_MARK = " [your owner]";
3118
3270
  var TRUSTED_MARK = " [trusted teammate agent]";
3119
3271
  var speaker = /* @__PURE__ */ __name((name) => inert(name).replace(/\[/g, "(").replace(/\]/g, ")"), "speaker");
3120
- var defused = /* @__PURE__ */ __name((text3) => text3.replace(/\[(\s*(?:trusted\s+teammate\s+agent|your\s+owner)\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?owner['\u2019]s\s+(?:private\s+note|errand)\s*)\]/gi, "($1)"), "defused");
3272
+ var defused = /* @__PURE__ */ __name((text3) => text3.replace(/\[(\s*(?:trusted\s+teammate\s+agent|your\s+owner)\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?owner['\u2019]s\s+(?:private\s+note|errand)\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?conversation\s+facts\s*)\]/gi, "($1)"), "defused");
3121
3273
  var NOTE_OPEN = "[owner's private note] Your owner left this for you on the machine you run on. It is not a message in this conversation: no one in it sees it, and it was not posted.";
3122
3274
  var NOTE_CLOSE = "[end of owner's private note]";
3123
3275
  var ERRAND_OPEN = "[owner's errand]";
3124
3276
  var ERRAND_CLOSE = "[end of owner's errand]";
3277
+ var FACTS_OPEN = "[conversation facts] Your runner added this from Canon's records. It is not a message, and no one in the conversation sees it.";
3278
+ var FACTS_CLOSE = "[end of conversation facts]";
3279
+ var MAX_FACT_MEMBERS = 50;
3280
+ var MAX_FACT_NAME = 200;
3281
+ var MAX_FACT_TOPIC = 500;
3282
+ var STYLE_RULES = {
3283
+ natural: void 0,
3284
+ collaborative: "Work openly with the people and agents here.",
3285
+ "mention-first": "Speak when you are clearly addressed; otherwise end your turn with no_reply.",
3286
+ "approval-gated": "Be cautious: join in only on a strong signal; otherwise end your turn with no_reply.",
3287
+ "handoff-only": "Act when someone explicitly hands you a task; otherwise end your turn with no_reply.",
3288
+ observer: "Stay quiet unless a reply is necessary, and end your turn with no_reply."
3289
+ };
3290
+ var factText = /* @__PURE__ */ __name((value, limit = MAX_FACT_NAME) => speaker(value).slice(0, limit).trimEnd(), "factText");
3291
+ function factsPrompt({ name, topic, owner, members, rules }, kind) {
3292
+ const shown = members.slice(0, MAX_FACT_MEMBERS).map((member) => member.isYou ? "you" : factText(member.name ?? member.id) + (member.isOwner ? OWNER_MARK : member.type === "ai_agent" ? " [agent]" : ""));
3293
+ const lines = !rules ? [] : [
3294
+ STYLE_RULES[rules.style],
3295
+ kind === "group" && rules.requireMentionForGroupReplies ? "Only messages that mention you reach you." : void 0,
3296
+ rules.allowAgentToAgent ? void 0 : "Messages from other agents do not reach you.",
3297
+ rules.allowLongRunningCollaboration ? void 0 : "After a person writes, another agent gets one exchange with you.",
3298
+ typeof rules.maxConsecutiveAgentTurns === "number" ? `Agents get at most ${rules.maxConsecutiveAgentTurns} turns in a row before a person writes.` : void 0
3299
+ ].filter((line) => line !== void 0);
3300
+ return [
3301
+ FACTS_OPEN,
3302
+ `Your owner: ${factText(owner.name ?? owner.id)}`,
3303
+ ...name ? [`Conversation: ${factText(name)}`] : [],
3304
+ ...topic ? [`Topic: ${factText(topic, MAX_FACT_TOPIC)}`] : [],
3305
+ `Members: ${shown.join(", ")}${members.length > shown.length ? `, and ${members.length - shown.length} more` : ""}`,
3306
+ ...lines.length ? [
3307
+ "This conversation's rules, which your owner or any of its admins can set. They say how you take part here, not what you may do:",
3308
+ ...lines.map((line) => `- ${line}`)
3309
+ ] : [],
3310
+ FACTS_CLOSE
3311
+ ];
3312
+ }
3313
+ __name(factsPrompt, "factsPrompt");
3125
3314
  var errandCommunicate = /* @__PURE__ */ __name((contact) => ({
3126
3315
  name: "communicate",
3127
3316
  description: "List Canon conversations or agents, or send text to your errand's recipient or to the conversation it came from" + (contact ? ". Start the conversation with the recipient first, with start_conversation." : "."),
@@ -3150,21 +3339,6 @@ var inputMessage = /* @__PURE__ */ __name((message2, trusted = false) => Object.
3150
3339
  ...message2.replyTo ? { replyTo: message2.replyTo } : {},
3151
3340
  createdAt: message2.createdAt ?? ""
3152
3341
  }), "inputMessage");
3153
- function outputOptions(turnId, trigger, kind, actedForOwner = false, messageId = `${turnId}:${kind}`) {
3154
- return {
3155
- messageId,
3156
- ...trigger.replyAuthority ? { replyAuthority: trigger.replyAuthority } : {},
3157
- ...trigger.activeSelfContextId ? { selfContextId: trigger.activeSelfContextId } : {},
3158
- metadata: {
3159
- turnId,
3160
- turnSemantics: "turn_complete",
3161
- ...kind === "final" ? {} : { replyBehavior: "suppress_auto_reply" },
3162
- ...kind === "final" && actedForOwner ? { [ACTED_FOR_OWNER]: true } : {}
3163
- }
3164
- };
3165
- }
3166
- __name(outputOptions, "outputOptions");
3167
- var ACTED_FOR_OWNER = "actedForOwner";
3168
3342
  var MAX_TEAM = 20;
3169
3343
  function approvalScope(request) {
3170
3344
  if (request.scopeKey !== void 0 && /^[A-Za-z0-9_:-]{1,64}$/.test(request.scopeKey)) return request.scopeKey;
@@ -3175,19 +3349,6 @@ function approvalScope(request) {
3175
3349
  __name(approvalScope, "approvalScope");
3176
3350
  var MAX_GROUP_PARTS = 100;
3177
3351
  var CUT_SHORT_NOTE = "(This reply was too long to send in full and was cut short.)";
3178
- var EXPIRED_UPLOAD = ["MEDIA_UPLOAD_TEMPORARY_EXPIRED", "MEDIA_UPLOAD_EXPIRED"];
3179
- function fitGroup(text3, files) {
3180
- const room = Math.max(1, MAX_GROUP_PARTS - files);
3181
- const count = /* @__PURE__ */ __name((value) => value ? splitTextByUtf8Bytes(value).length : 0, "count");
3182
- let fitted = text3;
3183
- for (let end = Math.floor(text3.length * Math.min(1, room / Math.max(1, count(text3)))); count(fitted) > room; end = Math.floor(end * 0.95)) {
3184
- fitted = `${text3.slice(0, end).replace(/[\uD800-\uDBFF]$/, "").trimEnd()}
3185
-
3186
- ${CUT_SHORT_NOTE}`;
3187
- }
3188
- return fitted;
3189
- }
3190
- __name(fitGroup, "fitGroup");
3191
3352
  function fitEncrypted(text3) {
3192
3353
  if (Buffer.byteLength(text3) <= MLS_DEV_TEXT_MAX_BYTES) return text3;
3193
3354
  const room = MLS_DEV_TEXT_MAX_BYTES - Buffer.byteLength(`
@@ -3205,6 +3366,27 @@ ${CUT_SHORT_NOTE}`);
3205
3366
  ${CUT_SHORT_NOTE}`;
3206
3367
  }
3207
3368
  __name(fitEncrypted, "fitEncrypted");
3369
+ function encryptedParts(text3, limit) {
3370
+ if (Buffer.byteLength(text3) <= MLS_DEV_TEXT_MAX_BYTES) return [text3];
3371
+ const parts = splitTextWithinBytes(text3, MLS_DEV_TEXT_MAX_BYTES).filter((part) => part.trim());
3372
+ if (parts.length <= limit) return parts;
3373
+ const room = MLS_DEV_TEXT_MAX_BYTES - Buffer.byteLength(`
3374
+
3375
+ ${CUT_SHORT_NOTE}`);
3376
+ let bytes = 0;
3377
+ let end = 0;
3378
+ for (const char of parts[limit - 1]) {
3379
+ bytes += Buffer.byteLength(char);
3380
+ if (bytes > room) break;
3381
+ end += char.length;
3382
+ }
3383
+ return [...parts.slice(0, limit - 1), `${parts[limit - 1].slice(0, end).trimEnd()}
3384
+
3385
+ ${CUT_SHORT_NOTE}`];
3386
+ }
3387
+ __name(encryptedParts, "encryptedParts");
3388
+ var MAX_REPLY_PARTS = 32;
3389
+ var REPLY_GRANT_RETRIES = 6;
3208
3390
  var ENCRYPTED_FILES = "Files cannot be sent in encrypted conversations yet";
3209
3391
  var ERRAND_RETRY_MS = 5e3;
3210
3392
  var STOPPED_ERRAND = {
@@ -3216,14 +3398,10 @@ var STOPPED_ERRAND = {
3216
3398
  };
3217
3399
  var SUBMITTED = { phase: "submitted", settle: /* @__PURE__ */ __name(async () => void 0, "settle"), release: /* @__PURE__ */ __name(async () => void 0, "release"), cancel: /* @__PURE__ */ __name(async () => void 0, "cancel") };
3218
3400
  var SILENT = { open() {
3219
- }, setMode() {
3220
- }, text() {
3221
- }, activity() {
3222
3401
  }, waiting() {
3223
- }, refresh() {
3224
3402
  }, end() {
3225
3403
  } };
3226
- var UNAVAILABLE_ENCRYPTED = ["react", "send_file", "ask_question"];
3404
+ var UNAVAILABLE_ENCRYPTED = ["send_file", "ask_question"];
3227
3405
  var carriesCards = /* @__PURE__ */ __name((audience) => mlsDevProtocolCarriesCards(audience?.protocol), "carriesCards");
3228
3406
  var carriesItems = /* @__PURE__ */ __name((audience) => audience?.protocol === MLS_DEV_TEXT_PROTOCOL_V3, "carriesItems");
3229
3407
  function runtimeDescriptor(driver) {
@@ -3264,14 +3442,6 @@ async function createRunner(options) {
3264
3442
  const mentionAliases = names(options.mentionAliases, "mentionAliases").map((name) => name.replace(/^@+/, "")).filter(Boolean);
3265
3443
  const descriptor = runtimeDescriptor(driver);
3266
3444
  const offersQuestions = !driver.capabilities.nativeQuestions && ["choose", "text"].every((kind) => driver.capabilities.ask.includes(kind));
3267
- const capabilities = {
3268
- supportsInterrupt: true,
3269
- supportsInputInterrupt: false,
3270
- supportsQueue: true,
3271
- supportsInterleave: false,
3272
- supportsRequiresAction: driver.capabilities.ask.length > 0,
3273
- supportsNonFinalPermanentMessages: false
3274
- };
3275
3445
  const rooms = /* @__PURE__ */ new Map();
3276
3446
  const running = /* @__PURE__ */ new Set();
3277
3447
  const detach = [];
@@ -3286,7 +3456,6 @@ async function createRunner(options) {
3286
3456
  let tools;
3287
3457
  let lock;
3288
3458
  let owner;
3289
- const approvals = /* @__PURE__ */ new Map();
3290
3459
  let roots = [];
3291
3460
  let mlsOn = false;
3292
3461
  let mlsAsks;
@@ -3295,12 +3464,14 @@ async function createRunner(options) {
3295
3464
  if (!mlsIds) throw new Error("MLS_AUTHORITY_REQUIRED");
3296
3465
  return mlsIds;
3297
3466
  }, "keyed");
3467
+ let outboundClosed = false;
3298
3468
  let ownershipVersion;
3299
3469
  let displayName;
3470
+ let ownerOf;
3300
3471
  const refused = /* @__PURE__ */ new Set();
3301
3472
  const deferredNotes = /* @__PURE__ */ new Set();
3473
+ const ruleNotes = /* @__PURE__ */ new Set();
3302
3474
  const guards = /* @__PURE__ */ new Map();
3303
- const relayed = /* @__PURE__ */ new Set();
3304
3475
  const relaySends = /* @__PURE__ */ new Map();
3305
3476
  let errands;
3306
3477
  let errandInbox2;
@@ -3329,24 +3500,17 @@ async function createRunner(options) {
3329
3500
  const settle = /* @__PURE__ */ __name(async (ids, reason) => (await Promise.allSettled(ids.map((id) => endpoint.setInboundState(id, "settled", reason)))).every(({ status }) => status === "fulfilled"), "settle");
3330
3501
  const queued = /* @__PURE__ */ __name((turn) => turn.inputs.filter((input) => input.handle.phase === "queued"), "queued");
3331
3502
  const turnId = /* @__PURE__ */ __name((roomId, inputs) => `turn-${sha2563(JSON.stringify([roomId, ...inputs.map((input) => [input.id, input.generation])])).slice(0, 32)}`, "turnId");
3332
- const livePublisher = /* @__PURE__ */ __name((current, turn, mode) => createLivePublisher({
3503
+ const livePublisher = /* @__PURE__ */ __name((current, turn) => createLivePublisher({
3333
3504
  client,
3334
3505
  conversationId: current.id,
3335
- mode,
3336
3506
  turnId: /* @__PURE__ */ __name(() => turn?.id ?? null, "turnId"),
3337
- context: /* @__PURE__ */ __name(() => ({
3338
- queueDepth: current.queue.length,
3339
- currentSpeakerId: agentId,
3340
- capabilities,
3341
- activeMessageIds: turn?.inputs.map(({ payload }) => payload.message.id) ?? []
3342
- }), "context"),
3343
3507
  enqueue: /* @__PURE__ */ __name((publish2) => {
3344
3508
  current.lane = current.lane.then(publish2).catch(() => void 0);
3345
3509
  }, "enqueue")
3346
3510
  }), "livePublisher");
3347
- const stateFile = /* @__PURE__ */ __name((session) => join6(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2563(JSON.stringify([scope, session]))}.json`), "stateFile");
3511
+ const stateFile = /* @__PURE__ */ __name((session) => join7(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2563(JSON.stringify([scope, session]))}.json`), "stateFile");
3348
3512
  const resetSession = /* @__PURE__ */ __name(async (session) => {
3349
- rmSync3(stateFile(session), { force: true });
3513
+ rmSync2(stateFile(session), { force: true });
3350
3514
  mlsAsks?.endSession(session);
3351
3515
  await driver.reset?.(session);
3352
3516
  }, "resetSession");
@@ -3354,15 +3518,18 @@ async function createRunner(options) {
3354
3518
  function refuse(roomId, why) {
3355
3519
  if (refused.has(roomId)) return;
3356
3520
  refused.add(roomId);
3357
- log(`Refusing encrypted room ${roomId}: ${why === "no-device" ? "this runner has no MLS device (run it with --mls-dev-config)" : why === "remote" ? `the ${driver.name} driver runs off this host, which an encrypted room allows only in relay mode` : "its encryption protocol is not supported"}. Its messages are not delivered to this agent.`);
3521
+ log(why === "plaintext" ? `Refusing room ${roomId}: it is not encrypted, and this runner takes part only in encrypted rooms. Its messages are not delivered to this agent.` : `Refusing encrypted room ${roomId}: ${why === "no-device" ? "this runner has no MLS device (run it with --mls-dev-config)" : why === "remote" ? `the ${driver.name} driver runs off this host, which an encrypted room allows only in relay mode` : "its encryption protocol is not supported"}. Its messages are not delivered to this agent.`);
3358
3522
  }
3359
3523
  __name(refuse, "refuse");
3360
3524
  async function authorize() {
3361
3525
  await endpoint.mlsDevDevice.ensureReady();
3362
3526
  const me = await client.getAgentMe();
3363
- requireMlsSupport({ environmentId: options.environmentId, agent: me });
3527
+ requireMlsSupport({ environmentId: options.environmentId });
3364
3528
  if (me.agentId !== agentId || me.ownershipVersion !== ownershipVersion) throw new Error("MLS_AUTHORITY_REQUIRED");
3529
+ outboundClosed = me.outboundPolicy === "closed";
3530
+ if (outboundClosed && options.relay) throw new Error("MLS_AUTHORITY_REQUIRED");
3365
3531
  displayName = me.displayName;
3532
+ ownerOf = { id: me.ownerId, ...me.ownerName ? { name: me.ownerName } : {} };
3366
3533
  return me.ownerId;
3367
3534
  }
3368
3535
  __name(authorize, "authorize");
@@ -3413,55 +3580,53 @@ ${error instanceof Error ? error.message : String(error)}`;
3413
3580
  if (!input) return;
3414
3581
  if (stopping) return void await handle.release("runner-stopping");
3415
3582
  const target = room(input.payload.conversationId);
3416
- if (input.mls) target.mls = true;
3417
3583
  target.queue.push(input);
3418
- target.turn?.live.refresh();
3419
3584
  requeue(target);
3420
3585
  pump();
3421
3586
  }
3422
3587
  __name(offer, "offer");
3423
3588
  async function admitInput(event, handle) {
3424
3589
  const payload = event.data;
3590
+ const { conversationId } = payload;
3425
3591
  const generation = (await endpoint.getInbound(event.id))?.handoffGeneration ?? "";
3426
- const mode = await endpoint.getConversationContentMode(payload.conversationId).catch((error) => {
3427
- if (error.code === "MLS_DEV_ROOM_MODE_UNAVAILABLE") return "plaintext";
3592
+ let mode;
3593
+ try {
3594
+ mode = await endpoint.getConversationContentMode(conversationId);
3595
+ } catch (error) {
3596
+ if (error.code !== "MLS_DEV_ROOM_MODE_UNAVAILABLE") throw error;
3597
+ if (!(await client.getConversation(conversationId))?.memberIds?.includes(agentId)) return void await handle.cancel("membership-lost");
3428
3598
  throw error;
3429
- });
3430
- if (mode === "plaintext") {
3431
- if (options.relay) {
3432
- if (!relayed.has(payload.conversationId)) log(`Relay mode: leaving plaintext room ${payload.conversationId} to the connector.`);
3433
- relayed.add(payload.conversationId);
3434
- return void await handle.cancel("relay-plaintext-room");
3435
- }
3436
- if (payload.turnDispatch && payload.turnDispatch.kind !== "run_turn") return void await handle.cancel("observe-only");
3437
- return { id: event.id, payload, handle, generation };
3438
3599
  }
3439
3600
  if (mode !== "mls-dev-text" || !mlsOn || driver.capabilities.remote && !options.relay) {
3440
- refuse(payload.conversationId, !mlsOn ? "no-device" : mode !== "mls-dev-text" ? "protocol" : "remote");
3441
- return void await handle.cancel("encrypted-room-refused");
3601
+ refuse(conversationId, mode === "plaintext" ? "plaintext" : !mlsOn ? "no-device" : mode !== "mls-dev-text" ? "protocol" : "remote");
3602
+ return void await handle.cancel(mode === "plaintext" ? "plaintext-room-refused" : "encrypted-room-refused");
3442
3603
  }
3443
- const { conversationId } = payload;
3444
3604
  const groups = endpoint.mlsDevGroups;
3445
3605
  await authorize();
3446
3606
  const binding = await groups.readVerifiedMessageBinding({ conversationId, messageId: payload.message.id });
3447
3607
  if (!binding) throw new Error("MLS_INPUT_NOT_VERIFIED");
3448
3608
  const { messages } = await endpoint.query("get_messages", { conversationId, messageIds: [binding.messageId] });
3449
3609
  const [message2] = messages;
3450
- if (messages.length !== 1 || message2.id !== binding.messageId || message2.senderId !== binding.senderId || message2.senderType !== binding.senderPrincipal.type || typeof message2.text !== "string" && !(message2.text === null && (message2.attachments?.length ?? 0) === 1)) throw new Error("MLS_INPUT_NOT_VERIFIED");
3610
+ if (messages.length !== 1 || message2.id !== binding.messageId || message2.senderId !== binding.senderId || message2.senderType !== binding.senderPrincipal.type || !Number.isSafeInteger(message2.mlsFeedOrdinal) || typeof message2.text !== "string" && !(message2.text === null && (message2.attachments?.length ?? 0) === 1)) throw new Error("MLS_INPUT_NOT_VERIFIED");
3451
3611
  const { audience, ownerId } = await currentAudience(conversationId);
3452
3612
  if (binding.groupId !== audience.groupId) throw new Error("MLS_INPUT_NOT_VERIFIED");
3453
3613
  const obsolete = binding.head.sequence < audience.head.sequence;
3454
3614
  if (!obsolete && JSON.stringify(binding.head) !== JSON.stringify(audience.head)) throw new Error("MLS_INPUT_AHEAD_OF_AUDIENCE");
3455
3615
  if (obsolete) return void await handle.settle("mls-obsolete-audience");
3456
3616
  const fromAgent = binding.senderPrincipal.type !== "human";
3457
- if (fromAgent) {
3458
- const wake = agentWake(conversationId, event.id, message2.senderId, message2.text ?? "", message2.mentions ?? []);
3459
- if (wake !== "admit") return void await handle.settle(wake);
3460
- } else {
3617
+ const mentioned = (message2.mentions ?? []).includes(agentId) || mentionsAgent(message2.text ?? "", [displayName, agentId, ...mentionAliases]);
3618
+ if (fromAgent && (message2.senderId === agentId || !mentioned)) return void await handle.settle("mls-observe-only-sender");
3619
+ if (!fromAgent) {
3461
3620
  const target = room(conversationId);
3462
3621
  target.agentInputs = void 0;
3463
3622
  target.capped = false;
3464
3623
  }
3624
+ const rule = await withheld(conversationId, message2, fromAgent, mentioned, ownerId);
3625
+ if (rule) return void await handle.settle(rule);
3626
+ if (fromAgent) {
3627
+ const wake = agentWake(conversationId, event.id);
3628
+ if (wake !== "admit") return void await handle.settle(wake);
3629
+ }
3465
3630
  const verified = {
3466
3631
  id: message2.id,
3467
3632
  senderId: message2.senderId,
@@ -3478,8 +3643,48 @@ ${error instanceof Error ? error.message : String(error)}`;
3478
3643
  return { id: event.id, handle, generation, mls: audience, payload: { conversationId, message: verified } };
3479
3644
  }
3480
3645
  __name(admitInput, "admitInput");
3481
- function agentWake(roomId, inputId, senderId, text3, mentions) {
3482
- if (senderId === agentId || !mentions.includes(agentId) && !mentionsAgent(text3, [displayName, agentId, ...mentionAliases])) return "mls-observe-only-sender";
3646
+ async function withheld(roomId, message2, fromAgent, mentioned, ownerId) {
3647
+ const current = await endpoint.query(
3648
+ "conversation",
3649
+ { conversationId: roomId }
3650
+ );
3651
+ if (current?.blockedMemberIds?.includes(message2.senderId)) return "mls-blocked-sender";
3652
+ if (options.relay || !current?.behavior) return void 0;
3653
+ const { maxConsecutiveAgentTurns, allowLongRunningCollaboration } = current.behavior.participation;
3654
+ const decision = evaluateParticipationPolicy(current.behavior, {
3655
+ conversationType: resolveConversationPolicyScope(current.memberIds),
3656
+ senderType: fromAgent ? "ai_agent" : "human",
3657
+ isOwner: !fromAgent && message2.senderId === ownerId,
3658
+ mentionedAgent: mentioned,
3659
+ ...fromAgent ? agentRun(await turnsBehind(roomId, message2, Math.max(maxConsecutiveAgentTurns ?? 0, allowLongRunningCollaboration ? 0 : 2))) : {}
3660
+ });
3661
+ if (decision.allow) return void 0;
3662
+ const note = `${roomId}
3663
+ ${decision.reasonCode}`;
3664
+ if (!ruleNotes.has(note)) log(`The rules of ${roomId} keep messages from a turn: ${decision.reasonCode}.`);
3665
+ ruleNotes.add(note);
3666
+ return `mls-${decision.reasonCode}`;
3667
+ }
3668
+ __name(withheld, "withheld");
3669
+ async function turnsBehind(roomId, message2, limit) {
3670
+ const turns = [message2];
3671
+ let before = String(message2.mlsFeedOrdinal);
3672
+ while (before && turns.length <= limit) {
3673
+ const page = await endpoint.query(
3674
+ "list_messages",
3675
+ { conversationId: roomId, limit: PARTICIPATION_HISTORY_FETCH_LIMIT, before }
3676
+ );
3677
+ for (const entry of page.messages) {
3678
+ if (entry.contentType === "interaction" || entry.senderId === turns.at(-1).senderId) continue;
3679
+ turns.push(entry);
3680
+ if (entry.senderType !== "ai_agent" || turns.length > limit) return turns;
3681
+ }
3682
+ before = page.nextBefore;
3683
+ }
3684
+ return turns;
3685
+ }
3686
+ __name(turnsBehind, "turnsBehind");
3687
+ function agentWake(roomId, inputId) {
3483
3688
  const target = room(roomId);
3484
3689
  const streak = target.agentInputs ??= /* @__PURE__ */ new Set();
3485
3690
  if (!streak.has(inputId) && streak.size >= agentTurnCap) {
@@ -3519,12 +3724,10 @@ ${error instanceof Error ? error.message : String(error)}`;
3519
3724
  continue;
3520
3725
  }
3521
3726
  if (!candidate.queue.length) continue;
3522
- const encrypted = !!candidate.queue[0].mls;
3523
- const other = candidate.queue.findIndex((input) => !!input.mls !== encrypted);
3524
- const inputs = candidate.queue.splice(0, Math.min(MAX_INPUTS_PER_TURN, other < 0 ? MAX_INPUTS_PER_TURN : other));
3727
+ const inputs = candidate.queue.splice(0, MAX_INPUTS_PER_TURN);
3525
3728
  const turn = newTurn(inputs, turnId(candidate.id, inputs), candidate.id);
3526
3729
  candidate.turn = turn;
3527
- (turn.live = driver.capabilities.remote ? SILENT : livePublisher(candidate, turn, encrypted ? "presence" : "status")).open();
3730
+ (turn.live = driver.capabilities.remote ? SILENT : livePublisher(candidate, turn)).open();
3528
3731
  const done = execute(candidate, turn).catch((error) => {
3529
3732
  warn("turn failed")(error);
3530
3733
  return dispose(queued(turn), "release", "turn-failed");
@@ -3541,6 +3744,32 @@ ${error instanceof Error ? error.message : String(error)}`;
3541
3744
  while (localErrands.length && !stopping && running.size < maxTurns) startErrand(void 0, localErrands.shift());
3542
3745
  }
3543
3746
  __name(pump, "pump");
3747
+ async function roomFacts(conversation) {
3748
+ const { members } = await endpoint.query("get_members", { conversationId: conversation.id });
3749
+ const names2 = new Map(members.map(({ userId, displayName: name }) => [userId, name]));
3750
+ const owner2 = Object.freeze(ownerOf);
3751
+ const participation = conversation.behavior?.participation;
3752
+ return Object.freeze({
3753
+ ...conversation.name ? { name: conversation.name } : {},
3754
+ ...conversation.topic ? { topic: conversation.topic } : {},
3755
+ owner: owner2,
3756
+ members: Object.freeze(conversation.memberIds.map((id) => Object.freeze({
3757
+ id,
3758
+ ...names2.get(id) ? { name: names2.get(id) } : {},
3759
+ type: conversation.participantTypes?.[id] === "ai_agent" || id === agentId ? "ai_agent" : "human",
3760
+ isOwner: id === owner2.id,
3761
+ isYou: id === agentId
3762
+ }))),
3763
+ ...participation ? { rules: Object.freeze({
3764
+ style: participation.style,
3765
+ requireMentionForGroupReplies: participation.requireMentionForGroupReplies,
3766
+ allowAgentToAgent: participation.allowAgentToAgent,
3767
+ allowLongRunningCollaboration: participation.allowLongRunningCollaboration,
3768
+ maxConsecutiveAgentTurns: participation.maxConsecutiveAgentTurns ?? null
3769
+ }) } : {}
3770
+ });
3771
+ }
3772
+ __name(roomFacts, "roomFacts");
3544
3773
  async function execute(current, turn) {
3545
3774
  let conversation;
3546
3775
  try {
@@ -3551,12 +3780,18 @@ ${error instanceof Error ? error.message : String(error)}`;
3551
3780
  }
3552
3781
  if (!conversation?.memberIds?.includes(agentId)) return loseMembership(current);
3553
3782
  const kind = conversation.memberIds.length > 2 ? "group" : "direct";
3554
- if (turn.inputs[0].mls && !await beforeTurn(current, turn)) return;
3783
+ if (!await beforeTurn(current, turn)) return;
3784
+ if (!options.relay) {
3785
+ try {
3786
+ turn.facts = await roomFacts(conversation);
3787
+ } catch (error) {
3788
+ warn("room read failed; input returns to the inbox")(error);
3789
+ return dispose(queued(turn), "release", "room-unavailable");
3790
+ }
3791
+ }
3555
3792
  turn.trusted = await confirmTrusted(turn, conversation.memberIds);
3556
3793
  turn.notes = pendingNotes(current.id);
3557
- 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) })));
3558
- const mode = turn.mls ? "presence" : driver.capabilities.streaming && resolveTurnVerbosity({ conversationType: kind }) === "verbose" ? "full" : "status";
3559
- turn.live.setMode(mode);
3794
+ turn.contexts = openErrands(current.id);
3560
3795
  const history = driver.capabilities.sessionMemory === "none" ? await recentHistory(current, turn) : [];
3561
3796
  await materialize(current, turn);
3562
3797
  const bound = bindTools(current, turn);
@@ -3567,7 +3802,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3567
3802
  if (live.length && !turn.controller.signal.aborted) {
3568
3803
  turn.inputs = live;
3569
3804
  turn.id = turnId(current.id, live);
3570
- turn.pickUp = () => pickUp(current, turn, mode);
3805
+ turn.pickUp = () => pickUp(current, turn);
3571
3806
  try {
3572
3807
  work = Promise.resolve(driver.runTurn(buildTurn(current.id, turn, kind, history, bound.tools)));
3573
3808
  } catch (error) {
@@ -3613,26 +3848,21 @@ ${error instanceof Error ? error.message : String(error)}`;
3613
3848
  turn.ended.abort();
3614
3849
  bound.release();
3615
3850
  clearTimeout(turn.timer);
3616
- await deliver(current, turn, outcome, audienceOf(conversation));
3851
+ await deliver(current, turn, outcome);
3617
3852
  } finally {
3618
3853
  turn.ended.abort();
3619
3854
  bound.release();
3620
- rmSync3(conversationMediaCacheDir(inboundMedia(current.id)), { recursive: true, force: true });
3855
+ rmSync2(conversationMediaCacheDir(inboundMedia(current.id)), { recursive: true, force: true });
3621
3856
  }
3622
3857
  }
3623
3858
  __name(execute, "execute");
3624
3859
  function started(current, turn) {
3625
3860
  turn.submitted = true;
3626
3861
  progress(turn);
3627
- if (!turn.mls) void client.markAsRead(current.id).catch(warn("mark read"));
3628
- for (const { payload } of turn.mls ? [] : turn.inputs) {
3629
- if (payload.message.metadata?.inboundDisposition === "queued") {
3630
- void client.updateMessageDisposition(current.id, payload.message.id, "accepted_now").catch(warn("accepted_now"));
3631
- }
3632
- }
3862
+ if (!options.relay) void client.markAsRead(current.id).catch(warn("mark read"));
3633
3863
  }
3634
3864
  __name(started, "started");
3635
- async function pickUp(current, turn, mode) {
3865
+ async function pickUp(current, turn) {
3636
3866
  const live = queued(turn);
3637
3867
  if (live.length !== turn.inputs.length) {
3638
3868
  abort(turn, "requeue");
@@ -3642,7 +3872,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3642
3872
  if (result.status === "submitted") {
3643
3873
  turn.pickUp = void 0;
3644
3874
  started(current, turn);
3645
- if (!turn.controller.signal.aborted) (turn.live = livePublisher(current, turn, mode)).open();
3875
+ if (!turn.controller.signal.aborted) (turn.live = livePublisher(current, turn)).open();
3646
3876
  }
3647
3877
  return result;
3648
3878
  });
@@ -3656,20 +3886,19 @@ ${error instanceof Error ? error.message : String(error)}`;
3656
3886
  __name(pickUp, "pickUp");
3657
3887
  function requeue(current) {
3658
3888
  const turn = current.turn, next = current.queue[0];
3659
- if (!turn?.pickUp || turn.pickingUp || turn.controller.signal.aborted || !next) return;
3660
- if (!!next.mls !== !!turn.inputs[0].mls || turn.inputs.length >= MAX_INPUTS_PER_TURN) return;
3889
+ if (!turn?.pickUp || turn.pickingUp || turn.controller.signal.aborted || !next || turn.inputs.length >= MAX_INPUTS_PER_TURN) return;
3661
3890
  abort(turn, "requeue");
3662
3891
  }
3663
3892
  __name(requeue, "requeue");
3664
3893
  async function confirmTrusted(turn, memberIds) {
3665
- if (!trustedAgents.size || driver.capabilities.remote || options.relay) return /* @__PURE__ */ new Set();
3666
- 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));
3894
+ const audience = turn.mls;
3895
+ if (!trustedAgents.size || driver.capabilities.remote || options.relay || !audience) return /* @__PURE__ */ new Set();
3896
+ const candidates = turn.inputs.map(({ payload }) => payload.message).filter((message2) => message2.senderType === "ai_agent" && message2.senderId !== agentId && trustedAgents.has(message2.senderId));
3667
3897
  if (!candidates.length) return /* @__PURE__ */ new Set();
3668
- const roster = turn.mls?.roster ?? [];
3669
- if (roster.some((entry) => isConnectorRelayInstallation(entry.device.installationId))) return /* @__PURE__ */ new Set();
3670
- const rosterIds = turn.mls ? await Promise.all(roster.map(async (entry) => await endpoint.mlsAccountOf(turn.mls.conversationId, entry.device.principal) ?? "")) : [];
3898
+ if (audience.roster.some((entry) => isConnectorRelayInstallation(entry.device.installationId))) return /* @__PURE__ */ new Set();
3899
+ const rosterIds = await Promise.all(audience.roster.map(async (entry) => await endpoint.mlsAccountOf(audience.conversationId, entry.device.principal) ?? ""));
3671
3900
  if (rosterIds.includes("")) return /* @__PURE__ */ new Set();
3672
- const member = /* @__PURE__ */ __name((id) => memberIds.includes(id) && (!turn.mls || rosterIds.includes(id)), "member");
3901
+ const member = /* @__PURE__ */ __name((id) => memberIds.includes(id) && rosterIds.includes(id), "member");
3673
3902
  if (candidates.some((message2) => !member(message2.senderId))) return /* @__PURE__ */ new Set();
3674
3903
  try {
3675
3904
  const { ownerId } = await client.getAgentMe();
@@ -3732,7 +3961,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3732
3961
  return check;
3733
3962
  }
3734
3963
  __name(reaudience, "reaudience");
3735
- const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join6(home, "runner", options.environmentId, agentId) }), "inboundMedia");
3964
+ const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join7(home, "runner", options.environmentId, agentId) }), "inboundMedia");
3736
3965
  async function materialize(current, turn) {
3737
3966
  if (driver.capabilities.remote) return;
3738
3967
  for (const { payload: { message: message2 } } of turn.inputs) {
@@ -3775,7 +4004,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3775
4004
  const file = await admitFile(path);
3776
4005
  if (typeof file === "string") return { result: toolText(file, true) };
3777
4006
  turn.outbox.push(file.path);
3778
- return { result: toolText(`${basename3(file.path)} will be sent with your reply.`) };
4007
+ return { result: toolText(`${basename4(file.path)} will be sent with your reply.`) };
3779
4008
  }, "send_file"),
3780
4009
  communicate: /* @__PURE__ */ __name(async (args) => ({ result: await communicate(current, turn, args) }), "communicate"),
3781
4010
  // RUN-25 through a tool: the same `ask`, so the same responder, card, expiry, waiting_input and paused deadline;
@@ -3797,35 +4026,30 @@ ${error instanceof Error ? error.message : String(error)}`;
3797
4026
  };
3798
4027
  const cards = carriesCards(turn.mls), items = carriesItems(turn.mls);
3799
4028
  const errand = turn.errand;
3800
- const sendsFiles = roots.length > 0 && actedForOwner(turn);
4029
+ const sendsFiles = roots.length > 0 && forOwner2(turn);
3801
4030
  const binding = {
3802
- // §6: an encrypted turn gets no_reply and a communicate that only lists and sends encrypted text to its own room,
3803
- // and in a room created for cards, ask_question. §12.6: an errand turn reaches its recipient and its origin, and
3804
- // asks only in an origin its report may go to.
4031
+ // §6: a turn gets no_reply, a communicate that only lists and sends encrypted text to its own room, and react, which
4032
+ // a relay's credential cannot do; in a room created for cards, ask_question, and in one that carries items, send_file.
4033
+ // §12.6: an errand turn reaches its recipient and its origin, and asks only in an origin its report goes to.
3805
4034
  definitions: errand ? [
3806
4035
  TOOLS.no_reply,
3807
4036
  errandCommunicate("contact" in errand.target),
3808
- ...errand.reports && (!turn.mls || cards) && offersQuestions ? [TOOLS.ask_question] : []
3809
- ] : turn.mls ? [
4037
+ ...errand.reports && cards && offersQuestions ? [TOOLS.ask_question] : []
4038
+ ] : [
3810
4039
  TOOLS.no_reply,
3811
4040
  ENCRYPTED_COMMUNICATE,
4041
+ ...options.relay ? [] : [TOOLS.react],
3812
4042
  ...items && sendsFiles ? [TOOLS.send_file] : [],
3813
4043
  ...cards && offersQuestions ? [TOOLS.ask_question] : []
3814
- ] : [
3815
- TOOLS.no_reply,
3816
- canonCommunicateToolDefinition(),
3817
- TOOLS.react,
3818
- ...sendsFiles ? [TOOLS.send_file] : [],
3819
- ...offersQuestions ? [TOOLS.ask_question] : []
3820
4044
  ],
3821
4045
  async call(name, args) {
3822
4046
  if (!turn.open) throw turnEnded2();
3823
4047
  if (name === "no_reply" && turn.silenced) return { result: toolText(NO_REPLY_ACK_NOTE) };
3824
4048
  if (turn.controller.signal.aborted || turn.silenced) throw turnEnded2();
3825
- if (turn.mls && !errand && UNAVAILABLE_ENCRYPTED.includes(name) && !(name === "ask_question" && cards) && !(name === "send_file" && items)) {
4049
+ if (!errand && UNAVAILABLE_ENCRYPTED.includes(name) && !(name === "ask_question" && cards) && !(name === "send_file" && items)) {
3826
4050
  return { result: toolText(`${name} is not available in encrypted conversations yet.`, true) };
3827
4051
  }
3828
- if (name === "send_file" && !errand && roots.length && !actedForOwner(turn)) {
4052
+ if (name === "send_file" && !errand && roots.length && !forOwner2(turn)) {
3829
4053
  return { result: toolText("send_file sends files only in a turn answering your owner.", true) };
3830
4054
  }
3831
4055
  const handler = binding.definitions.some((definition) => definition.name === name) ? handlers[name] : void 0;
@@ -3846,11 +4070,9 @@ ${error instanceof Error ? error.message : String(error)}`;
3846
4070
  }, "call") } };
3847
4071
  }
3848
4072
  __name(bindTools, "bindTools");
3849
- const sealed = /* @__PURE__ */ __name((turn) => turn.errand ? "room" in turn.errand.target && !!turn.errand.target.mls : !!turn.mls, "sealed");
3850
4073
  async function communicate(current, turn, args) {
3851
4074
  const errand = turn.errand;
3852
- const origins = errand ? errand.origin ? [errand.origin.room] : [] : turn.contexts.flatMap(({ record }) => record.origin ? [record.origin] : []);
3853
- if (!errand && !sealed(turn) && !origins.length) return executeCanonCommunicateTool(client, args);
4075
+ const origins = errand ? errand.origin ? [errand.origin.room] : [] : turn.contexts.flatMap((record) => record.origin ? [record.origin] : []);
3854
4076
  let command;
3855
4077
  try {
3856
4078
  command = parseCommunicateToolInput(args);
@@ -3862,19 +4084,14 @@ ${error instanceof Error ? error.message : String(error)}`;
3862
4084
  }
3863
4085
  if (command.action === "message_existing" && command.text.trim()) {
3864
4086
  const to = command.conversationId;
3865
- if (turn.mls && !errand && to === current.id) return sealedSend(turn, to, command.text, turn.mls);
4087
+ if (!errand && to === current.id) return sealedSend(turn, to, command.text, turn.mls);
3866
4088
  if (errand && "room" in errand.target && to === errand.target.room) {
3867
4089
  reach(turn, to);
3868
- return errand.target.mls ? sealedSend(turn, to, command.text) : executeCanonCommunicateTool(client, args);
4090
+ return sealedSend(turn, to, command.text);
3869
4091
  }
3870
4092
  if (origins.includes(to)) {
3871
- const mode = await endpoint.getConversationContentMode(to);
3872
- if (mode === "mls-dev-text" && mlsOn) return sealedSend(turn, to, command.text);
3873
- if (mode !== "plaintext") return toolText(`${to} is an encrypted conversation this runner cannot write to.`, true);
3874
- if (sealed(turn)) {
3875
- return toolText(errand ? `This errand's recipient is in an encrypted conversation, so nothing of it may go to ${to}, which is not encrypted. Your final answer stays on your owner's machine as your report.` : `Nothing from this encrypted conversation may go to ${to}: it is not encrypted, and Canon must never receive this conversation's content in plaintext. Your owner sees on their machine that the errand was answered.`, true);
3876
- }
3877
- return executeCanonCommunicateTool(client, args);
4093
+ if (await endpoint.getConversationContentMode(to) === "mls-dev-text" && mlsOn) return sealedSend(turn, to, command.text);
4094
+ return toolText(`${to} is not an encrypted conversation this runner can write to.`, true);
3878
4095
  }
3879
4096
  }
3880
4097
  if (errand && "contact" in errand.target && (command.action === "start_conversation" || command.action === "start_direct") && command.principalId === errand.target.contact) {
@@ -3884,8 +4101,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3884
4101
  ${JSON.stringify(result, null, 2)}`);
3885
4102
  }
3886
4103
  if (errand) return toolText("On an errand, communicate can only list conversations or agents, or send text to the errand's recipient or its origin.", true);
3887
- if (sealed(turn)) return toolText("In an encrypted conversation, communicate can only list conversations or agents, or send text to this conversation.", true);
3888
- return executeCanonCommunicateTool(client, args);
4104
+ return toolText("In an encrypted conversation, communicate can only list conversations or agents, or send text to this conversation.", true);
3889
4105
  }
3890
4106
  __name(communicate, "communicate");
3891
4107
  async function sealedSend(turn, roomId, text3, audience) {
@@ -3906,9 +4122,10 @@ ${JSON.stringify(result, null, 2)}`);
3906
4122
  notice: false,
3907
4123
  mls: mls2
3908
4124
  };
3909
- journal.save(record);
3910
- const sent2 = await publish(record);
4125
+ saveOutput(record);
4126
+ const sent2 = await publish(record, turn.retire.signal);
3911
4127
  if (sent2 !== "pending") journal.remove(messageId);
4128
+ else if (turn.retire.signal.aborted) await stopOutput(record);
3912
4129
  return sent2;
3913
4130
  })().catch((error) => {
3914
4131
  turn.said.delete(messageId);
@@ -3922,7 +4139,7 @@ ${JSON.stringify(result, null, 2)}`);
3922
4139
  async function admitFile(path) {
3923
4140
  if (typeof path !== "string" || !isAbsolute3(path)) return "send_file needs an absolute file path";
3924
4141
  try {
3925
- const real = await realpath5(path);
4142
+ const real = await realpath4(path);
3926
4143
  if (!roots.some((root) => real.startsWith(root.endsWith(sep) ? root : `${root}${sep}`))) return `${path} is outside the directories this agent may share from`;
3927
4144
  const info = await stat2(real);
3928
4145
  if (!info.isFile() || info.size < 1 || info.size > MAX_CANON_MEDIA_BYTES) return `${path} must be a file from 1 byte to 100 MiB`;
@@ -3932,60 +4149,6 @@ ${JSON.stringify(result, null, 2)}`);
3932
4149
  }
3933
4150
  }
3934
4151
  __name(admitFile, "admitFile");
3935
- async function attach(conversationId, turn, candidates) {
3936
- const attachments = [];
3937
- const flows = [];
3938
- const refused2 = [];
3939
- const seen = /* @__PURE__ */ new Set();
3940
- for (const candidate of candidates) {
3941
- const file = await admitFile(candidate);
3942
- if (typeof file === "string") {
3943
- refused2.push(typeof candidate === "string" ? basename3(candidate) : "a file");
3944
- continue;
3945
- }
3946
- if (seen.has(file.path)) continue;
3947
- seen.add(file.path);
3948
- if (attachments.length >= MAX_GROUP_PARTS - 1) {
3949
- refused2.push(basename3(file.path));
3950
- continue;
3951
- }
3952
- try {
3953
- const media = await client.getFileMediaPipeline(options.fetch);
3954
- const attachmentId = `${turn.id}-${seen.size}`;
3955
- const [prepared] = await media.prepare({ conversationId, attachmentId, files: [{
3956
- source: file.path,
3957
- descriptor: { mimeType: inferUploadMimeType(file.path), sizeBytes: file.size, fileName: basename3(file.path) }
3958
- }] }, { signal: turn.retire.signal });
3959
- attachments.push(prepared);
3960
- flows.push(`${conversationId}:upload:${attachmentId}`);
3961
- } catch (error) {
3962
- if (turn.retire.signal.aborted) break;
3963
- warn("file upload")(error);
3964
- refused2.push(basename3(file.path));
3965
- }
3966
- }
3967
- return { attachments, flows, refused: refused2 };
3968
- }
3969
- __name(attach, "attach");
3970
- const releaseUploads = /* @__PURE__ */ __name((attachments = []) => void (attachments.length && client.getFileMediaPipeline(options.fetch).then((media) => Promise.all(attachments.map((attachment) => attachment.uploadId && media.releasePreparedUpload(attachment.uploadId)))).catch(warn("release upload"))), "releaseUploads");
3971
- async function renew(flows, signal) {
3972
- const media = await client.getFileMediaPipeline(options.fetch);
3973
- const renewed = [];
3974
- for (const id of flows) {
3975
- let prepared;
3976
- const off = media.subscribe((flow) => {
3977
- if (flow.id === id && flow.status === "prepared") prepared = flow.files[0]?.attachment;
3978
- });
3979
- try {
3980
- await media.resume(id, { signal });
3981
- } finally {
3982
- off();
3983
- }
3984
- renewed.push(prepared);
3985
- }
3986
- return renewed;
3987
- }
3988
- __name(renew, "renew");
3989
4152
  async function recentHistory(current, turn) {
3990
4153
  const first = turn.inputs[0].payload.message;
3991
4154
  try {
@@ -3993,9 +4156,11 @@ ${JSON.stringify(result, null, 2)}`);
3993
4156
  const skip = new Set([...turn.inputs, ...current.queue].map(({ payload }) => payload.message.id));
3994
4157
  const at = Math.max(...turn.inputs.map(({ payload }) => recent.findIndex((message2) => message2.id === payload.message.id)));
3995
4158
  const before = /* @__PURE__ */ __name((message2, index) => message2.senderId === agentId || (at >= 0 ? index > at : !first.createdAt || String(message2.createdAt ?? "") < first.createdAt), "before");
3996
- const window = recent.filter((message2, index) => before(message2, index) && !message2.deleted && !skip.has(message2.id)).slice(0, HISTORY_WINDOW);
3997
- 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;
3998
- return window.filter((_, index) => !sameAudience || sameAudience[index]).reverse();
4159
+ const ownerId = driver.capabilities.ownerHistoryOnly ? (await client.getAgentMe()).ownerId : void 0;
4160
+ const allowed = /* @__PURE__ */ __name((message2) => ownerId === void 0 || message2.senderId === agentId || message2.senderId === ownerId, "allowed");
4161
+ const window = recent.filter((message2, index) => before(message2, index) && !message2.deleted && !skip.has(message2.id) && allowed(message2)).slice(0, HISTORY_WINDOW);
4162
+ const sameAudience = 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)));
4163
+ return window.filter((_, index) => sameAudience[index]).reverse();
3999
4164
  } catch (error) {
4000
4165
  warn("history read failed; the turn runs without it")(error);
4001
4166
  return [];
@@ -4015,8 +4180,9 @@ ${JSON.stringify(result, null, 2)}`);
4015
4180
  return void 0;
4016
4181
  }
4017
4182
  __name(outcomeOf, "outcomeOf");
4018
- async function deliver(current, turn, outcome, audience) {
4183
+ async function deliver(current, turn, outcome) {
4019
4184
  if (turn.reason === "shutdown") return turn.live.end("stopped");
4185
+ const audience = turn.mls;
4020
4186
  const result = outcome && "result" in outcome ? outcome.result : void 0;
4021
4187
  const stopped = /* @__PURE__ */ __name(() => turn.retire.signal.aborted, "stopped");
4022
4188
  const silent = !stopped() && (turn.silenced !== void 0 || !turn.reason && result?.reply === null);
@@ -4024,141 +4190,108 @@ ${JSON.stringify(result, null, 2)}`);
4024
4190
  const failed = !stopped() && !silent && !answered2;
4025
4191
  if (failed && outcome && "error" in outcome) warn("the runtime failed the turn")(outcome.error);
4026
4192
  let text3 = answered2 ? result.reply : failed ? FAILURE_NOTICE : null;
4027
- let attachments = [];
4028
- let flows = [];
4029
4193
  let items = [];
4030
4194
  const resultFiles = answered2 && Array.isArray(result.files) ? result.files : [];
4031
- if (answered2 && turn.mls && carriesItems(turn.mls)) {
4195
+ if (answered2 && carriesItems(audience)) {
4032
4196
  const admitted = await admitItems(current.id, turn, [...turn.outbox, ...resultFiles]);
4033
4197
  items = admitted.items;
4034
4198
  if (admitted.refused.length) text3 = `${text3}
4035
4199
 
4036
4200
  (Could not attach ${admitted.refused.join(", ")}.)`;
4037
- } else if (answered2 && turn.mls && resultFiles.length) {
4201
+ } else if (answered2 && resultFiles.length) {
4038
4202
  text3 = `${text3}
4039
4203
 
4040
- (${ENCRYPTED_FILES}: ${resultFiles.map((file) => typeof file === "string" ? basename3(file) : "a file").join(", ")}.)`;
4041
- } else if (answered2 && !turn.mls) {
4042
- const files = await attach(current.id, turn, [...turn.outbox, ...resultFiles]);
4043
- ({ attachments, flows } = files);
4044
- if (files.refused.length) text3 = `${text3}
4045
-
4046
- (Could not attach ${files.refused.join(", ")}.)`;
4204
+ (${ENCRYPTED_FILES}: ${resultFiles.map((file) => typeof file === "string" ? basename4(file) : "a file").join(", ")}.)`;
4047
4205
  }
4048
4206
  let sent;
4049
4207
  if (text3 !== null && !stopped()) {
4050
4208
  const kind = failed ? "failed" : "final";
4209
+ const replyAuthority = await replyGrant(current.id, turn.inputs.at(-1).payload.message.id, stopped);
4051
4210
  const record = {
4052
4211
  version: 1,
4053
4212
  turnId: turn.id,
4054
4213
  conversationId: current.id,
4055
4214
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4056
- audience,
4215
+ audience: null,
4057
4216
  inputs: turn.inputs.map((input) => input.id),
4058
4217
  text: text3,
4059
- ...attachments.length ? { attachments, flows } : {},
4060
4218
  ...items.length ? { items } : {},
4061
- options: outputOptions(
4062
- turn.id,
4063
- turn.inputs.at(-1).payload,
4064
- kind,
4065
- !turn.mls && actedForOwner(turn),
4066
- turn.mls ? keyed().text(kind, current.id, turn.id) : void 0
4067
- ),
4219
+ options: { messageId: keyed().text(kind, current.id, turn.id), ...replyAuthority ? { replyAuthority } : {} },
4068
4220
  notice: failed,
4069
- ...turn.mls ? { mls: turn.mls } : {}
4221
+ mls: audience
4070
4222
  };
4071
- journal.save(record);
4223
+ saveOutput(record);
4072
4224
  sent = await publish(record, turn.retire.signal);
4073
- attachments = record.attachments ?? [];
4074
- if (sent === "pending" && !stopped()) return turn.live.end(turn.reason && (!turn.mls || STOPS.includes(turn.reason)) ? "stopped" : "ended");
4225
+ if (sent === "pending" && !stopped()) return turn.live.end(turn.reason && STOPS.includes(turn.reason) ? "stopped" : "ended");
4226
+ if (sent === "pending") await stopOutput(record);
4075
4227
  }
4076
4228
  turn.live.end(sent === "delivered" ? "answered" : stopped() ? "stopped" : "ended");
4077
- releaseUploads(attachments);
4078
4229
  if (failed && outcome && "error" in outcome && outcome.error instanceof DriverSessionError) {
4079
4230
  await resetSession(turn.session).catch(warn("session reset"));
4080
4231
  }
4081
- if (turn.mls && (turn.reason === "audience" || turn.reason === "membership")) {
4232
+ if (turn.reason === "audience" || turn.reason === "membership") {
4082
4233
  await resetSession(turn.session).catch(warn("session reset"));
4083
4234
  if (current.session === turn.session) current.session = void 0;
4084
4235
  }
4085
- if (silent && !turn.mls) reportNoReplyOutcome(client, { conversationId: current.id, messageId: turn.inputs.at(-1).payload.message.id, ...turn.silenced });
4086
4236
  const reason = stopped() ? turn.reason : silent ? "silent" : answered2 ? "answered" : "failed";
4087
4237
  if (await settle(turn.inputs.map((input) => input.id), reason) && sent !== void 0) journal.remove(turn.id);
4088
4238
  }
4089
4239
  __name(deliver, "deliver");
4090
- const actedForOwner = /* @__PURE__ */ __name((turn) => turn.inputs.every(({ payload: { message: message2 } }) => message2.isOwner === true || message2.senderType === "ai_agent" && turn.trusted.has(message2.id)), "actedForOwner");
4091
- async function sendWithFiles(record, text3, options2, signal) {
4092
- const root = options2.messageId;
4093
- const register = /* @__PURE__ */ __name((attachments, replaces) => {
4094
- const bodies = [
4095
- ...attachments.map((attachment) => ({ text: "", contentType: attachment.kind, attachments: [attachment] })),
4096
- ...(text3 ? splitTextByUtf8Bytes(text3) : []).map((part) => ({ text: part }))
4097
- ];
4098
- const parts = bodies.map(({ text: partText, ...body }, index) => ({ text: partText, options: {
4099
- ...options2,
4100
- ...body,
4101
- messageId: `${root}-part-${index + 1}`,
4102
- metadata: buildChunkedMessagePartMetadata({ groupId: root, metadata: options2.metadata, index: index + 1, count: bodies.length })
4103
- } }));
4104
- return client.prepareMessageGroup(record.conversationId, root, parts, { signal, ...replaces ? { operationId: sha2563(JSON.stringify([root, ...attachments.map((attachment) => attachment.uploadId)])), replacesOperationId: replaces } : {} }).then((group2) => ({ ...group2, count: parts.length }));
4105
- }, "register");
4106
- const original = await register(record.attachments);
4107
- let group = original;
4108
- const cancel = /* @__PURE__ */ __name(() => {
4109
- void client.cancelMessageGroup(group.id).catch(() => void 0);
4110
- }, "cancel");
4111
- signal?.addEventListener("abort", cancel, { once: true });
4112
- try {
4113
- for (; ; ) {
4114
- try {
4115
- for (let index = 0; index < group.count; index += 1) await group.sendPart(index, { signal });
4116
- return;
4117
- } catch (error) {
4118
- if (!record.flows || !EXPIRED_UPLOAD.includes(error.code ?? "")) throw error;
4119
- const renewed = await renew(record.flows, signal).catch((failure) => {
4120
- throw failure.retryable === false ? error : failure;
4121
- });
4122
- if (renewed.some((attachment) => !attachment) || renewed.every((attachment, index) => attachment.uploadId === record.attachments[index].uploadId)) throw error;
4123
- record.attachments = renewed;
4124
- group = await register(record.attachments, original.id);
4125
- }
4126
- }
4127
- } finally {
4128
- signal?.removeEventListener("abort", cancel);
4129
- }
4130
- }
4131
- __name(sendWithFiles, "sendWithFiles");
4240
+ const forOwner2 = /* @__PURE__ */ __name((turn) => turn.inputs.every(({ payload: { message: message2 } }) => message2.isOwner === true || message2.senderType === "ai_agent" && turn.trusted.has(message2.id)), "forOwner");
4132
4241
  async function publish(record, retire) {
4133
- const sent = await send(record.conversationId, record.text, record.options, retire, record);
4134
- if (sent !== "refused" || record.notice || record.mls || record.errand) return sent;
4135
- const notice = {
4136
- ...record.options,
4137
- messageId: `${record.options.messageId}:short`,
4138
- metadata: { ...record.options.metadata, replyBehavior: "suppress_auto_reply" }
4139
- };
4140
- return await send(record.conversationId, STOPS_SHORT_NOTICE, notice, retire) === "pending" ? "pending" : "refused";
4242
+ return send(record.text, record.options, record.mls, retire, record);
4141
4243
  }
4142
4244
  __name(publish, "publish");
4143
- async function send(conversationId, text3, options2, retire, record, mls2 = record?.mls) {
4245
+ async function send(text3, options2, mls2, retire, record) {
4246
+ const grant = options2.replyAuthority;
4247
+ const parts = encryptedMessages(mls2, text3, options2, record);
4144
4248
  const sent = await (async () => {
4145
4249
  for (let attempt = 0; ; attempt += 1) {
4146
- const files = record?.attachments?.length ?? 0;
4147
- 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));
4250
+ const result = await untilHalt(sendEncryptedItems(mls2, record?.items ?? [], retire).then(async () => {
4251
+ for (const part of parts) await sendEncrypted(mls2, part.messageId, part.text, retire, grant && { ...grant, turnId: options2.messageId, complete: part.last });
4252
+ }).then(() => "delivered", (error) => error));
4148
4253
  if (result === "delivered" || result === "pending") return result;
4149
- if (retire?.aborted || isPendingCanonOperation(result)) return "pending";
4150
- if (mls2 ? result.retryable === false : !isRetryableCanonDeliveryError(result)) {
4254
+ if (retire?.aborted || isPendingCanonOperation(result) && parts.length < 2) return "pending";
4255
+ if (result.retryable === false) {
4151
4256
  warn("Canon refused a reply")(result);
4152
4257
  return "refused";
4153
4258
  }
4154
4259
  if (await untilHalt(pause2(Math.min(3e4, 1e3 * 2 ** attempt))) === "pending") return "pending";
4155
4260
  }
4156
4261
  })();
4157
- if (mls2) forgetGuard(mls2, options2.messageId, sent);
4262
+ for (const part of parts) forgetGuard(mls2, part.messageId, sent);
4158
4263
  return sent;
4159
4264
  }
4160
4265
  __name(send, "send");
4161
- async function sendEncrypted(audience, messageId, text3, signal) {
4266
+ const partsOf = /* @__PURE__ */ __name((text3, options2) => encryptedParts(text3, options2.replyAuthority ? MAX_REPLY_PARTS : MAX_GROUP_PARTS), "partsOf");
4267
+ function saveOutput(record) {
4268
+ record.parts = partsOf(record.text, record.options).length;
4269
+ journal.save(record);
4270
+ }
4271
+ __name(saveOutput, "saveOutput");
4272
+ function encryptedMessages(mls2, text3, options2, record) {
4273
+ const all = record && record.parts === void 0 ? [fitEncrypted(text3)] : partsOf(text3, options2);
4274
+ return all.slice(0, record?.parts).map((part, index) => ({
4275
+ text: part,
4276
+ last: index === all.length - 1,
4277
+ messageId: index ? keyed().text("part", mls2.conversationId, options2.messageId, String(index)) : options2.messageId
4278
+ }));
4279
+ }
4280
+ __name(encryptedMessages, "encryptedMessages");
4281
+ async function stopOutput(record) {
4282
+ let queued2 = 0;
4283
+ for (const [index, { messageId }] of encryptedMessages(record.mls, record.text, record.options, record).entries()) {
4284
+ const status = (await endpoint.getOperation(encryptedOperation(record.mls, messageId)))?.status;
4285
+ if (status && status !== "succeeded" && status !== "rejected") queued2 = index + 1;
4286
+ }
4287
+ if (queued2) {
4288
+ const { items: _items, errand: _errand, ...rest } = record;
4289
+ journal.save({ ...rest, turnId: `${record.turnId}:stopped`, inputs: [], parts: queued2 });
4290
+ }
4291
+ journal.remove(record.turnId);
4292
+ }
4293
+ __name(stopOutput, "stopOutput");
4294
+ async function sendEncrypted(audience, messageId, text3, signal, replyAuthority) {
4162
4295
  const operationId = encryptedOperation(audience, messageId);
4163
4296
  if (!guards.has(operationId)) {
4164
4297
  guards.set(operationId, endpoint.registerDeliveryGuard(operationId, () => assertAudience(audience).then(() => true, (error) => {
@@ -4169,11 +4302,38 @@ ${JSON.stringify(result, null, 2)}`);
4169
4302
  if (!await endpoint.getOperation(operationId)) await assertAudience(audience);
4170
4303
  signal?.throwIfAborted();
4171
4304
  await endpoint.executeResult(
4172
- { action: "send_message", body: { conversationId: audience.conversationId, messageId, text: fitEncrypted(text3), contentType: "text" } },
4305
+ { action: "send_message", body: {
4306
+ conversationId: audience.conversationId,
4307
+ messageId,
4308
+ text: text3,
4309
+ contentType: "text",
4310
+ ...replyAuthority ? { replyAuthority } : {}
4311
+ } },
4173
4312
  { operationId, guardId: operationId }
4174
4313
  );
4175
4314
  }
4176
4315
  __name(sendEncrypted, "sendEncrypted");
4316
+ async function replyGrant(roomId, source, stopped) {
4317
+ for (let attempt = 0; !options.relay; attempt += 1) {
4318
+ try {
4319
+ await authorize();
4320
+ if (!outboundClosed) return void 0;
4321
+ const { changes } = await endpoint.query(
4322
+ "admit_messages",
4323
+ { conversationId: roomId, messageIds: [source] }
4324
+ );
4325
+ return changes[0]?.data?.replyAuthority;
4326
+ } catch (error) {
4327
+ if (attempt >= REPLY_GRANT_RETRIES || stopped() || halt.signal.aborted) {
4328
+ warn("no reply authority")(error);
4329
+ return void 0;
4330
+ }
4331
+ await untilHalt(pause2(1e3 * 2 ** attempt));
4332
+ }
4333
+ }
4334
+ return void 0;
4335
+ }
4336
+ __name(replyGrant, "replyGrant");
4177
4337
  async function sendEncryptedItems(audience, items, signal) {
4178
4338
  if (!items.length) return;
4179
4339
  const media = await client.getFileMediaPipeline(options.fetch);
@@ -4203,20 +4363,20 @@ ${JSON.stringify(result, null, 2)}`);
4203
4363
  for (const candidate of candidates) {
4204
4364
  const file = await admitFile(candidate);
4205
4365
  if (typeof file === "string") {
4206
- refused2.push(typeof candidate === "string" ? basename3(candidate) : "a file");
4366
+ refused2.push(typeof candidate === "string" ? basename4(candidate) : "a file");
4207
4367
  continue;
4208
4368
  }
4209
4369
  if (seen.has(file.path)) continue;
4210
4370
  seen.add(file.path);
4211
4371
  const mimeType = inferUploadMimeType(file.path);
4212
- if (items.length >= MAX_GROUP_PARTS - 1 || !resolveAllowedUploadMimeType({ mimeType, fileName: basename3(file.path) })) {
4213
- refused2.push(basename3(file.path));
4372
+ if (items.length >= MAX_GROUP_PARTS - 1 || !resolveAllowedUploadMimeType({ mimeType, fileName: basename4(file.path) })) {
4373
+ refused2.push(basename4(file.path));
4214
4374
  continue;
4215
4375
  }
4216
4376
  items.push({
4217
4377
  path: file.path,
4218
4378
  messageId: keyed().text("item", roomId, turn.id, String(items.length)),
4219
- fileName: basename3(file.path),
4379
+ fileName: basename4(file.path),
4220
4380
  mimeType,
4221
4381
  sizeBytes: file.size
4222
4382
  });
@@ -4249,8 +4409,9 @@ ${JSON.stringify(result, null, 2)}`);
4249
4409
  messages: Object.freeze(messages),
4250
4410
  history: Object.freeze(history.map((message2) => inputMessage(message2))),
4251
4411
  ...turn.notes.length ? { ownerNotes: Object.freeze(turn.notes.map((note) => note.text)) } : {},
4252
- ...turn.contexts.length ? { errands: Object.freeze(turn.contexts.map(({ record }) => errandFacts(record))) } : {},
4253
- ...turn.errand ? { errand: errandFacts(turn.errand.record) } : {}
4412
+ ...turn.contexts.length ? { errands: Object.freeze(turn.contexts.map((record) => errandFacts(record))) } : {},
4413
+ ...turn.errand ? { errand: errandFacts(turn.errand.record) } : {},
4414
+ ...turn.facts ? { roomFacts: turn.facts } : {}
4254
4415
  });
4255
4416
  const line = /* @__PURE__ */ __name((message2, mark = "") => promptLine(message2, message2.senderId === agentId ? "You" : message2.senderName ?? message2.senderId, turn.media.get(message2.id), mark), "line");
4256
4417
  const open4 = /* @__PURE__ */ __name(() => turn.open && !turn.controller.signal.aborted, "open");
@@ -4259,25 +4420,21 @@ ${JSON.stringify(result, null, 2)}`);
4259
4420
  session: turn.session,
4260
4421
  // §12.6: an errand turn's prompt is its errand; the owner's message is only its input's record.
4261
4422
  prompt: turn.errand ? errandPrompt(turn.errand) : [
4423
+ ...turn.facts ? [...factsPrompt(turn.facts, kind), ""] : [],
4262
4424
  ...turn.notes.flatMap((note) => [NOTE_OPEN, defused(note.text), NOTE_CLOSE, ""]),
4263
- ...turn.contexts.flatMap((context) => [...contextPrompt(context, !!turn.mls), ""]),
4264
- ...history.length ? ["Earlier in this conversation (context, not new requests):", ...history.map((message2) => line(message2)), "", "New messages:"] : turn.notes.length || turn.contexts.length ? ["New messages:"] : [],
4425
+ ...turn.contexts.flatMap((record) => [...contextPrompt(record), ""]),
4426
+ ...history.length ? ["Earlier in this conversation (context, not new requests):", ...history.map((message2) => line(message2)), "", "New messages:"] : turn.facts || turn.notes.length || turn.contexts.length ? ["New messages:"] : [],
4265
4427
  ...turn.inputs.map(({ payload: { message: message2 } }) => line(message2, message2.isOwner === true ? OWNER_MARK : trustedSender(message2) ? TRUSTED_MARK : ""))
4266
4428
  ].join("\n"),
4267
4429
  input,
4268
4430
  files: turn.inputs.flatMap(({ payload: { message: message2 } }) => turn.media.get(message2.id)?.files.map((file) => file.path) ?? []),
4269
4431
  signal: turn.controller.signal,
4270
- onText: /* @__PURE__ */ __name((snapshot) => {
4271
- if (live()) {
4272
- progress(turn);
4273
- turn.live.text(snapshot);
4274
- }
4432
+ // Text and activity are progress (RUN-12), and nothing of them is published (§6).
4433
+ onText: /* @__PURE__ */ __name(() => {
4434
+ if (live()) progress(turn);
4275
4435
  }, "onText"),
4276
- onActivity: /* @__PURE__ */ __name((event) => {
4277
- if (live()) {
4278
- progress(turn);
4279
- turn.live.activity(event);
4280
- }
4436
+ onActivity: /* @__PURE__ */ __name(() => {
4437
+ if (live()) progress(turn);
4281
4438
  }, "onActivity"),
4282
4439
  ask: /* @__PURE__ */ __name((request) => live() ? ask(roomId, turn, input.sender, request) : Promise.reject(turnEnded2()), "ask"),
4283
4440
  state: driverState(turn.session, open4),
@@ -4296,7 +4453,7 @@ ${JSON.stringify(result, null, 2)}`);
4296
4453
  loseMembership(room(roomId));
4297
4454
  throw new Error("The agent is no longer a member of this conversation");
4298
4455
  }
4299
- if (turn.mls && await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4456
+ if (await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4300
4457
  if (!turn.controller.signal.aborted) {
4301
4458
  void dispose(queued(turn), "settle", "mls-obsolete-audience");
4302
4459
  abort(turn, "audience");
@@ -4319,68 +4476,32 @@ ${JSON.stringify(result, null, 2)}`);
4319
4476
  if (turn.errand && !turn.errand.reports) return Promise.resolve({ status: "cancelled" });
4320
4477
  const signal = AbortSignal.any([turn.controller.signal, turn.ended.signal]);
4321
4478
  turn.asking += 1;
4322
- if (!turn.mls) turn.live.waiting(true);
4323
4479
  const answer = (async () => {
4324
4480
  try {
4325
- if (turn.mls && (!carriesCards(turn.mls) || !mlsAsks)) throw new Error("approvals and questions need an encrypted conversation created for them");
4481
+ if (!turn.mls || !carriesCards(turn.mls) || !mlsAsks) throw new Error("approvals and questions need an encrypted conversation created for them");
4326
4482
  const ownerId = await (owner ??= client.getAgentMe().then((me) => me.ownerId, (error) => {
4327
4483
  owner = void 0;
4328
4484
  throw error;
4329
4485
  }));
4330
4486
  const secret = request.kind === "text" && request.secret === true;
4331
4487
  const responder = secret || sender.type !== "human" ? ownerId : sender.id;
4332
- const requestId = turn.mls ? keyed().request(roomId, turn.id, request.id) : `ask-${sha2563(JSON.stringify([turn.id, request.id])).slice(0, 32)}`;
4333
- const expiresAt = Date.now() + askExpiryMs;
4334
- if (turn.mls) {
4335
- return await mlsAsks.ask({
4336
- conversationId: roomId,
4337
- session: turn.session,
4338
- requestId,
4339
- responder,
4340
- ownerId,
4341
- request,
4342
- expiresAtMs: expiresAt,
4343
- audience: /* @__PURE__ */ __name(() => turn.mls, "audience"),
4344
- signal,
4345
- recheck: /* @__PURE__ */ __name(() => checkAudience(room(roomId)), "recheck"),
4346
- pending: /* @__PURE__ */ __name(() => turn.live.waiting(true), "pending"),
4347
- // Stop, and a new member (whose card is void for every device), withdraw a card, and so does the turn's end;
4348
- // shutdown leaves it to restart recovery, and an agent no longer in the room can send nothing.
4349
- ending: /* @__PURE__ */ __name(() => turn.reason === "shutdown" ? "keep" : turn.reason === "membership" ? "drop" : turn.reason && STOPS.includes(turn.reason) ? "stopped" : "turn_ended", "ending")
4350
- });
4351
- }
4352
- let manager = approvals.get(roomId);
4353
- if (!manager) approvals.set(roomId, manager = new ApprovalManager(client, agentId, ownerId));
4354
- const options2 = { requestId, expiresAt, signal };
4355
- if (request.kind === "approve") {
4356
- let created = false;
4357
- const result2 = await manager.requestApproval(roomId, approvalScope(request), {}, {
4358
- toolSummary: request.title,
4359
- responseUserId: responder,
4360
- turnId: turn.id,
4361
- ...request.detail ? { details: [{ label: "Details", value: request.detail.slice(0, 500) }] } : {},
4362
- ...request.risk ? { riskLevel: request.risk, risk: request.risk } : {},
4363
- ignoreSessionRules: request.allowSession !== true,
4364
- allowSessionRule: request.allowSession === true
4365
- }, { ...options2, onCreated: /* @__PURE__ */ __name(() => {
4366
- created = true;
4367
- }, "onCreated") });
4368
- if (created && !result2.respondedBy) return { status: "expired" };
4369
- return { status: "answered", by: result2.respondedBy ?? ownerId, allow: result2.decision === "allow", scope: !created || result2.sessionRule ? "session" : "once" };
4370
- }
4371
- const choices = request.kind === "choose" ? request.options.map(({ id, label }) => ({ label, value: id })) : void 0;
4372
- const result = await manager.request("input", roomId, {
4373
- kind: secret ? "secret" : "clarify",
4374
- prompt: request.question,
4375
- responseUserId: responder,
4376
- turnId: turn.id,
4377
- ...secret ? { sensitive: true } : {},
4378
- ...request.kind === "choose" && request.multiple ? { questions: [{ id: "choice", question: request.question, choices, multiSelect: true }] } : choices ? { choices } : {}
4379
- }, options2);
4380
- if (result.status !== "submitted") return { status: result.status === "timeout" ? "expired" : "cancelled" };
4381
- if (request.kind !== "choose") return { status: "answered", by: responder, text: result.value };
4382
- const picked = result.answers ? Object.values(result.answers).flatMap((entry) => entry.answers) : [result.value];
4383
- return { status: "answered", by: responder, choices: picked.filter((id) => request.options.some((option) => option.id === id)) };
4488
+ const requestId = keyed().request(roomId, turn.id, request.id);
4489
+ return await mlsAsks.ask({
4490
+ conversationId: roomId,
4491
+ session: turn.session,
4492
+ requestId,
4493
+ responder,
4494
+ ownerId,
4495
+ request,
4496
+ expiresAtMs: Date.now() + askExpiryMs,
4497
+ audience: /* @__PURE__ */ __name(() => turn.mls, "audience"),
4498
+ signal,
4499
+ recheck: /* @__PURE__ */ __name(() => checkAudience(room(roomId)), "recheck"),
4500
+ pending: /* @__PURE__ */ __name(() => turn.live.waiting(true), "pending"),
4501
+ // Stop, and a new member (whose card is void for every device), withdraw a card, and so does the turn's end;
4502
+ // shutdown leaves it to restart recovery, and an agent no longer in the room can send nothing.
4503
+ ending: /* @__PURE__ */ __name(() => turn.reason === "shutdown" ? "keep" : turn.reason === "membership" ? "drop" : turn.reason && STOPS.includes(turn.reason) ? "stopped" : "turn_ended", "ending")
4504
+ });
4384
4505
  } catch (error) {
4385
4506
  if (signal.aborted) throw turnEnded2();
4386
4507
  warn("ask could not be put to anyone")(error);
@@ -4401,7 +4522,7 @@ ${JSON.stringify(result, null, 2)}`);
4401
4522
  const file = stateFile(session);
4402
4523
  let data = {};
4403
4524
  try {
4404
- data = JSON.parse(readFileSync5(file, "utf8"));
4525
+ data = JSON.parse(readFileSync6(file, "utf8"));
4405
4526
  } catch {
4406
4527
  }
4407
4528
  const save = /* @__PURE__ */ __name((next) => {
@@ -4433,20 +4554,13 @@ ${JSON.stringify(result, null, 2)}`);
4433
4554
  if (consumed.status !== "signal") return;
4434
4555
  const signal = consumed.signal;
4435
4556
  const turn = current.turn;
4436
- if (signal === "stop_and_drop" || signal === "new_session") {
4437
- const dropped = current.queue.splice(0);
4438
- for (const { payload } of current.mls ? [] : dropped) {
4439
- void client.updateMessageDisposition(current.id, payload.message.id, "rejected").catch(warn("rejected disposition"));
4440
- }
4441
- void dispose(dropped, "cancel", "stop-and-clear");
4442
- }
4557
+ if (signal === "stop_and_drop" || signal === "new_session") void dispose(current.queue.splice(0), "cancel", "stop-and-clear");
4443
4558
  if (turn) {
4444
4559
  void dispose(turn.inputs, "cancel", "stopped-before-submit");
4445
4560
  abort(turn, signal === "interrupt" ? "stop" : signal === "stop_and_drop" ? "drop" : "new_session");
4446
4561
  }
4447
- (turn?.live ?? livePublisher(current, void 0, current.mls ? "presence" : "full")).end("stopped");
4562
+ (turn?.live ?? livePublisher(current, void 0)).end("stopped");
4448
4563
  if (signal === "new_session") {
4449
- approvals.delete(current.id);
4450
4564
  const sessions = /* @__PURE__ */ new Set([
4451
4565
  turn?.session ?? current.session ?? current.id,
4452
4566
  current.session ?? (turn?.mls ? mlsSession(current.id, mlsAudienceEpoch(turn.mls)) : current.id)
@@ -4481,25 +4595,19 @@ ${JSON.stringify(result, null, 2)}`);
4481
4595
  const target = room(roomId);
4482
4596
  const work = (async () => {
4483
4597
  for (const record of records.filter((candidate) => candidate.conversationId === roomId)) {
4484
- if (record.mls) target.mls = true;
4485
4598
  await redeliver(record).catch(warn("journaled reply kept for reconciliation"));
4486
4599
  }
4487
4600
  const inputs = orphans.filter(({ event }) => event.conversationId === roomId).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.event.id.localeCompare(b.event.id));
4488
4601
  if (inputs.length) {
4489
4602
  const id = turnId(roomId, inputs.map(({ event, handoffGeneration }) => ({ id: event.id, generation: handoffGeneration ?? "" })));
4490
- const trigger = inputs.at(-1).event.data;
4491
4603
  const mode = await endpoint.getConversationContentMode(roomId);
4492
- if (mode !== "plaintext") target.mls = true;
4493
- if (mode !== "plaintext" && (!mlsOn || mode !== "mls-dev-text")) refuse(roomId, mlsOn ? "protocol" : "no-device");
4604
+ if (mode !== "mls-dev-text" || !mlsOn) refuse(roomId, mode === "plaintext" ? "plaintext" : mlsOn ? "protocol" : "no-device");
4494
4605
  const mls2 = mode === "mls-dev-text" && mlsOn ? (await currentAudience(roomId)).audience : void 0;
4495
4606
  const legacy = `${id}:restart`;
4496
4607
  const messageId = mls2 && !await endpoint.getOperation(encryptedOperation(mls2, legacy)) ? keyed().text("restart", roomId, id) : legacy;
4497
- if (mode !== "plaintext" && !mls2) await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4498
- else if (await send(roomId, RESTART_NOTICE, outputOptions(id, trigger, "restart", false, messageId), void 0, void 0, mls2) !== "pending") {
4499
- await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4500
- }
4608
+ if (!mls2 || await send(RESTART_NOTICE, { messageId }, mls2) !== "pending") await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4501
4609
  }
4502
- livePublisher(target, void 0, target.mls ? "presence" : "full").end("ended");
4610
+ livePublisher(target, void 0).end("ended");
4503
4611
  })().catch(warn("restart recovery"));
4504
4612
  target.recovering = work.finally(() => {
4505
4613
  target.recovering = void 0;
@@ -4509,24 +4617,16 @@ ${JSON.stringify(result, null, 2)}`);
4509
4617
  }
4510
4618
  __name(recover, "recover");
4511
4619
  async function redeliver(record) {
4512
- if (record.mls) {
4513
- if (!mlsOn) return refuse(record.conversationId, "no-device");
4514
- } else if (!await client.getMessageOperation(record.conversationId, record.options.messageId)) {
4515
- const members = audienceOf(await client.getConversation(record.conversationId));
4516
- try {
4517
- assertCompletedOutputAudience(record, members, "agent runner");
4518
- } catch (error) {
4519
- if (!record.errand) throw error;
4520
- return finishErrandOutput(record, "refused", "the conversation's members changed before it was sent");
4521
- }
4620
+ if (!record.mls) {
4621
+ if (record.errand) return finishErrandOutput(record, "refused", "its conversation is not encrypted");
4622
+ if (await settle(record.inputs, "plaintext-output-dropped")) journal.remove(record.turnId);
4623
+ return;
4522
4624
  }
4625
+ if (!mlsOn) return refuse(record.conversationId, "no-device");
4523
4626
  const sent = await publish(record);
4524
4627
  if (sent === "pending") return;
4525
4628
  if (record.errand) return finishErrandOutput(record, sent);
4526
- if (await settle(record.inputs, "recovered")) {
4527
- journal.remove(record.turnId);
4528
- releaseUploads(record.attachments);
4529
- }
4629
+ if (await settle(record.inputs, "recovered")) journal.remove(record.turnId);
4530
4630
  }
4531
4631
  __name(redeliver, "redeliver");
4532
4632
  function pendingNotes(roomId) {
@@ -4624,15 +4724,15 @@ ${JSON.stringify(result, null, 2)}`);
4624
4724
  turnId: turnId2,
4625
4725
  conversationId,
4626
4726
  createdAt,
4627
- audience: target.audience,
4727
+ audience: null,
4628
4728
  inputs: [],
4629
4729
  text: parsed.text,
4630
- options: target.mls ? { messageId: keyed().text("final", conversationId, turnId2) } : { messageId: `${turnId2}:final`, metadata: { turnId: turnId2, turnSemantics: "turn_complete" } },
4730
+ options: { messageId: keyed().text("final", conversationId, turnId2) },
4631
4731
  notice: false,
4632
4732
  errand: id,
4633
- ...target.mls ? { mls: target.mls } : {}
4733
+ mls: target.mls
4634
4734
  };
4635
- journal.save(record);
4735
+ saveOutput(record);
4636
4736
  errands.put({ ...base, status: "sending", updatedAt });
4637
4737
  const delivery = publish(record).then((sent) => {
4638
4738
  if (sent !== "pending") finishErrandOutput(record, sent);
@@ -4651,9 +4751,9 @@ ${JSON.stringify(result, null, 2)}`);
4651
4751
  if (!conversation) return "Canon does not show the agent that conversation";
4652
4752
  if (!conversation.memberIds?.includes(agentId)) return "the agent is not a member of that conversation";
4653
4753
  const mode = await endpoint.getConversationContentMode(roomId).catch(missing);
4654
- if (mode === "plaintext") return { audience: audienceOf(conversation) };
4754
+ if (mode === "plaintext") return "it is not encrypted, and the agent takes part only in encrypted conversations";
4655
4755
  if (mode !== "mls-dev-text" || !mlsOn) return "it is an encrypted conversation this runner cannot take part in";
4656
- return { audience: null, ...kind === "post" ? { mls: (await currentAudience(roomId)).audience } : {} };
4756
+ return kind === "post" ? { mls: (await currentAudience(roomId)).audience } : {};
4657
4757
  }
4658
4758
  __name(errandRoom, "errandRoom");
4659
4759
  function finishErrandOutput(record, sent, reason = "Canon did not accept it") {
@@ -4696,8 +4796,8 @@ ${JSON.stringify(result, null, 2)}`);
4696
4796
  ...record.origin ? { origin: record.origin } : {}
4697
4797
  }), "errandFacts");
4698
4798
  function errandPrompt({ record, target, origin, reports }) {
4699
- const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation.` : `the ${target.mls ? "encrypted " : ""}conversation ${target.room}. Send to it with communicate message_existing.`;
4700
- const report = reports ? `your final answer is your report to your owner, posted in the conversation ${origin.room}.` : origin ? `your final answer is your report to your owner, and it stays on their machine: the conversation ${origin.room} is not encrypted, and nothing of an errand to an encrypted conversation may go there.` : "your final answer is your report to your owner, and it stays on their machine.";
4799
+ const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation.` : `the encrypted conversation ${target.room}. Send to it with communicate message_existing.`;
4800
+ const report = reports ? `your final answer is your report to your owner, posted in the conversation ${origin.room}.` : "your final answer is your report to your owner, and it stays on their machine.";
4701
4801
  return [
4702
4802
  `${ERRAND_OPEN} Your owner sends you on an errand, from the machine you run on. It is not a message in any conversation, and no one else sees it.`,
4703
4803
  `Why: ${defused(record.why)}`,
@@ -4709,8 +4809,8 @@ ${JSON.stringify(result, null, 2)}`);
4709
4809
  ].join("\n");
4710
4810
  }
4711
4811
  __name(errandPrompt, "errandPrompt");
4712
- function contextPrompt({ record, encryptedOrigin }, encryptedRoom) {
4713
- const report = !record.origin ? "Your owner follows this errand on their machine; it has no conversation to report to." : encryptedOrigin || !encryptedRoom ? `You may report to your owner in the ${encryptedOrigin ? "encrypted " : ""}conversation ${record.origin} with communicate.` : `Nothing from this encrypted conversation may go to ${record.origin}, which is not encrypted; your owner sees on their machine that the errand was answered.`;
4812
+ function contextPrompt(record) {
4813
+ const report = !record.origin ? "Your owner follows this errand on their machine; it has no conversation to report to." : `You may report to your owner in the encrypted conversation ${record.origin} with communicate.`;
4714
4814
  return [
4715
4815
  `${ERRAND_OPEN} Your owner sent you into this conversation on an errand, from the machine you run on. No one here sees this note.`,
4716
4816
  `Why: ${defused(record.why)}`,
@@ -4728,7 +4828,7 @@ ${JSON.stringify(result, null, 2)}`);
4728
4828
  }
4729
4829
  __name(openErrands, "openErrands");
4730
4830
  function replied(turn) {
4731
- for (const { record } of turn.contexts) {
4831
+ for (const record of turn.contexts) {
4732
4832
  const known = latest(record.id);
4733
4833
  if (!known || !openErrand(known)) continue;
4734
4834
  try {
@@ -4753,7 +4853,7 @@ ${JSON.stringify(result, null, 2)}`);
4753
4853
  const turn = newTurn([], errandTurnId(record.id), `errand:${record.id}`);
4754
4854
  holder.turn = turn;
4755
4855
  errandTurns.set(record.id, turn);
4756
- (turn.live = origin ? livePublisher(origin, turn, "presence") : SILENT).open();
4856
+ (turn.live = origin ? livePublisher(origin, turn) : SILENT).open();
4757
4857
  const done = executeErrand(holder, turn, record).catch(warn(`errand ${record.id}`)).finally(() => {
4758
4858
  turn.live.end("ended");
4759
4859
  if (holder.turn === turn) holder.turn = void 0;
@@ -4788,7 +4888,6 @@ ${JSON.stringify(result, null, 2)}`);
4788
4888
  if (typeof setup === "string") return void saveErrand({ ...known, status: "refused", reason: setup });
4789
4889
  turn.errand = setup;
4790
4890
  turn.mls = setup.origin?.mls;
4791
- if (turn.mls) current.mls = true;
4792
4891
  const message2 = {
4793
4892
  id: `${turn.id}-errand`,
4794
4893
  senderId: setup.owner,
@@ -4831,16 +4930,15 @@ ${JSON.stringify(result, null, 2)}`);
4831
4930
  if (record.target === "room") {
4832
4931
  const found = await errandRoom(record.to, "post");
4833
4932
  if (typeof found === "string") return `its recipient: ${found}`;
4834
- target = { room: record.to, ...found.mls ? { mls: found.mls } : {} };
4933
+ target = { room: record.to };
4835
4934
  }
4836
4935
  let origin;
4837
4936
  if (record.origin) {
4838
4937
  const found = await errandRoom(record.origin, "post");
4839
4938
  if (typeof found === "string") return `its origin: ${found}`;
4840
- origin = { room: record.origin, audience: found.audience, ...found.mls ? { mls: found.mls } : {} };
4939
+ origin = { room: record.origin, mls: found.mls };
4841
4940
  }
4842
- const reports = !!origin && (!!origin.mls || !("room" in target && target.mls));
4843
- return { record, owner: owner2, target, ...origin ? { origin } : {}, reports };
4941
+ return { record, owner: owner2, target, ...origin ? { origin } : {}, reports: !!origin };
4844
4942
  }
4845
4943
  __name(errandSetup, "errandSetup");
4846
4944
  async function deliverReport(turn, outcome) {
@@ -4854,7 +4952,7 @@ ${JSON.stringify(result, null, 2)}`);
4854
4952
  let text3 = answered2 ? result.reply : failed ? FAILURE_NOTICE : null;
4855
4953
  if (answered2 && result.files?.length) text3 = `${text3}
4856
4954
 
4857
- (Files cannot go with an errand's report: ${result.files.map((file) => basename3(String(file))).join(", ")}.)`;
4955
+ (Files cannot go with an errand's report: ${result.files.map((file) => basename4(String(file))).join(", ")}.)`;
4858
4956
  const known = latest(record.id) ?? record;
4859
4957
  const cancelled = known.status === "cancelled" || stopped();
4860
4958
  const status = cancelled ? "cancelled" : ended(known, failed);
@@ -4873,19 +4971,16 @@ ${JSON.stringify(result, null, 2)}`);
4873
4971
  turnId: turn.id,
4874
4972
  conversationId: origin.room,
4875
4973
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4876
- audience: origin.audience,
4974
+ audience: null,
4877
4975
  inputs: [],
4878
4976
  text: text3,
4879
4977
  notice: failed,
4880
4978
  errand: record.id,
4881
- ...origin.mls ? { mls: origin.mls } : {},
4882
- options: origin.mls ? { messageId: keyed().text(kind, origin.room, turn.id) } : {
4883
- messageId: `${turn.id}:${kind}`,
4884
- metadata: { turnId: turn.id, turnSemantics: "turn_complete", ...failed ? { replyBehavior: "suppress_auto_reply" } : {} }
4885
- }
4979
+ mls: origin.mls,
4980
+ options: { messageId: keyed().text(kind, origin.room, turn.id) }
4886
4981
  };
4887
4982
  try {
4888
- journal.save(out);
4983
+ saveOutput(out);
4889
4984
  } catch (error) {
4890
4985
  warn(`errand ${record.id}: its report could not be journaled; it stays in the record`)(error);
4891
4986
  saveErrand({ ...closing, report: text3, reported: "refused", reason: "its report: it could not be saved for sending" });
@@ -4894,9 +4989,9 @@ ${JSON.stringify(result, null, 2)}`);
4894
4989
  saveErrand({ ...closing, reported: "sending" });
4895
4990
  const sent = await publish(out, turn.retire.signal);
4896
4991
  if (sent === "pending" && stopped()) {
4897
- journal.remove(out.turnId);
4898
- const { reported: _reported, ...withheld } = latest(record.id) ?? closing;
4899
- saveErrand({ ...withheld, status: "cancelled", reason: STOPPED_ERRAND[turn.reason] ?? "stopped" });
4992
+ await stopOutput(out);
4993
+ const { reported: _reported, ...withheld2 } = latest(record.id) ?? closing;
4994
+ saveErrand({ ...withheld2, status: "cancelled", reason: STOPPED_ERRAND[turn.reason] ?? "stopped" });
4900
4995
  } else if (sent !== "pending") finishErrandOutput(out, sent);
4901
4996
  turn.live.end(sent === "delivered" ? "answered" : stopped() ? "stopped" : "ended");
4902
4997
  }
@@ -4930,7 +5025,6 @@ ${JSON.stringify(result, null, 2)}`);
4930
5025
  } else if (event.kind === "conversation.updated") {
4931
5026
  void onRoomChanged(data).catch(warn("membership update"));
4932
5027
  } else if (event.kind === "runtime.control" && data.kind === "signal" && typeof data.conversationId === "string") {
4933
- if (options.relay && !rooms.get(data.conversationId)?.mls) return;
4934
5028
  scheduleControl(room(data.conversationId));
4935
5029
  }
4936
5030
  }
@@ -4994,7 +5088,7 @@ ${JSON.stringify(result, null, 2)}`);
4994
5088
  notice: false,
4995
5089
  mls: audience
4996
5090
  };
4997
- journal.save(record);
5091
+ saveOutput(record);
4998
5092
  return { text: text3, record };
4999
5093
  })();
5000
5094
  const sent = ready.then(({ record, settled }) => settled ?? publish(record).then((result) => {
@@ -5023,7 +5117,7 @@ ${JSON.stringify(result, null, 2)}`);
5023
5117
  client = new CanonClient(options.apiKey, connection.apiBaseUrl, {
5024
5118
  environmentId: connection.environmentId,
5025
5119
  streamUrl: connection.streamUrl,
5026
- stateDirectory: join6(home, "endpoint"),
5120
+ stateDirectory: join7(home, "endpoint"),
5027
5121
  ...options.client ? { client: options.client } : {},
5028
5122
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
5029
5123
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
@@ -5071,19 +5165,19 @@ ${JSON.stringify(result, null, 2)}`);
5071
5165
  }
5072
5166
  scope = JSON.stringify([identity.environmentId, agentId, identity.ownershipVersion ?? 0, identity.installationId]);
5073
5167
  if (mlsOn) {
5074
- mlsIds = createMlsIds(privateKey(join6(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2563(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
5168
+ mlsIds = createMlsIds(privateKey(join7(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2563(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
5075
5169
  mlsAsks = createMlsAsks({
5076
5170
  endpoint,
5077
5171
  agentId,
5078
- directory: join6(home, "runner", options.environmentId, agentId, "asks", sha2563(scope).slice(0, 32)),
5172
+ directory: join7(home, "runner", options.environmentId, agentId, "asks", sha2563(scope).slice(0, 32)),
5079
5173
  assertAudience,
5080
5174
  sessionForDestructive: options.sessionApprovalForDestructive === true,
5081
5175
  log
5082
5176
  }, approvalScope);
5083
5177
  }
5084
- journal = createCompletedOutputJournal(join6(home, "runner", options.environmentId, agentId, "finals"), scope, {
5178
+ journal = createCompletedOutputJournal(join7(home, "runner", options.environmentId, agentId, "finals"), scope, {
5085
5179
  label: "agent runner",
5086
- 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")
5180
+ validate: /* @__PURE__ */ __name((record) => typeof record.text === "string" && typeof record.options?.messageId === "string" && Array.isArray(record.inputs) && typeof record.notice === "boolean" && (record.mls === void 0 || !!record.mls && typeof record.mls === "object") && (record.parts === void 0 || Number.isSafeInteger(record.parts)) && (record.errand === void 0 || typeof record.errand === "string"), "validate")
5087
5181
  });
5088
5182
  if (!options.relay && !driver.capabilities.remote) {
5089
5183
  errands = errandJournal(errandsDirectory(home, options.environmentId, agentId));
@@ -5101,12 +5195,12 @@ ${JSON.stringify(result, null, 2)}`);
5101
5195
  else if (report) saveErrand({ ...record, status: ended(record, report.notice), reported: "sending" });
5102
5196
  else {
5103
5197
  saveErrand({ ...record, status: "interrupted", reason: "the runner stopped during its turn, which never runs again" });
5104
- if (record.origin) livePublisher(room(record.origin), void 0, "presence").end("stopped");
5198
+ if (record.origin) livePublisher(room(record.origin), void 0).end("stopped");
5105
5199
  }
5106
5200
  }
5107
5201
  }
5108
5202
  const outbound = driver.capabilities.outboundFiles;
5109
- roots = outbound ? (await Promise.all(outbound.roots.map((root) => realpath5(root).catch(() => "")))).filter(Boolean) : [];
5203
+ roots = outbound ? (await Promise.all(outbound.roots.map((root) => realpath4(root).catch(() => "")))).filter(Boolean) : [];
5110
5204
  if (driver.capabilities.tools) tools = await startToolServer();
5111
5205
  await recover();
5112
5206
  const publisher = createRuntimeStatePublisher({ agentId, clientType: CLIENT_TYPES[driver.name], hostMode: true, rtdb: initRTDBAuth(client) });
@@ -5283,14 +5377,14 @@ export {
5283
5377
  DriverSessionError,
5284
5378
  ConnectorRelayError,
5285
5379
  loadInstructions,
5380
+ checkAcp,
5381
+ acpDriver,
5286
5382
  loadClaudeQuery,
5287
5383
  claudeDriver,
5288
5384
  codeDriver,
5289
5385
  checkCodex,
5290
5386
  codexDriver,
5291
5387
  connectorDriver,
5292
- runnerLockName,
5293
- assertPrivateHome,
5294
5388
  errandsDirectory,
5295
5389
  queueErrand,
5296
5390
  errandJournal,
@@ -5301,6 +5395,7 @@ export {
5301
5395
  runMlsDevSetup,
5302
5396
  MLS_ENVIRONMENT_PINS,
5303
5397
  bundledMlsCustodyHelper,
5398
+ bundledMlsWasmCore,
5304
5399
  defaultMlsDevice,
5305
5400
  managedMlsDevice,
5306
5401
  startRelayChannel,