@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.
- package/README.md +57 -24
- package/dist/chunks/{chunk-C2WJT3VX.js → chunk-5WPAB5MB.js} +1047 -856
- package/dist/chunks/{chunk-3KFVBOVI.js → chunk-DNHJGAEL.js} +1115 -1020
- package/dist/chunks/chunk-DTAWEQCM.js +406 -0
- package/dist/chunks/{chunk-TFSURFF6.js → chunk-TYZT346O.js} +1720 -4006
- package/dist/chunks/chunk-YHDDOADK.js +118 -0
- package/dist/chunks/chunk-YJUPKU47.js +1490 -0
- package/dist/chunks/{mls-crypto-Y44BOSEM.js → mls-crypto-NZVMSUFU.js} +2 -1
- package/dist/cli.js +86 -48
- package/dist/drivers/acp.d.ts +56 -0
- package/dist/drivers/codex.d.ts +1 -2
- package/dist/drivers/subprocess.d.ts +38 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +12 -3
- package/dist/internal/backend-contracts/endpointContentPolicy.d.ts +2 -2
- package/dist/internal/backend-contracts/index.d.ts +1 -0
- package/dist/internal/backend-contracts/loggedOutSessions.d.ts +7 -0
- package/dist/internal/backend-contracts/mlsDevCredential.d.ts +3 -0
- package/dist/internal/backend-contracts/mlsDevGroup.d.ts +17 -0
- package/dist/internal/core/completed-output-journal.d.ts +0 -5
- package/dist/internal/core/index.d.ts +3 -9
- package/dist/internal/core/retry.d.ts +2 -89
- package/dist/internal/core/silent-turn.d.ts +4 -33
- package/dist/internal/endpoint/engine.d.ts +6 -0
- package/dist/internal/endpoint/mls-dev-client.d.ts +2 -0
- package/dist/internal/endpoint/mls-dev-groups.d.ts +12 -0
- package/dist/internal/endpoint/mls-dev-text.d.ts +32 -0
- package/dist/internal/endpoint/node-mls-custody-protocol.d.ts +5 -0
- package/dist/internal/endpoint/node.d.ts +1 -0
- package/dist/internal/endpoint/transport.d.ts +1 -1
- package/dist/media.js +4 -2
- package/dist/mls/darwin-arm64/canon-mls-custody +0 -0
- package/dist/mls/linux-arm64/canon-mls-custody +0 -0
- package/dist/mls/linux-x64/canon-mls-custody +0 -0
- package/dist/mls/wasm/canon_mls_wasm.js +363 -0
- package/dist/mls/wasm/canon_mls_wasm_bg.wasm +0 -0
- package/dist/mls-wasm-host.js +258 -0
- package/dist/runner/mls-bundled.d.ts +15 -2
- package/dist/runner/mls-managed.d.ts +6 -3
- package/dist/runner/mls.d.ts +4 -7
- package/dist/runner/runner.d.ts +9 -12
- package/dist/runner/types.d.ts +37 -1
- package/package.json +1 -1
- package/dist/internal/core/approval-manager.d.ts +0 -43
- package/dist/internal/core/runtime-request-manager.d.ts +0 -283
- package/dist/internal/core/turn-verbosity.d.ts +0 -79
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
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
|
-
|
|
30
|
-
sendMessageWithRetryChunked,
|
|
31
|
-
splitTextByUtf8Bytes,
|
|
32
|
+
splitTextWithinBytes,
|
|
32
33
|
verifyCanonRuntimeConnection
|
|
33
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
130
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
152
|
-
var
|
|
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
|
|
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 =
|
|
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") ?
|
|
171
|
-
return
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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:
|
|
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(
|
|
701
|
+
__name(approve2, "approve");
|
|
238
702
|
async function answerQuestions(turn, id, questions, input) {
|
|
239
|
-
const list = (Array.isArray(questions) ? questions : []).filter(
|
|
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(
|
|
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 =
|
|
720
|
+
const cwd = realpathSync2(options.cwd);
|
|
257
721
|
const permissionMode = options.permissionMode ?? "default";
|
|
258
722
|
const interruptGraceMs = options.interruptGraceMs ?? 5e3;
|
|
259
|
-
const system = withInstructions(
|
|
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
|
|
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 =
|
|
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) =>
|
|
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,
|
|
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(
|
|
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(
|
|
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 ? `: ${
|
|
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
|
|
388
|
-
import { tmpdir as
|
|
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: [
|
|
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 {
|
|
425
|
-
import {
|
|
426
|
-
import {
|
|
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
|
|
892
|
+
var CANON2 = "canon";
|
|
431
893
|
var API_KEY_PROVIDER = "canon_openai_api_key";
|
|
432
|
-
var
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
492
|
-
return
|
|
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
|
|
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
|
|
954
|
+
return oneLine(`Search the web${text(item.query) ? `: ${text(item.query)}` : ""}`, 120);
|
|
500
955
|
case "imageView":
|
|
501
|
-
return `View ${
|
|
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 =
|
|
524
|
-
const parsed = (Array.isArray(params.commandActions) ? params.commandActions : []).filter(
|
|
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
|
|
974
|
+
const verdict = await approve(turn, {
|
|
528
975
|
id,
|
|
529
|
-
title:
|
|
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(
|
|
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
|
|
993
|
+
const verdict = await approve(turn, {
|
|
547
994
|
id,
|
|
548
|
-
title:
|
|
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) =>
|
|
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 =
|
|
559
|
-
const verdict = await
|
|
1005
|
+
const permissions = isRecord(params.permissions) ? params.permissions : {};
|
|
1006
|
+
const verdict = await approve(turn, {
|
|
560
1007
|
id,
|
|
561
|
-
title:
|
|
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 =
|
|
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
|
|
1023
|
+
const verdict = await approve(turn, {
|
|
577
1024
|
id: `mcp-${String(rpcId)}`,
|
|
578
|
-
title:
|
|
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(
|
|
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(
|
|
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 =
|
|
1053
|
+
const cwd = realpathSync4(options.cwd);
|
|
651
1054
|
const codexPath = options.codexPath ?? "codex";
|
|
652
1055
|
const interruptGraceMs = options.interruptGraceMs ?? 5e3;
|
|
653
|
-
const developerInstructions = withInstructions(
|
|
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 =
|
|
665
|
-
const servers = guest &&
|
|
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 ? { [
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
753
|
-
settle(new Error(`Codex ended the turn ${ended.status === "interrupted" ? "interrupted" : `with an error${detail ? `: ${
|
|
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 ===
|
|
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 =
|
|
1166
|
+
const link = stdioRpc(child, {
|
|
763
1167
|
notify(method, params) {
|
|
764
|
-
const item =
|
|
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 &&
|
|
1184
|
+
} else if (method === "error" && params.willRetry !== true && isRecord(params.error)) {
|
|
781
1185
|
failure = text(params.error.message);
|
|
782
|
-
} else if (method === "turn/completed" &&
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
1158
|
-
|
|
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(
|
|
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 =
|
|
1197
|
-
const file = /* @__PURE__ */ __name((id) =>
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
1550
|
+
const problem = custody(fstatSync(fd), "file");
|
|
1254
1551
|
if (problem) return { problem };
|
|
1255
1552
|
let value;
|
|
1256
1553
|
try {
|
|
1257
|
-
value = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1340
|
-
import { open as open2, realpath as
|
|
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)
|
|
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
|
|
1457
|
-
const file = await open2(filePath,
|
|
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
|
|
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
|
|
1586
|
-
import { dirname as dirname4, join as
|
|
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-
|
|
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": "
|
|
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": "
|
|
1610
|
-
"mls/core/dev_key_package.rs": "
|
|
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": "
|
|
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: "
|
|
1920
|
+
sha256: "b09c82209cc7884c4521a310c727a8faaf387e9d0603443b1c1d9a55d87b7103",
|
|
1624
1921
|
bytes: 2932976,
|
|
1625
|
-
unitTestsPassed:
|
|
1626
|
-
commit: "
|
|
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: "
|
|
1636
|
-
bytes:
|
|
1637
|
-
unitTestsPassed:
|
|
1638
|
-
commit: "
|
|
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: "
|
|
1943
|
+
sha256: "649d90ba74a42b34025292bd942068fc08979e35f66c9d6ad358469f5022fac7",
|
|
1647
1944
|
bytes: 3234232,
|
|
1648
|
-
unitTestsPassed:
|
|
1649
|
-
commit: "
|
|
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 =
|
|
1691
|
-
if (
|
|
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 &&
|
|
1701
|
-
if (!pin || !path || !
|
|
1702
|
-
if (createHash5("sha256").update(
|
|
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
|
|
1712
|
-
import { dirname as dirname5, join as
|
|
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
|
|
1717
|
-
return helper ? managedMlsDevice({ home, environmentId,
|
|
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
|
|
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 =
|
|
1735
|
-
const directory = /* @__PURE__ */ __name((context) =>
|
|
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) =>
|
|
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 =
|
|
1745
|
-
if (
|
|
1746
|
-
|
|
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(
|
|
1751
|
-
|
|
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 && !
|
|
1762
|
-
|
|
1763
|
-
|
|
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 =
|
|
1780
|
-
if (!
|
|
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
|
-
|
|
1807
|
-
|
|
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
|
|
1818
|
-
import { lstat
|
|
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
|
|
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
|
|
1954
|
-
const file = await open3(path,
|
|
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) =>
|
|
1992
|
-
var credentialFile = /* @__PURE__ */ __name((home) =>
|
|
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
|
|
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
|
-
|
|
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 =
|
|
2396
|
+
const fd = openSync2(temporary, "w", 384);
|
|
2022
2397
|
try {
|
|
2023
|
-
|
|
2024
|
-
|
|
2398
|
+
writeSync(fd, JSON.stringify(value));
|
|
2399
|
+
fsyncSync(fd);
|
|
2025
2400
|
} finally {
|
|
2026
|
-
|
|
2401
|
+
closeSync2(fd);
|
|
2027
2402
|
}
|
|
2028
|
-
|
|
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 =
|
|
2035
|
-
|
|
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
|
|
2043
|
-
import { realpath as
|
|
2044
|
-
import { basename as
|
|
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(() =>
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
2540
|
-
import { join as
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 = ["
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3427
|
-
|
|
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(
|
|
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
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
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
|
|
3482
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
|
3851
|
+
await deliver(current, turn, outcome);
|
|
3617
3852
|
} finally {
|
|
3618
3853
|
turn.ended.abort();
|
|
3619
3854
|
bound.release();
|
|
3620
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
|
|
3666
|
-
|
|
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
|
-
|
|
3669
|
-
|
|
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) &&
|
|
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:
|
|
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(`${
|
|
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 &&
|
|
4029
|
+
const sendsFiles = roots.length > 0 && forOwner2(turn);
|
|
3801
4030
|
const binding = {
|
|
3802
|
-
// §6:
|
|
3803
|
-
//
|
|
3804
|
-
// asks only in an origin its report
|
|
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 &&
|
|
3809
|
-
] :
|
|
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 (
|
|
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 && !
|
|
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((
|
|
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 (
|
|
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
|
|
4090
|
+
return sealedSend(turn, to, command.text);
|
|
3869
4091
|
}
|
|
3870
4092
|
if (origins.includes(to)) {
|
|
3871
|
-
|
|
3872
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3997
|
-
const
|
|
3998
|
-
|
|
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
|
|
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 &&
|
|
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 &&
|
|
4201
|
+
} else if (answered2 && resultFiles.length) {
|
|
4038
4202
|
text3 = `${text3}
|
|
4039
4203
|
|
|
4040
|
-
(${ENCRYPTED_FILES}: ${resultFiles.map((file) => typeof file === "string" ?
|
|
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:
|
|
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
|
-
|
|
4221
|
+
mls: audience
|
|
4070
4222
|
};
|
|
4071
|
-
|
|
4223
|
+
saveOutput(record);
|
|
4072
4224
|
sent = await publish(record, turn.retire.signal);
|
|
4073
|
-
|
|
4074
|
-
if (sent === "pending"
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
4147
|
-
|
|
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 (
|
|
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
|
-
|
|
4262
|
+
for (const part of parts) forgetGuard(mls2, part.messageId, sent);
|
|
4158
4263
|
return sent;
|
|
4159
4264
|
}
|
|
4160
4265
|
__name(send, "send");
|
|
4161
|
-
|
|
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: {
|
|
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" ?
|
|
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:
|
|
4213
|
-
refused2.push(
|
|
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:
|
|
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((
|
|
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((
|
|
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
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
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((
|
|
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 (
|
|
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
|
|
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 =
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
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(
|
|
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
|
|
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 !== "
|
|
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 (
|
|
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
|
|
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 (
|
|
4514
|
-
|
|
4515
|
-
|
|
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:
|
|
4727
|
+
audience: null,
|
|
4628
4728
|
inputs: [],
|
|
4629
4729
|
text: parsed.text,
|
|
4630
|
-
options:
|
|
4730
|
+
options: { messageId: keyed().text("final", conversationId, turnId2) },
|
|
4631
4731
|
notice: false,
|
|
4632
4732
|
errand: id,
|
|
4633
|
-
|
|
4733
|
+
mls: target.mls
|
|
4634
4734
|
};
|
|
4635
|
-
|
|
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
|
|
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
|
|
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
|
|
4700
|
-
const report = reports ? `your final answer is your report to your owner, posted in the conversation ${origin.room}.` :
|
|
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(
|
|
4713
|
-
const report = !record.origin ? "Your owner follows this errand on their machine; it has no conversation to report to." :
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
4939
|
+
origin = { room: record.origin, mls: found.mls };
|
|
4841
4940
|
}
|
|
4842
|
-
|
|
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) =>
|
|
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:
|
|
4974
|
+
audience: null,
|
|
4877
4975
|
inputs: [],
|
|
4878
4976
|
text: text3,
|
|
4879
4977
|
notice: failed,
|
|
4880
4978
|
errand: record.id,
|
|
4881
|
-
|
|
4882
|
-
options:
|
|
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
|
-
|
|
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
|
-
|
|
4898
|
-
const { reported: _reported, ...
|
|
4899
|
-
saveErrand({ ...
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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.
|
|
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
|
|
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) =>
|
|
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,
|