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