@canonmsg/agent-sdk 12.3.2 → 12.4.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 CHANGED
@@ -211,16 +211,20 @@ The CLI is `canon-agent run --driver code --module <path> [--profile <name>] [--
211
211
 
212
212
  ## Encrypted rooms
213
213
 
214
- Dev and Prod require every new room to be end-to-end encrypted (MLS). Without an MLS device an agent takes part in no encrypted room. An agent whose outbound policy is closed cannot send there. The runner holds the agent's MLS device; drivers need nothing for it. Give the runner the operator's MLS configuration: a private JSON file with the custody helper and the environment's issuer and receipt public keys, never private keys or credentials. There is no prebuilt custody helper yet: build it from `mls/native` in the Canon repository. The `dev` in `--mls-dev-config` and `loadMlsDevConfiguration` is historical; they serve production too. `issuerKeys` is a list, and during an issuer rotation it must hold both key versions (Dev signs with version 2 and still trusts version 1 until 2026-11-07), so credentials from either verify. Its format and custody provisioning are in `docs/design/mls-dev-agent-host.md` in the Canon repository:
214
+ Dev and Prod require every new room to be end-to-end encrypted (MLS). Without an MLS device an agent takes part in no encrypted room. An agent whose outbound policy is closed cannot send there. The runner holds the agent's MLS device; drivers need nothing for it.
215
+
216
+ On macOS (Apple silicon) and Linux (x64 and arm64, glibc 2.28 or later) this package ships the portable custody helper, and the runner keeps the device itself (managed custody). On the agent's first start it provisions and enrolls the device under `$CANON_HOME/mls/<environment>/`, with the environment's issuer and receipt public keys this package pins: there is nothing to configure. Later starts reuse it, and a start that finds a setup unfinished finishes it; nothing replaces a vault, so keep the home (a lost vault means a new device). The helper and the pins are trusted as this package is, as is the runner code that already handles every message. `canon-agent mls --action setup` does the same without starting a model; in library form, pass `mls: managedMlsDevice({ home, environmentId })` to `createRunner`.
217
+
218
+ Elsewhere (an Intel Mac or musl; Windows is not supported), or to keep the device in custody of your own, give the runner an operator's MLS configuration with `--mls-dev-config`: a private JSON file with the custody helper, built from `mls/native` in the Canon repository, and the environment's issuer and receipt public keys, never private keys or credentials. The `dev` in `--mls-dev-config` and `loadMlsDevConfiguration` is historical; they serve production too. `issuerKeys` is a list, and during an issuer rotation it must hold both key versions (Dev signs with version 2 and still trusts version 1 until 2026-11-07), so credentials from either verify. Its format and custody provisioning are in `docs/design/mls-dev-agent-host.md` in the Canon repository:
215
219
 
216
220
  ```sh
217
- canon-agent mls --action status --mls-dev-config /abs/mls-dev.json --profile dev-agent
218
- canon-agent mls --action enroll --mls-dev-config /abs/mls-dev.json --profile dev-agent
219
- canon-agent mls --action publish --room <conversation id> --mls-dev-config /abs/mls-dev.json --profile dev-agent
221
+ canon-agent run --driver claude --profile dev-agent
222
+ canon-agent mls --action status --profile dev-agent
223
+ canon-agent mls --action publish --room <conversation id> --profile dev-agent
220
224
  canon-agent run --driver claude --mls-dev-config /abs/mls-dev.json --profile dev-agent
221
225
  ```
222
226
 
223
- `canon-agent mls` runs one setup step (`status`, `provision` for portable custody, once, `enroll`, `renew`, `publish` a KeyPackage for a room before someone adds the agent, `refresh` a room) on the same home as `run`, so stop the agent first. It prints public device state and never starts a model. In library form, pass `mls: await loadMlsDevConfiguration(path, { environmentId })` to `createRunner`.
227
+ `canon-agent mls` runs one setup step (`status`, `setup` for whatever is left of provision then enroll, `provision` for portable custody, once, `enroll`, `renew`, `publish` a KeyPackage for a room before someone adds the agent, `refresh` a room) on the same home as `run`, so stop the agent first. Each takes `--mls-dev-config` for an operator's custody. It prints public device state and never starts a model. In library form, pass `mls: await loadMlsDevConfiguration(path, { environmentId })` to `createRunner`.
224
228
 
225
229
  In an encrypted room:
226
230
 
@@ -232,18 +236,18 @@ In an encrypted room:
232
236
  - A reply is journaled before it is encrypted. A retry or a restart sends the same ciphertext, and only while the room's audience is unchanged.
233
237
  - The ids Canon sees for what the runner sends there (each message's id, and a card's request id) are keyed with a secret the runner creates once in `$CANON_HOME/runner/<environment>/<agent>/mls-ids/` (0600; it belongs with the rest of the home). They reveal nothing about the text or what was asked, and every message's id has the same shape, whether it is a reply, a notice or a `communicate` send.
234
238
 
235
- Without `--mls-dev-config` (or `mls`), the runner refuses each encrypted room, logs it once, and runs nothing for it. Where Canon encrypts every new conversation (its capabilities say `roomMode: 'default'`), such a runner does not start: it could take part in none of them.
239
+ Without a device (no helper for the host and no `--mls-dev-config`, or no `mls` for `createRunner`), the runner refuses each encrypted room, logs it once, and runs nothing for it. Where Canon encrypts every new conversation (its capabilities say `roomMode: 'default'`), such a runner does not start: it could take part in none of them.
236
240
 
237
241
  ### The class-C relay (Canon-operated)
238
242
 
239
243
  An assistant without an install of its own (ChatGPT, Claude.ai and the like) reaches Canon through the remote MCP connector; its provider runs it, not Canon. For such an agent's encrypted rooms Canon operates a **relay** that only relays messages: one process per agent, running this package's runner with the `connector` driver, which holds an MLS device for the agent, decrypts each turn for the assistant (handed over through the connector's pickup tools) and encrypts its reply. Those rooms are therefore **not end-to-end encrypted** to that agent. Agent developers do not run it; Canon's operator does:
240
244
 
241
245
  ```sh
242
- canon-agent run --driver connector --relay-config /abs/relay.json --mls-dev-config /abs/mls-dev.json
243
- canon-agent mls --action provision --mls-dev-config /abs/mls-dev.json --relay-config /abs/relay.json
246
+ canon-agent run --driver connector --relay-config /abs/relay.json
247
+ canon-agent mls --action status --relay-config /abs/relay.json
244
248
  ```
245
249
 
246
- The relay configuration (a private JSON file) names the agent, where the channel to Canon's Functions listens, and the path of the channel secret. No API key is involved: when the owner enables the relay, Canon delivers the relay's own credential over that channel (it works only for the relay's encrypted-room work, and stops when the relay or its connection is revoked), and the relay keeps it in `$CANON_HOME/relay` and starts under the relay installation it names (`canon-relay:…`). The relay serves only encrypted rooms; plaintext ones stay with the connector. In library form: `startRelayChannel({ agentId, secret, onCredential })`, then `createRunner({ apiKey: credential, driver: connectorDriver(), relay: true, installationId, mls })` and `channel.serve({ runner, inbox: driver.inbox }, grantId)`. The `connector` driver declares `remote`: a runtime off the runner's host gets no local files, and the runner takes it into encrypted rooms only in relay mode.
250
+ The relay configuration (a private JSON file) names the agent, where the channel to Canon's Functions listens, and the path of the channel secret. No API key is involved: when the owner enables the relay, Canon delivers the relay's own credential over that channel (it works only for the relay's encrypted-room work, and stops when the relay or its connection is revoked), and the relay keeps it in `$CANON_HOME/relay` and starts under the relay installation it names (`canon-relay:…`), setting up that installation's device as any runner does (or with `--mls-dev-config`). The relay serves only encrypted rooms; plaintext ones stay with the connector. In library form: `startRelayChannel({ agentId, secret, onCredential })`, then `createRunner({ apiKey: credential, driver: connectorDriver(), relay: true, installationId, mls })` and `channel.serve({ runner, inbox: driver.inbox }, grantId)`. The `connector` driver declares `remote`: a runtime off the runner's host gets no local files, and the runner takes it into encrypted rooms only in relay mode.
247
251
 
248
252
  ## Team rooms
249
253
 
@@ -8,6 +8,7 @@ import {
8
8
  MAX_PUBLIC_RUNTIME_COMMANDS,
9
9
  MLS_DEV_CREDENTIAL_MAX_BYTES,
10
10
  MLS_DEV_CREDENTIAL_RECONCILIATION_MAX_BYTES,
11
+ MLS_DEV_DISCLOSED_PROCESSING_FIELDS,
11
12
  MLS_DEV_ENROLLMENT_MAX_BYTES,
12
13
  MLS_DEV_GROUP_RECEIPT_CLOCK_SKEW,
13
14
  MLS_DEV_INTERACTION_LIMITS,
@@ -109,7 +110,7 @@ import {
109
110
  readMembershipChangeMetadata,
110
111
  resolveAllowedUploadMimeType,
111
112
  sanitizeDownloadFileName
112
- } from "./chunk-5MI6Q3VS.js";
113
+ } from "./chunk-GWQF4WFT.js";
113
114
 
114
115
  // src/media.ts
115
116
  import { createWriteStream as createWriteStream2 } from "node:fs";
@@ -2602,6 +2603,8 @@ async function assertMlsDevPlaintextAllowed(tx, intent2, lane = "operation.reque
2602
2603
  }), "failure");
2603
2604
  const action = intent2.action === "verb_execute" ? intent2.body.verb : intent2.action;
2604
2605
  if (typeof action === "string" && MLS_DEV_UNSUPPORTED_CONTENT_ACTIONS.some((unsupported) => unsupported === action)) throw failure2();
2606
+ const disclosed = lane === "operation.request" && Object.hasOwn(MLS_DEV_DISCLOSED_PROCESSING_FIELDS, intent2.action) ? MLS_DEV_DISCLOSED_PROCESSING_FIELDS[intent2.action] : void 0;
2607
+ if (disclosed && !intent2.envelope && Object.keys(intent2.body).every((field) => disclosed.includes(field))) return;
2605
2608
  const payload = lane === "operation.request" ? { body: { encoding: "json", value: intent2.body }, ...intent2.envelope ? { envelope: intent2.envelope } : {} } : intent2.body;
2606
2609
  let boundary;
2607
2610
  try {
@@ -8803,9 +8806,28 @@ function createEndpoint(options) {
8803
8806
  }
8804
8807
  __name(sendDeferredOpeners, "sendDeferredOpeners");
8805
8808
  const stubReads = /* @__PURE__ */ new Map();
8809
+ const stubNames = /* @__PURE__ */ new Map();
8806
8810
  let liveActionCounter = 0;
8807
8811
  const liveActions = /* @__PURE__ */ new Map();
8808
8812
  const stubKey = /* @__PURE__ */ __name((conversationId, messageId) => `${conversationId}\0${messageId}`, "stubKey");
8813
+ function rememberStubName(conversationId, stub) {
8814
+ const name = typeof stub.senderName === "string" ? stub.senderName.trim() : "";
8815
+ if (typeof stub.senderId !== "string" || !stub.senderId || !name || name === stub.senderId || name.length > 80) return;
8816
+ const key2 = stubKey(conversationId, stub.id);
8817
+ stubNames.delete(key2);
8818
+ stubNames.set(key2, { senderId: stub.senderId, name });
8819
+ if (stubNames.size > 1e3) stubNames.delete(stubNames.keys().next().value);
8820
+ }
8821
+ __name(rememberStubName, "rememberStubName");
8822
+ function withStubNames(conversationId, history) {
8823
+ const messages = history.messages;
8824
+ if (!Array.isArray(messages)) return history;
8825
+ return { ...history, messages: messages.map((message) => {
8826
+ const named = typeof message.id === "string" ? stubNames.get(stubKey(conversationId, message.id)) : void 0;
8827
+ return named && named.senderId === message.senderId ? { ...message, senderName: named.name } : message;
8828
+ }) };
8829
+ }
8830
+ __name(withStubNames, "withStubNames");
8809
8831
  function forgetStubReads(conversationId) {
8810
8832
  for (const key2 of stubReads.keys()) if (key2.startsWith(`${conversationId}\0`)) stubReads.delete(key2);
8811
8833
  }
@@ -8823,6 +8845,7 @@ function createEndpoint(options) {
8823
8845
  } catch {
8824
8846
  return false;
8825
8847
  }
8848
+ for (const stub of stubs) if (typeof stub.id === "string" && due.includes(stub.id)) rememberStubName(conversationId, { ...stub, id: stub.id });
8826
8849
  return store.transaction(async (tx) => {
8827
8850
  let changed = false;
8828
8851
  for (const stub of stubs) {
@@ -9667,7 +9690,7 @@ function createEndpoint(options) {
9667
9690
  const room = normalized.conversationId, read = /* @__PURE__ */ __name(() => mlsDevGroups.queryTextHistory(room, name, normalized), "read");
9668
9691
  const history = await read();
9669
9692
  const shown = (history.messages ?? []).map((entry) => entry.id).filter((id2) => typeof id2 === "string");
9670
- return await recoverMlsDevMessageActions(room, shown) ? await read() : history;
9693
+ return withStubNames(room, await recoverMlsDevMessageActions(room, shown) ? await read() : history);
9671
9694
  }
9672
9695
  }
9673
9696
  if (name === "get_runtime_card_state" && await mlsDevInteractionRoom(normalized.conversationId, false)) {
@@ -10096,7 +10119,7 @@ async function acquireAgentEndpoint(input) {
10096
10119
  }
10097
10120
  const requestedMls = input.options.mlsDev;
10098
10121
  const mls = requestedMls ? await (async () => {
10099
- const crypto2 = await import("./mls-crypto-D5YAGYJ3.js");
10122
+ const crypto2 = await import("./mls-crypto-LGPCXM3K.js");
10100
10123
  const configuration = crypto2.parseMlsDevClientConfiguration(requestedMls.configuration);
10101
10124
  if (configuration.environmentId !== environmentId || baseUrl !== CANON_RUNTIME_ENDPOINT_DEFAULTS[configuration.environmentId]?.apiBaseUrl || configuration.principalIds && !configuration.principalIds.includes(input.principalId) || !Number.isSafeInteger(input.ownershipVersion) || input.ownershipVersion < 0) throw new Error("MLS_DEV_AGENT_CONFIGURATION_BINDING_MISMATCH");
10102
10125
  const custody = requestedMls.custody;
@@ -14097,6 +14120,7 @@ function getCodexImagePath(attachment) {
14097
14120
  __name(getCodexImagePath, "getCodexImagePath");
14098
14121
 
14099
14122
  export {
14123
+ NodeMlsDevCustodyError,
14100
14124
  createNodeMlsDevCustody,
14101
14125
  createPortableNodeMlsDevCustody,
14102
14126
  resolveCanonRuntimeConnection,
@@ -4065,6 +4065,9 @@ __name(normalizePublicRuntimeFacts, "normalizePublicRuntimeFacts");
4065
4065
 
4066
4066
  // ../backend-contracts/src/endpointContentPolicy.ts
4067
4067
  var MLS_DEV_UNSUPPORTED_CONTENT_ACTIONS = ["forward_message"];
4068
+ var MLS_DEV_DISCLOSED_PROCESSING_FIELDS = {
4069
+ generate_image: ["conversationId", "requestId", "prompt", "sourceUploadId"]
4070
+ };
4068
4071
  var ENDPOINT_CONTENT_ACTIONS = [
4069
4072
  "mls_application",
4070
4073
  "mls_group_control",
@@ -5381,6 +5384,7 @@ export {
5381
5384
  normalizePublicRuntimeDescriptor,
5382
5385
  normalizePublicRuntimeFacts,
5383
5386
  MLS_DEV_UNSUPPORTED_CONTENT_ACTIONS,
5387
+ MLS_DEV_DISCLOSED_PROCESSING_FIELDS,
5384
5388
  endpointOperationRequestRetention,
5385
5389
  endpointContentBoundary,
5386
5390
  canonicalEndpointJson,
@@ -3,6 +3,7 @@ import {
3
3
  BoundedFileReadError,
4
4
  CanonClient,
5
5
  MAX_CANON_MEDIA_BYTES,
6
+ NodeMlsDevCustodyError,
6
7
  RUNTIME_NEW_SESSION_ACTION,
7
8
  RUNTIME_STOP_ACTION,
8
9
  RUNTIME_STOP_AND_DROP_ACTION,
@@ -29,7 +30,7 @@ import {
29
30
  sendMessageWithRetryChunked,
30
31
  splitTextByUtf8Bytes,
31
32
  verifyCanonRuntimeConnection
32
- } from "./chunk-RFNYPZXD.js";
33
+ } from "./chunk-6PKXASSK.js";
33
34
  import {
34
35
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
35
36
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
@@ -58,7 +59,7 @@ import {
58
59
  parseMlsDevGroupHead,
59
60
  parseMlsDevGroupRoster,
60
61
  resolveAllowedUploadMimeType
61
- } from "./chunk-5MI6Q3VS.js";
62
+ } from "./chunk-GWQF4WFT.js";
62
63
 
63
64
  // src/runner/types.ts
64
65
  var DriverSessionError = class extends Error {
@@ -464,6 +465,8 @@ var GUEST_ITEMS = /* @__PURE__ */ new Set([
464
465
  "subAgentActivity",
465
466
  "sleep"
466
467
  ]);
468
+ var OWN_LISTINGS = /* @__PURE__ */ new Set(["list_mcp_resources", "list_mcp_resource_templates"]);
469
+ var GUEST = "Only your owner can let you use tools in this conversation, and this turn holds a message from someone else: you cannot read files, run commands or use any tool but the canon MCP server's. Answer from the conversation and what you already know, and say so when something needs your owner.";
467
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;
468
471
  var IMAGE = /\.(png|jpe?g|gif|webp)$/i;
469
472
  var isRecord2 = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null && !Array.isArray(value), "isRecord");
@@ -750,10 +753,11 @@ function codexDriver(options) {
750
753
  settle(new Error(`Codex ended the turn ${ended.status === "interrupted" ? "interrupted" : `with an error${detail ? `: ${oneLine2(detail, 300)}` : ""}`}`));
751
754
  }, "conclude");
752
755
  const guard = /* @__PURE__ */ __name((item) => {
753
- if (!guest || GUEST_ITEMS.has(text(item.type)) || item.type === "mcpToolCall" && item.server === CANON) return;
756
+ if (!guest || GUEST_ITEMS.has(text(item.type)) || item.type === "mcpToolCall" && (item.server === CANON || item.server === "codex" && OWN_LISTINGS.has(text(item.tool)))) return;
754
757
  interrupt();
755
758
  entry.close();
756
- settle(new DriverSessionError(`Codex started ${text(item.type) || "an unknown item"} in a turn holding someone other than the owner's message`));
759
+ const what = item.type === "mcpToolCall" ? `mcpToolCall (${describe(item)})` : text(item.type) || "an unknown item";
760
+ settle(new DriverSessionError(`Codex started ${what} in a turn holding someone other than the owner's message`));
757
761
  }, "guard");
758
762
  const link = rpc(child, {
759
763
  notify(method, params) {
@@ -813,6 +817,7 @@ function codexDriver(options) {
813
817
  }
814
818
  if (turn.signal.aborted) return finish();
815
819
  const started = await link.call("turn/start", { threadId, input: [
820
+ ...guest ? [{ type: "text", text: GUEST, text_elements: [] }] : [],
816
821
  { type: "text", text: turn.prompt, text_elements: [] },
817
822
  // Images go to the model as images; every attachment's path is also in the prompt for Codex to read.
818
823
  ...turn.files.filter((file) => IMAGE.test(file)).map((path) => ({ type: "localImage", path }))
@@ -1485,7 +1490,7 @@ async function loadMlsDevConfiguration(filePath, input) {
1485
1490
  return { configuration, custody: custody2, close: /* @__PURE__ */ __name(() => custody2.close(), "close") };
1486
1491
  }
1487
1492
  __name(loadMlsDevConfiguration, "loadMlsDevConfiguration");
1488
- var MLS_SETUP_ACTIONS = ["status", "provision", "enroll", "renew", "publish", "refresh"];
1493
+ var MLS_SETUP_ACTIONS = ["status", "setup", "provision", "enroll", "renew", "publish", "refresh"];
1489
1494
  function mlsDevRoomPackageId(identity, room) {
1490
1495
  const hex = createHash4("sha256").update(JSON.stringify([
1491
1496
  "canon.mls.dev-agent-room-package.v1",
@@ -1498,25 +1503,37 @@ function mlsDevRoomPackageId(identity, room) {
1498
1503
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
1499
1504
  }
1500
1505
  __name(mlsDevRoomPackageId, "mlsDevRoomPackageId");
1501
- async function runMlsDevSetup(endpoint, action, room, provision) {
1506
+ async function runMlsDevSetup(endpoint, action, room, provision, provisioned) {
1502
1507
  const identity = await endpoint.ready();
1503
- if (!isMlsEnvironmentId(identity.environmentId) || !endpoint.mlsDevDevice || !endpoint.mlsDevKeyPackages || !endpoint.mlsDevGroups) throw new Error("MLS_DEV_SETUP_UNAVAILABLE");
1508
+ const { environmentId } = identity, devices = endpoint.mlsDevDevice;
1509
+ if (!isMlsEnvironmentId(environmentId) || !devices || !endpoint.mlsDevKeyPackages || !endpoint.mlsDevGroups) throw new Error("MLS_DEV_SETUP_UNAVAILABLE");
1504
1510
  if ((action === "publish" || action === "refresh") !== !!room) throw new Error("MLS_DEV_SETUP_ARGUMENTS_INVALID");
1505
- if (action === "provision") {
1511
+ const enroll = /* @__PURE__ */ __name(async () => {
1512
+ await devices.enroll();
1513
+ if ((await devices.read()).status !== "credential_committed") await devices.renew();
1514
+ }, "enroll");
1515
+ const provisionDevice = /* @__PURE__ */ __name(async (unlessProvisioned) => {
1506
1516
  if (!provision || identity.ownershipVersion === void 0) throw new Error("MLS_DEV_SETUP_UNAVAILABLE");
1507
- const device2 = await endpoint.mlsDevDevice.read();
1517
+ const device2 = await devices.read();
1508
1518
  if (device2.registration || device2.status !== "absent") throw new Error("MLS_DEV_SETUP_ALREADY_PROVISIONED");
1509
- await provision({
1519
+ const context = {
1510
1520
  version: 1,
1511
- environmentId: identity.environmentId,
1521
+ environmentId,
1512
1522
  installationId: identity.installationId,
1513
1523
  principal: { type: "ai_agent", id: await endpoint.mlsPrincipalId(), ownershipVersion: identity.ownershipVersion },
1514
1524
  revision: 0
1515
- });
1516
- } else if (action === "enroll") {
1517
- await endpoint.mlsDevDevice.enroll();
1518
- if ((await endpoint.mlsDevDevice.read()).status !== "credential_committed") await endpoint.mlsDevDevice.renew();
1519
- } else if (action === "renew") await endpoint.mlsDevDevice.renew();
1525
+ };
1526
+ if (!unlessProvisioned || !provisioned?.(context)) await provision(context);
1527
+ }, "provisionDevice");
1528
+ if (action === "setup") {
1529
+ const { status } = await devices.read();
1530
+ if (status === "revoked") throw new Error("MLS_DEV_SETUP_REVOKED");
1531
+ if (status === "absent") await provisionDevice(true);
1532
+ if (status === "registered") await devices.ensureReady();
1533
+ else if (status !== "credential_committed") await enroll();
1534
+ } else if (action === "provision") await provisionDevice(false);
1535
+ else if (action === "enroll") await enroll();
1536
+ else if (action === "renew") await devices.renew();
1520
1537
  else if (action === "publish") {
1521
1538
  const keyPackageId = mlsDevRoomPackageId(identity, room);
1522
1539
  await endpoint.mlsDevKeyPackages.prepare({ keyPackageId });
@@ -1525,7 +1542,7 @@ async function runMlsDevSetup(endpoint, action, room, provision) {
1525
1542
  const group = await endpoint.mlsDevGroups.refresh(room);
1526
1543
  if (group?.status !== "active" || await endpoint.mlsDevGroups.readRecoveryFailure(room)) throw new Error("MLS_DEV_SETUP_ROOM_NOT_READY");
1527
1544
  }
1528
- const device = await endpoint.mlsDevDevice.read();
1545
+ const device = await devices.read();
1529
1546
  return {
1530
1547
  version: 1,
1531
1548
  action,
@@ -1541,12 +1558,238 @@ async function runMlsDevSetup(endpoint, action, room, provision) {
1541
1558
  }
1542
1559
  __name(runMlsDevSetup, "runMlsDevSetup");
1543
1560
 
1561
+ // src/runner/mls-bundled.ts
1562
+ import { createHash as createHash5 } from "node:crypto";
1563
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1564
+ import { dirname as dirname4, join as join2 } from "node:path";
1565
+ import { fileURLToPath } from "node:url";
1566
+
1567
+ // mls-custody.json
1568
+ var mls_custody_default = {
1569
+ schemaVersion: 1,
1570
+ release: "mls-custody-cadb937436ce",
1571
+ protocolVersion: 1,
1572
+ rustVersion: "rustc 1.91.0 (f8297e351 2025-10-28)",
1573
+ sourceHashes: {
1574
+ "mls/native/Cargo.toml": "f93b48329951d5a11ef6d5830ff7c375cb847543bdb1c98a6fd8a8d02026db89",
1575
+ "mls/native/Cargo.lock": "9c2e9a3bb0f79c14540942c091124cba4aa262ee14cef2cdd5490653cd4656f3",
1576
+ "mls/native/src/lib.rs": "8e586bfaedc5fe376febbda78c32f8383aaa2ac92868000dd4c84ca083731571",
1577
+ "mls/native/src/bin/canon-mls-custody.rs": "3ab54102bfe315e425b6c3cf0aa7feb66e44d047e135336b0291305507062459",
1578
+ "mls/native/src/portable/mod.rs": "3005b60f7777a841347421ad96eb631b72063e85cebe96afd1c11fef095b0e34",
1579
+ "mls/native/src/portable/json.rs": "d4e68556a051ffe9008686036597b1fd8239ae66fad67bd848c3f0e16bf8ccaf",
1580
+ "mls/native/src/portable/vault.rs": "14b9ff1e10ec4058e39a46511a489c7e82a9836e4175a768ac5d4b69c1dba8e5",
1581
+ "mls/native/src/portable/tests.rs": "7eadba2a49ed044470bb567d9b93fba44b64dc117b1da50a22187cff3dccb64e",
1582
+ "mls/native/portable/build.mjs": "84c422a8a8aa6a7ec112a358bdeef080201aa4ea9a91f6d5726b05f46fa2a10c",
1583
+ "mls/core/open.rs": "8cdc03882f3b84dca72ab06afd437f162de01a0fe21327b019f17867dd59f5d0",
1584
+ "mls/core/protocol.rs": "7f3e021d6e4f1cc130b5dd3c98e0b0fa23edf813c72802c3fa3f8fb9e431c947",
1585
+ "mls/core/fault_storage.rs": "fdd6672a21f6ffee657142755637824541a5090b4f8a5fb4902f482556fb2793",
1586
+ "mls/core/mls_environment.rs": "390d11ee82ef8b108a7fffc7bb193ac7f3e470aa14e9643d029007aaaa29c4e4",
1587
+ "mls/core/dev_identity.rs": "97df6bdb9daab66f0e3d4ca3b49bf9954fa06ac032c927d9ae6c83509b0166bd",
1588
+ "mls/core/dev_key_package.rs": "3e7011fe8ab3044d80d84d04944068dbcf683141fcaba8a7a00bd2cfd103de72",
1589
+ "mls/core/dev_application.rs": "fafe245ace38e384da265bab2c12fef6b77c86b2c20477d00563ab891a54f94b",
1590
+ "mls/core/dev_group.rs": "1d519a5d15394bbc163aef00e0ddacb9bb87631beaf8a13e52c76d628749152b",
1591
+ "packages/test-fixtures/src/mlsDevEnrollmentProofV1.json": "50d7896e0a3a25b420412e84484e64f1e49480a3f298ee2d125f2f9188382810",
1592
+ "packages/test-fixtures/src/mlsDevCredentialV1.json": "24cd23af794b8a6e35caf050995020c6584b806de060969db4536f44b1188780",
1593
+ "packages/test-fixtures/src/mlsDevCredentialReconciliationV1.json": "bd4bbb3a2e5dc7921867a2e5936d1da77d536740b4360012c1f741f9caed2826",
1594
+ "packages/test-fixtures/src/mlsDevGroupV1.json": "10b7c0da00cfd3ecfb3c9e7f0118bc574be613cc82d97268766ba271c464ca9d",
1595
+ "packages/test-fixtures/src/mlsDevApplicationV1.json": "77c05e728acb7793a70c03a6857bc8b0e522e586b117241432460eec05ae3673",
1596
+ "packages/test-fixtures/src/mlsDevInteractionV1.json": "045ee34e1b5e126b67da94705b3f03a1d265774d7889ec3f4f272dd06ecef676"
1597
+ },
1598
+ platforms: {
1599
+ "darwin-arm64": {
1600
+ target: "aarch64-apple-darwin",
1601
+ sha256: "918ae350f1d7bd339c4eee548f6668943217f5fa66022bb93f41a02992dd296c",
1602
+ bytes: 2932976,
1603
+ unitTestsPassed: 95,
1604
+ commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1605
+ builder: {
1606
+ macos: "26.3.1",
1607
+ sdk: "26.4",
1608
+ linker: "@(#)PROGRAM:ld PROJECT:ld-1266.8"
1609
+ }
1610
+ },
1611
+ "linux-x64": {
1612
+ target: "x86_64-unknown-linux-gnu",
1613
+ sha256: "1c5a12d0213c53bdf59b5f36a7390a4b4c17f8a53c95e760ad8cb4905297d256",
1614
+ bytes: 3389816,
1615
+ unitTestsPassed: 95,
1616
+ commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1617
+ builder: {
1618
+ image: "quay.io/pypa/manylinux_2_28_x86_64@sha256:e0adf18521ad35192df733a6c62d35df7801957d24e0470238aacecfab2188a3",
1619
+ dockerfileSha256: "fe8d4895b8bd304954441fd27c6273c8c00baa10a1ad912d3c3851b193b8ad75"
1620
+ }
1621
+ },
1622
+ "linux-arm64": {
1623
+ target: "aarch64-unknown-linux-gnu",
1624
+ sha256: "371c5754cf0c437f561e8c49b3ea654c037311b951e11b1c5a29fb6b91656619",
1625
+ bytes: 3234232,
1626
+ unitTestsPassed: 95,
1627
+ commit: "cdc6ce8fd9d7a47bc5c796d19a72828e4cc9db06",
1628
+ builder: {
1629
+ image: "quay.io/pypa/manylinux_2_28_aarch64@sha256:a3e02308c822fb6ce0d12248b0c941817ba5e1a1bc9e7bb5929f29c50eaa40a9",
1630
+ dockerfileSha256: "fe8d4895b8bd304954441fd27c6273c8c00baa10a1ad912d3c3851b193b8ad75"
1631
+ }
1632
+ }
1633
+ }
1634
+ };
1635
+
1636
+ // src/runner/mls-bundled.ts
1637
+ var MLS_ENVIRONMENT_PINS = {
1638
+ "canon-prod-v1": {
1639
+ issuerKeys: [{
1640
+ keyVersion: "projects/canonmail-prod/locations/europe-west1/keyRings/canon-mls/cryptoKeys/device-issuer/cryptoKeyVersions/1",
1641
+ signaturePublicKey: "AYLEqY7gKOhQvRALb-31lz82XinehW2_gl0jKC-G8Pk"
1642
+ }],
1643
+ receiptKeys: [{
1644
+ keyVersion: "projects/canonmail-prod/locations/europe-west1/keyRings/canon-mls/cryptoKeys/feed-receipt/cryptoKeyVersions/1",
1645
+ signaturePublicKey: "sRi4mvMbFTL-LKzXilIJedefSqtqrF8Q-deM2AUSt40"
1646
+ }]
1647
+ },
1648
+ "canon-dev-v1": {
1649
+ // Dev signs with version 2 and still trusts version 1 until 2026-11-07.
1650
+ issuerKeys: [
1651
+ {
1652
+ keyVersion: "projects/canonmail-dev/locations/europe-west1/keyRings/canon-mls/cryptoKeys/device-issuer/cryptoKeyVersions/1",
1653
+ signaturePublicKey: "Hm1ULJGsF9MIQbHYIWmpkK3j2dPcFzeJ71xbYVsPZNs"
1654
+ },
1655
+ {
1656
+ keyVersion: "projects/canonmail-dev/locations/europe-west1/keyRings/canon-mls/cryptoKeys/device-issuer/cryptoKeyVersions/2",
1657
+ signaturePublicKey: "G2fwJdPQXP6Ar68LJveXQ_WAOtdtcsiyKdeEVb-9Gcw"
1658
+ }
1659
+ ],
1660
+ receiptKeys: [{
1661
+ keyVersion: "projects/canonmail-dev/locations/europe-west1/keyRings/canon-mls/cryptoKeys/feed-receipt/cryptoKeyVersions/1",
1662
+ signaturePublicKey: "LSRMjRHH73Q4ExPFSTaU_KTvr8wQ4Cp9vtaBBrVZvig"
1663
+ }]
1664
+ }
1665
+ };
1666
+ function packageRoot() {
1667
+ for (let directory = dirname4(fileURLToPath(import.meta.url)); ; directory = dirname4(directory)) {
1668
+ const manifest = join2(directory, "package.json");
1669
+ if (existsSync(manifest) && JSON.parse(readFileSync2(manifest, "utf8")).name === "@canonmsg/agent-sdk") return directory;
1670
+ if (dirname4(directory) === directory) return void 0;
1671
+ }
1672
+ }
1673
+ __name(packageRoot, "packageRoot");
1674
+ function bundledMlsCustodyHelper() {
1675
+ const platform = `${process.platform}-${process.arch}`;
1676
+ const pin = mls_custody_default.platforms[platform];
1677
+ const root = pin && packageRoot();
1678
+ const path = root && join2(root, "dist", "mls", platform, "canon-mls-custody");
1679
+ if (!pin || !path || !existsSync(path)) return void 0;
1680
+ if (createHash5("sha256").update(readFileSync2(path)).digest("hex") !== pin.sha256) {
1681
+ throw new Error(`${path} is not the custody helper this package pins; reinstall @canonmsg/agent-sdk.`);
1682
+ }
1683
+ return { path, sha256: pin.sha256 };
1684
+ }
1685
+ __name(bundledMlsCustodyHelper, "bundledMlsCustodyHelper");
1686
+
1687
+ // src/runner/mls-managed.ts
1688
+ import { createHash as createHash6 } from "node:crypto";
1689
+ import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2 } from "node:fs";
1690
+ import { dirname as dirname5, join as join3 } from "node:path";
1691
+ var sha256 = /* @__PURE__ */ __name((bytes) => createHash6("sha256").update(bytes).digest("hex"), "sha256");
1692
+ function managedMlsDevice(options) {
1693
+ const { environmentId } = options;
1694
+ const helper = options.helper ?? bundledMlsCustodyHelper();
1695
+ if (!helper) {
1696
+ 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
+ }
1698
+ const pins = options.pins ?? MLS_ENVIRONMENT_PINS[environmentId];
1699
+ const configuration = parseMlsDevClientConfiguration({
1700
+ version: 1,
1701
+ environmentId,
1702
+ enabled: true,
1703
+ issuerKeys: pins.issuerKeys,
1704
+ receiptKeys: pins.receiptKeys
1705
+ });
1706
+ const root = join3(options.home, "mls", environmentId);
1707
+ const directory = /* @__PURE__ */ __name((context) => join3(root, sha256(Buffer.from(JSON.stringify([
1708
+ context.installationId,
1709
+ context.principal.type,
1710
+ context.principal.id,
1711
+ "ownershipVersion" in context.principal ? context.principal.ownershipVersion : null
1712
+ ]))).slice(0, 32)), "directory");
1713
+ const vault = /* @__PURE__ */ __name((context) => join3(directory(context), "vault", "vault.bin"), "vault");
1714
+ let executable;
1715
+ function install() {
1716
+ const target = join3(root, "bin", helper.sha256, "canon-mls-custody");
1717
+ if (existsSync2(target) && sha256(readFileSync3(target)) === helper.sha256) return target;
1718
+ mkdirSync3(dirname5(target), { recursive: true, mode: 448 });
1719
+ const temporary = `${target}.${process.pid}.tmp`;
1720
+ copyFileSync(helper.path, temporary);
1721
+ chmodSync(temporary, 448);
1722
+ if (sha256(readFileSync3(temporary)) !== helper.sha256) throw new Error(`${helper.path} is not the pinned custody helper`);
1723
+ renameSync2(temporary, target);
1724
+ return target;
1725
+ }
1726
+ __name(install, "install");
1727
+ const devices = /* @__PURE__ */ new Map();
1728
+ function device(context, provisioning) {
1729
+ const key = directory(context);
1730
+ let found = devices.get(key);
1731
+ if (!found) {
1732
+ found = (async () => {
1733
+ if (!provisioning && !existsSync2(vault(context))) throw new NodeMlsDevCustodyError();
1734
+ mkdirSync3(dirname5(vault(context)), { recursive: true, mode: 448 });
1735
+ const unlock = join3(key, "unlock");
1736
+ if (provisioning) privateKey(unlock, 32);
1737
+ const text3 = `${JSON.stringify({
1738
+ version: 1,
1739
+ environmentId,
1740
+ context: {
1741
+ version: 1,
1742
+ environmentId: context.environmentId,
1743
+ principal: context.principal,
1744
+ installationId: context.installationId
1745
+ },
1746
+ vaultPath: vault(context),
1747
+ issuerKeys: configuration.issuerKeys,
1748
+ receiptKeys: configuration.receiptKeys
1749
+ }, null, 2)}
1750
+ `;
1751
+ const file = join3(key, "native.json");
1752
+ if (!existsSync2(file) || readFileSync3(file, "utf8") !== text3) writeDurable(file, text3);
1753
+ return createPortableNodeMlsDevCustody({
1754
+ executablePath: executable ??= install(),
1755
+ executableSha256: helper.sha256,
1756
+ configurationPath: file,
1757
+ configurationSha256: sha256(Buffer.from(text3)),
1758
+ secretSource: { kind: "managed-file", path: unlock },
1759
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
1760
+ });
1761
+ })();
1762
+ devices.set(key, found);
1763
+ found.catch(() => {
1764
+ if (devices.get(key) === found) devices.delete(key);
1765
+ });
1766
+ }
1767
+ return found;
1768
+ }
1769
+ __name(device, "device");
1770
+ const custody2 = {
1771
+ prepareProtected: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open4) => open4.prepareProtected(context, ...rest)), "prepareProtected"),
1772
+ prepareProtectedKeyPackage: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open4) => open4.prepareProtectedKeyPackage(context, ...rest)), "prepareProtectedKeyPackage"),
1773
+ prepareProtectedGroup: /* @__PURE__ */ __name((identity, ...rest) => device(identity, false).then((open4) => open4.prepareProtectedGroup(identity, ...rest)), "prepareProtectedGroup")
1774
+ };
1775
+ return {
1776
+ configuration,
1777
+ custody: custody2,
1778
+ provision: /* @__PURE__ */ __name((context) => device(context, true).then((open4) => open4.provision(context)), "provision"),
1779
+ provisioned: /* @__PURE__ */ __name((context) => existsSync2(vault(context)), "provisioned"),
1780
+ async close() {
1781
+ await Promise.allSettled([...devices.values()].map((open4) => open4.then((custody3) => custody3.close())));
1782
+ }
1783
+ };
1784
+ }
1785
+ __name(managedMlsDevice, "managedMlsDevice");
1786
+
1544
1787
  // src/runner/relay.ts
1545
1788
  import { createHmac as createHmac2, timingSafeEqual } from "node:crypto";
1546
- import { closeSync as closeSync3, constants as constants5, fsyncSync as fsyncSync2, mkdirSync as mkdirSync3, openSync as openSync3, renameSync as renameSync2, writeSync as writeSync2 } from "node:fs";
1789
+ import { closeSync as closeSync3, constants as constants5, fsyncSync as fsyncSync2, mkdirSync as mkdirSync4, openSync as openSync3, renameSync as renameSync3, writeSync as writeSync2 } from "node:fs";
1547
1790
  import { lstat as lstat2, open as open3, realpath as realpath4 } from "node:fs/promises";
1548
1791
  import { createServer } from "node:http";
1549
- import { isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
1792
+ import { isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
1550
1793
  var TOLERANCE_S = 300;
1551
1794
  function relaySecretKey(secret) {
1552
1795
  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");
@@ -1715,8 +1958,8 @@ async function loadRelayConfiguration(path, input) {
1715
1958
  return { environmentId: value.environmentId, agentId: value.agentId, listen: { host: listen.host, port: listen.port }, secret };
1716
1959
  }
1717
1960
  __name(loadRelayConfiguration, "loadRelayConfiguration");
1718
- var relayDirectory = /* @__PURE__ */ __name((home) => join2(home, "relay"), "relayDirectory");
1719
- var credentialFile = /* @__PURE__ */ __name((home) => join2(relayDirectory(home), "credential.json"), "credentialFile");
1961
+ var relayDirectory = /* @__PURE__ */ __name((home) => join4(home, "relay"), "relayDirectory");
1962
+ var credentialFile = /* @__PURE__ */ __name((home) => join4(relayDirectory(home), "credential.json"), "credentialFile");
1720
1963
  async function readRelayCredential(home, relay) {
1721
1964
  const path = credentialFile(home);
1722
1965
  if (!await lstat2(path).then(() => true, () => false)) return void 0;
@@ -1742,7 +1985,7 @@ function writeRelayCredential(home, relay, delivery) {
1742
1985
  generation: delivery.generation,
1743
1986
  credential: delivery.credential
1744
1987
  };
1745
- mkdirSync3(relayDirectory(home), { recursive: true, mode: 448 });
1988
+ mkdirSync4(relayDirectory(home), { recursive: true, mode: 448 });
1746
1989
  const file = credentialFile(home);
1747
1990
  const temporary = `${file}.${process.pid}.tmp`;
1748
1991
  const fd = openSync3(temporary, "w", 384);
@@ -1752,23 +1995,23 @@ function writeRelayCredential(home, relay, delivery) {
1752
1995
  } finally {
1753
1996
  closeSync3(fd);
1754
1997
  }
1755
- renameSync2(temporary, file);
1998
+ renameSync3(temporary, file);
1756
1999
  return value;
1757
2000
  }
1758
2001
  __name(writeRelayCredential, "writeRelayCredential");
1759
2002
  function relayRunnerHome(home, installationId) {
1760
2003
  if (!CONNECTOR_RELAY_INSTALLATION_PATTERN.test(installationId)) throw new Error("Invalid relay installation");
1761
- const directory = join2(relayDirectory(home), installationId.slice(CONNECTOR_RELAY_INSTALLATION_PREFIX.length));
1762
- mkdirSync3(directory, { recursive: true, mode: 448 });
2004
+ const directory = join4(relayDirectory(home), installationId.slice(CONNECTOR_RELAY_INSTALLATION_PREFIX.length));
2005
+ mkdirSync4(directory, { recursive: true, mode: 448 });
1763
2006
  return directory;
1764
2007
  }
1765
2008
  __name(relayRunnerHome, "relayRunnerHome");
1766
2009
 
1767
2010
  // src/runner/runner.ts
1768
- import { createHash as createHash6 } from "node:crypto";
1769
- import { readFileSync as readFileSync3, rmSync as rmSync3 } from "node:fs";
2011
+ import { createHash as createHash8 } from "node:crypto";
2012
+ import { readFileSync as readFileSync5, rmSync as rmSync3 } from "node:fs";
1770
2013
  import { realpath as realpath5, stat as stat2 } from "node:fs/promises";
1771
- import { basename as basename3, isAbsolute as isAbsolute3, join as join4, sep } from "node:path";
2014
+ import { basename as basename3, isAbsolute as isAbsolute3, join as join6, sep } from "node:path";
1772
2015
 
1773
2016
  // ../agent-tools/src/communication-tool.ts
1774
2017
  var CANON_COMMUNICATE_TOOL_NAME = "communicate";
@@ -2262,10 +2505,10 @@ function createLivePublisher(options) {
2262
2505
  __name(createLivePublisher, "createLivePublisher");
2263
2506
 
2264
2507
  // src/runner/mls-ask.ts
2265
- import { createHash as createHash5 } from "node:crypto";
2266
- import { readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
2267
- import { join as join3 } from "node:path";
2268
- var sha256 = /* @__PURE__ */ __name((value) => createHash5("sha256").update(value).digest("hex"), "sha256");
2508
+ import { createHash as createHash7 } from "node:crypto";
2509
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
2510
+ import { join as join5 } from "node:path";
2511
+ var sha2562 = /* @__PURE__ */ __name((value) => createHash7("sha256").update(value).digest("hex"), "sha256");
2269
2512
  var pause = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
2270
2513
  setTimeout(resolve3, ms).unref?.();
2271
2514
  }), "pause");
@@ -2319,10 +2562,10 @@ function createMlsAsks(options, approvalScope2) {
2319
2562
  });
2320
2563
  }
2321
2564
  });
2322
- const file = /* @__PURE__ */ __name((conversationId, requestId) => join3(options.directory, `${sha256(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2565
+ const file = /* @__PURE__ */ __name((conversationId, requestId) => join5(options.directory, `${sha2562(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2323
2566
  const save = /* @__PURE__ */ __name((journal) => writeDurable(file(journal.conversationId, journal.requestId), JSON.stringify(journal)), "save");
2324
2567
  const forget = /* @__PURE__ */ __name((journal) => rmSync2(file(journal.conversationId, journal.requestId), { force: true }), "forget");
2325
- const operationOf = /* @__PURE__ */ __name((conversationId, messageId) => `runner-mls-ask:${sha256(JSON.stringify([conversationId, messageId])).slice(0, 48)}`, "operationOf");
2568
+ const operationOf = /* @__PURE__ */ __name((conversationId, messageId) => `runner-mls-ask:${sha2562(JSON.stringify([conversationId, messageId])).slice(0, 48)}`, "operationOf");
2326
2569
  const withdrawalOf = /* @__PURE__ */ __name((journal) => operationOf(journal.conversationId, `${journal.requestId}:withdraw`), "withdrawalOf");
2327
2570
  const audienceChanged = /* @__PURE__ */ __name((error) => codeOf(error) === "MLS_DEV_AUDIENCE_CHANGED", "audienceChanged");
2328
2571
  const sameHead = /* @__PURE__ */ __name((a, b) => JSON.stringify(a.head) === JSON.stringify(b.head), "sameHead");
@@ -2663,11 +2906,11 @@ function createMlsAsks(options, approvalScope2) {
2663
2906
  for (const name of names) {
2664
2907
  let journal;
2665
2908
  try {
2666
- journal = JSON.parse(readFileSync2(join3(options.directory, name), "utf8"));
2909
+ journal = JSON.parse(readFileSync4(join5(options.directory, name), "utf8"));
2667
2910
  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();
2668
2911
  for (const sent of journal.sends) sent.audience = snapshotMlsAudience(sent.audience);
2669
2912
  } catch {
2670
- rmSync2(join3(options.directory, name), { force: true });
2913
+ rmSync2(join5(options.directory, name), { force: true });
2671
2914
  continue;
2672
2915
  }
2673
2916
  try {
@@ -2825,7 +3068,7 @@ function answerText(answer) {
2825
3068
  return answer.text?.trim() ? answer.text : "They answered with an empty message.";
2826
3069
  }
2827
3070
  __name(answerText, "answerText");
2828
- var sha2562 = /* @__PURE__ */ __name((value) => createHash6("sha256").update(value).digest("hex"), "sha256");
3071
+ var sha2563 = /* @__PURE__ */ __name((value) => createHash8("sha256").update(value).digest("hex"), "sha256");
2829
3072
  var toolText = /* @__PURE__ */ __name((text3, isError = false) => ({ content: [{ type: "text", text: text3 }], ...isError ? { isError } : {} }), "toolText");
2830
3073
  var turnEnded2 = /* @__PURE__ */ __name(() => new DOMException("This turn has ended", "AbortError"), "turnEnded");
2831
3074
  function within(promise, ms) {
@@ -2841,9 +3084,10 @@ var pause2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
2841
3084
  }), "pause");
2842
3085
  var audienceOf = /* @__PURE__ */ __name((room) => room?.memberIds ? { memberIds: [...room.memberIds], ...typeof room.membershipRevision === "number" ? { membershipRevision: room.membershipRevision } : {} } : null, "audienceOf");
2843
3086
  var inert = /* @__PURE__ */ __name((value) => value.replace(/\s+/g, " ").trim(), "inert");
3087
+ var OWNER_MARK = " [your owner]";
2844
3088
  var TRUSTED_MARK = " [trusted teammate agent]";
2845
3089
  var speaker = /* @__PURE__ */ __name((name) => inert(name).replace(/\[/g, "(").replace(/\]/g, ")"), "speaker");
2846
- var defused = /* @__PURE__ */ __name((text3) => text3.replace(/\[(\s*trusted\s+teammate\s+agent\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?owner['\u2019]s\s+(?:private\s+note|errand)\s*)\]/gi, "($1)"), "defused");
3090
+ var defused = /* @__PURE__ */ __name((text3) => text3.replace(/\[(\s*(?:trusted\s+teammate\s+agent|your\s+owner)\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?owner['\u2019]s\s+(?:private\s+note|errand)\s*)\]/gi, "($1)"), "defused");
2847
3091
  var NOTE_OPEN = "[owner's private note] Your owner left this for you on the machine you run on. It is not a message in this conversation: no one in it sees it, and it was not posted.";
2848
3092
  var NOTE_CLOSE = "[end of owner's private note]";
2849
3093
  var ERRAND_OPEN = "[owner's errand]";
@@ -2858,8 +3102,8 @@ var errandCommunicate = /* @__PURE__ */ __name((contact) => ({
2858
3102
  properties: { action: { const: "start_conversation" }, principalId: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 } }
2859
3103
  }] : []] }
2860
3104
  }), "errandCommunicate");
2861
- var promptLine = /* @__PURE__ */ __name((message2, name, media, trusted = false) => [
2862
- `${speaker(name)}${trusted ? TRUSTED_MARK : ""}: ${defused(renderCanonHostInboundContent(message2, media?.files))}`,
3105
+ var promptLine = /* @__PURE__ */ __name((message2, name, media, mark = "") => [
3106
+ `${speaker(name)}${mark}: ${defused(renderCanonHostInboundContent(message2, media?.files))}`,
2863
3107
  ...(media?.failed ?? []).map((label) => `[Attachment "${defused(inert(label))}" could not be downloaded]`)
2864
3108
  ].join("\n"), "promptLine");
2865
3109
  var inputMessage = /* @__PURE__ */ __name((message2, trusted = false) => Object.freeze({
@@ -2896,7 +3140,7 @@ function approvalScope(request) {
2896
3140
  if (request.scopeKey !== void 0 && /^[A-Za-z0-9_:-]{1,64}$/.test(request.scopeKey)) return request.scopeKey;
2897
3141
  const key = request.scopeKey ?? request.title;
2898
3142
  const slug = key.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "approval";
2899
- return `${slug}-${sha2562(JSON.stringify([request.scopeKey === void 0 ? "title" : "scope", key])).slice(0, 16)}`;
3143
+ return `${slug}-${sha2563(JSON.stringify([request.scopeKey === void 0 ? "title" : "scope", key])).slice(0, 16)}`;
2900
3144
  }
2901
3145
  __name(approvalScope, "approvalScope");
2902
3146
  var MAX_GROUP_PARTS = 100;
@@ -3053,7 +3297,7 @@ async function createRunner(options) {
3053
3297
  const dispose = /* @__PURE__ */ __name((inputs, kind, reason) => Promise.allSettled(inputs.map((input) => input.handle[kind](reason))).then(() => void 0), "dispose");
3054
3298
  const settle = /* @__PURE__ */ __name(async (ids, reason) => (await Promise.allSettled(ids.map((id) => endpoint.setInboundState(id, "settled", reason)))).every(({ status }) => status === "fulfilled"), "settle");
3055
3299
  const queued = /* @__PURE__ */ __name((turn) => turn.inputs.filter((input) => input.handle.phase === "queued"), "queued");
3056
- const turnId = /* @__PURE__ */ __name((roomId, inputs) => `turn-${sha2562(JSON.stringify([roomId, ...inputs.map((input) => [input.id, input.generation])])).slice(0, 32)}`, "turnId");
3300
+ const turnId = /* @__PURE__ */ __name((roomId, inputs) => `turn-${sha2563(JSON.stringify([roomId, ...inputs.map((input) => [input.id, input.generation])])).slice(0, 32)}`, "turnId");
3057
3301
  const livePublisher = /* @__PURE__ */ __name((current, turn, mode) => createLivePublisher({
3058
3302
  client,
3059
3303
  conversationId: current.id,
@@ -3069,7 +3313,7 @@ async function createRunner(options) {
3069
3313
  current.lane = current.lane.then(publish2).catch(() => void 0);
3070
3314
  }, "enqueue")
3071
3315
  }), "livePublisher");
3072
- const stateFile = /* @__PURE__ */ __name((session) => join4(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2562(JSON.stringify([scope, session]))}.json`), "stateFile");
3316
+ const stateFile = /* @__PURE__ */ __name((session) => join6(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2563(JSON.stringify([scope, session]))}.json`), "stateFile");
3073
3317
  const resetSession = /* @__PURE__ */ __name(async (session) => {
3074
3318
  rmSync3(stateFile(session), { force: true });
3075
3319
  mlsAsks?.endSession(session);
@@ -3189,6 +3433,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3189
3433
  const verified = {
3190
3434
  id: message2.id,
3191
3435
  senderId: message2.senderId,
3436
+ ...message2.senderName ? { senderName: message2.senderName } : {},
3192
3437
  senderType: fromAgent ? "ai_agent" : "human",
3193
3438
  isOwner: !fromAgent && message2.senderId === ownerId,
3194
3439
  text: message2.text ?? void 0,
@@ -3399,7 +3644,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3399
3644
  return check;
3400
3645
  }
3401
3646
  __name(reaudience, "reaudience");
3402
- const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join4(home, "runner", options.environmentId, agentId) }), "inboundMedia");
3647
+ const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join6(home, "runner", options.environmentId, agentId) }), "inboundMedia");
3403
3648
  async function materialize(current, turn) {
3404
3649
  if (driver.capabilities.remote) return;
3405
3650
  for (const { payload: { message: message2 } } of turn.inputs) {
@@ -3450,7 +3695,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3450
3695
  ask_question: /* @__PURE__ */ __name(async (args) => {
3451
3696
  const question = questionOf(args);
3452
3697
  if (typeof question === "string") return { result: toolText(question, true) };
3453
- const key = sha2562(JSON.stringify(question));
3698
+ const key = sha2563(JSON.stringify(question));
3454
3699
  const waiting = questions.get(key);
3455
3700
  const entry = waiting?.waiting ? waiting : { id: `canon:ask_question:${asked += 1}`, waiting: true };
3456
3701
  questions.set(key, entry);
@@ -3768,7 +4013,7 @@ ${JSON.stringify(result, null, 2)}`);
3768
4013
  messageId: `${root}-part-${index + 1}`,
3769
4014
  metadata: buildChunkedMessagePartMetadata({ groupId: root, metadata: options2.metadata, index: index + 1, count: bodies.length })
3770
4015
  } }));
3771
- return client.prepareMessageGroup(record.conversationId, root, parts, { signal, ...replaces ? { operationId: sha2562(JSON.stringify([root, ...attachments.map((attachment) => attachment.uploadId)])), replacesOperationId: replaces } : {} }).then((group2) => ({ ...group2, count: parts.length }));
4016
+ return client.prepareMessageGroup(record.conversationId, root, parts, { signal, ...replaces ? { operationId: sha2563(JSON.stringify([root, ...attachments.map((attachment) => attachment.uploadId)])), replacesOperationId: replaces } : {} }).then((group2) => ({ ...group2, count: parts.length }));
3772
4017
  }, "register");
3773
4018
  const original = await register(record.attachments);
3774
4019
  let group = original;
@@ -3891,7 +4136,7 @@ ${JSON.stringify(result, null, 2)}`);
3891
4136
  return { items, refused: refused2 };
3892
4137
  }
3893
4138
  __name(admitItems, "admitItems");
3894
- const encryptedOperation = /* @__PURE__ */ __name((audience, messageId) => `runner-mls:${sha2562(JSON.stringify([audience.conversationId, messageId])).slice(0, 48)}`, "encryptedOperation");
4139
+ const encryptedOperation = /* @__PURE__ */ __name((audience, messageId) => `runner-mls:${sha2563(JSON.stringify([audience.conversationId, messageId])).slice(0, 48)}`, "encryptedOperation");
3895
4140
  function forgetGuard(audience, messageId, sent) {
3896
4141
  if (sent === "pending") return;
3897
4142
  const operationId = encryptedOperation(audience, messageId);
@@ -3919,7 +4164,7 @@ ${JSON.stringify(result, null, 2)}`);
3919
4164
  ...turn.contexts.length ? { errands: Object.freeze(turn.contexts.map(({ record }) => errandFacts(record))) } : {},
3920
4165
  ...turn.errand ? { errand: errandFacts(turn.errand.record) } : {}
3921
4166
  });
3922
- const line = /* @__PURE__ */ __name((message2, trusted = false) => promptLine(message2, message2.senderId === agentId ? "You" : message2.senderName ?? message2.senderId, turn.media.get(message2.id), trusted), "line");
4167
+ const line = /* @__PURE__ */ __name((message2, mark = "") => promptLine(message2, message2.senderId === agentId ? "You" : message2.senderName ?? message2.senderId, turn.media.get(message2.id), mark), "line");
3923
4168
  const open4 = /* @__PURE__ */ __name(() => turn.open && !turn.controller.signal.aborted, "open");
3924
4169
  const live = /* @__PURE__ */ __name(() => open4() && !turn.silenced, "live");
3925
4170
  return {
@@ -3929,7 +4174,7 @@ ${JSON.stringify(result, null, 2)}`);
3929
4174
  ...turn.notes.flatMap((note) => [NOTE_OPEN, defused(note.text), NOTE_CLOSE, ""]),
3930
4175
  ...turn.contexts.flatMap((context) => [...contextPrompt(context, !!turn.mls), ""]),
3931
4176
  ...history.length ? ["Earlier in this conversation (context, not new requests):", ...history.map((message2) => line(message2)), "", "New messages:"] : turn.notes.length || turn.contexts.length ? ["New messages:"] : [],
3932
- ...turn.inputs.map(({ payload: { message: message2 } }) => line(message2, trustedSender(message2)))
4177
+ ...turn.inputs.map(({ payload: { message: message2 } }) => line(message2, message2.isOwner === true ? OWNER_MARK : trustedSender(message2) ? TRUSTED_MARK : ""))
3933
4178
  ].join("\n"),
3934
4179
  input,
3935
4180
  files: turn.inputs.flatMap(({ payload: { message: message2 } }) => turn.media.get(message2.id)?.files.map((file) => file.path) ?? []),
@@ -3989,7 +4234,7 @@ ${JSON.stringify(result, null, 2)}`);
3989
4234
  }));
3990
4235
  const secret = request.kind === "text" && request.secret === true;
3991
4236
  const responder = secret || sender.type !== "human" ? ownerId : sender.id;
3992
- const requestId = turn.mls ? keyed().request(roomId, turn.id, request.id) : `ask-${sha2562(JSON.stringify([turn.id, request.id])).slice(0, 32)}`;
4237
+ const requestId = turn.mls ? keyed().request(roomId, turn.id, request.id) : `ask-${sha2563(JSON.stringify([turn.id, request.id])).slice(0, 32)}`;
3993
4238
  const expiresAt = Date.now() + askExpiryMs;
3994
4239
  if (turn.mls) {
3995
4240
  return await mlsAsks.ask({
@@ -4061,7 +4306,7 @@ ${JSON.stringify(result, null, 2)}`);
4061
4306
  const file = stateFile(session);
4062
4307
  let data = {};
4063
4308
  try {
4064
- data = JSON.parse(readFileSync3(file, "utf8"));
4309
+ data = JSON.parse(readFileSync5(file, "utf8"));
4065
4310
  } catch {
4066
4311
  }
4067
4312
  const save = /* @__PURE__ */ __name((next) => {
@@ -4278,7 +4523,7 @@ ${JSON.stringify(result, null, 2)}`);
4278
4523
  log(`Errand ${id} (tell) kept for the next turn in ${conversationId}.`);
4279
4524
  return true;
4280
4525
  }
4281
- const turnId2 = `turn-${sha2562(JSON.stringify(["canon.runner.errand", id])).slice(0, 32)}`;
4526
+ const turnId2 = `turn-${sha2563(JSON.stringify(["canon.runner.errand", id])).slice(0, 32)}`;
4282
4527
  const record = {
4283
4528
  version: 1,
4284
4529
  turnId: turnId2,
@@ -4347,7 +4592,7 @@ ${JSON.stringify(result, null, 2)}`);
4347
4592
  pump();
4348
4593
  }
4349
4594
  __name(enqueueErrand, "enqueueErrand");
4350
- const errandTurnId = /* @__PURE__ */ __name((id) => `turn-${sha2562(JSON.stringify(["canon.runner.errand-turn", id])).slice(0, 32)}`, "errandTurnId");
4595
+ const errandTurnId = /* @__PURE__ */ __name((id) => `turn-${sha2563(JSON.stringify(["canon.runner.errand-turn", id])).slice(0, 32)}`, "errandTurnId");
4351
4596
  const errandFacts = /* @__PURE__ */ __name((record) => Object.freeze({
4352
4597
  id: record.id,
4353
4598
  why: record.why,
@@ -4683,7 +4928,7 @@ ${JSON.stringify(result, null, 2)}`);
4683
4928
  client = new CanonClient(options.apiKey, connection.apiBaseUrl, {
4684
4929
  environmentId: connection.environmentId,
4685
4930
  streamUrl: connection.streamUrl,
4686
- stateDirectory: join4(home, "endpoint"),
4931
+ stateDirectory: join6(home, "endpoint"),
4687
4932
  ...options.client ? { client: options.client } : {},
4688
4933
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
4689
4934
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
@@ -4704,7 +4949,14 @@ ${JSON.stringify(result, null, 2)}`);
4704
4949
  if (options.wake !== void 0) await client.configureWake(options.wake);
4705
4950
  mlsOn = !!(endpoint.mlsDevDevice && endpoint.mlsDevGroups);
4706
4951
  if (!mlsOn && (await endpoint.capabilities().catch(() => null))?.mls?.roomMode === "default") {
4707
- throw new Error("Canon encrypts every new conversation here, and this runner has no MLS device: run it with --mls-dev-config <path> (set one up with canon-agent mls).");
4952
+ throw new Error("Canon encrypts every new conversation here, and this runner has no MLS device: give it mls (managedMlsDevice, or an operator's configuration: --mls-dev-config <path>).");
4953
+ }
4954
+ if (mlsOn && options.mls?.provisioned && (await endpoint.mlsDevDevice.read()).status !== "credential_committed") {
4955
+ log("Setting up this agent's MLS device");
4956
+ const device = await runMlsDevSetup(endpoint, "setup", void 0, options.mls.provision, options.mls.provisioned).catch((error) => {
4957
+ throw new Error(`Could not set up this agent's MLS device: ${error instanceof Error ? error.message : String(error)}`);
4958
+ });
4959
+ log(`MLS device ${device.deviceId} is ${device.status}`);
4708
4960
  }
4709
4961
  if (mlsOn) await endpoint.mlsDevDevice.ensureReady().catch(warn("MLS device not ready; encrypted rooms wait for it"));
4710
4962
  const keepPool = /* @__PURE__ */ __name(() => endpoint.mlsDevDevice.read().then(async (view) => {
@@ -4721,23 +4973,23 @@ ${JSON.stringify(result, null, 2)}`);
4721
4973
  }
4722
4974
  scope = JSON.stringify([identity.environmentId, agentId, identity.ownershipVersion ?? 0, identity.installationId]);
4723
4975
  if (mlsOn) {
4724
- mlsIds = createMlsIds(privateKey(join4(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2562(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
4976
+ mlsIds = createMlsIds(privateKey(join6(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2563(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
4725
4977
  mlsAsks = createMlsAsks({
4726
4978
  endpoint,
4727
4979
  agentId,
4728
- directory: join4(home, "runner", options.environmentId, agentId, "asks", sha2562(scope).slice(0, 32)),
4980
+ directory: join6(home, "runner", options.environmentId, agentId, "asks", sha2563(scope).slice(0, 32)),
4729
4981
  assertAudience,
4730
4982
  sessionForDestructive: options.sessionApprovalForDestructive === true,
4731
4983
  log
4732
4984
  }, approvalScope);
4733
4985
  }
4734
- journal = createCompletedOutputJournal(join4(home, "runner", options.environmentId, agentId, "finals"), scope, {
4986
+ journal = createCompletedOutputJournal(join6(home, "runner", options.environmentId, agentId, "finals"), scope, {
4735
4987
  label: "agent runner",
4736
4988
  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")
4737
4989
  });
4738
4990
  if (!options.relay && !driver.capabilities.remote) {
4739
4991
  errands = errandJournal(errandsDirectory(home, options.environmentId, agentId));
4740
- errandScope = sha2562(scope).slice(0, 32);
4992
+ errandScope = sha2563(scope).slice(0, 32);
4741
4993
  const pending = errands.list().filter((record) => record.kind === "tell" && record.status === "pending").sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? "") || a.id.localeCompare(b.id));
4742
4994
  for (const note of pending) {
4743
4995
  if (note.scope === errandScope) notes.set(note.conversationId, [...notes.get(note.conversationId) ?? [], note]);
@@ -4858,7 +5110,7 @@ ${JSON.stringify(result, null, 2)}`);
4858
5110
  throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A send needs a messageId and 1 byte to 32 KiB of text");
4859
5111
  }
4860
5112
  const deadline = options2.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
4861
- const id = `relay-send-${sha2562(JSON.stringify([conversationId, messageId])).slice(0, 32)}`;
5113
+ const id = `relay-send-${sha2563(JSON.stringify([conversationId, messageId])).slice(0, 32)}`;
4862
5114
  let entry = relaySends.get(id);
4863
5115
  if (!entry) relaySends.set(id, entry = prepareRelaySend(conversationId, id, text3, deadline));
4864
5116
  const prepared = await beforeDeadline(entry.ready, deadline);
@@ -4903,6 +5155,9 @@ export {
4903
5155
  MLS_SETUP_ACTIONS,
4904
5156
  mlsDevRoomPackageId,
4905
5157
  runMlsDevSetup,
5158
+ MLS_ENVIRONMENT_PINS,
5159
+ bundledMlsCustodyHelper,
5160
+ managedMlsDevice,
4906
5161
  startRelayChannel,
4907
5162
  loadRelayConfiguration,
4908
5163
  readRelayCredential,
@@ -2,7 +2,7 @@ import {
2
2
  createMlsDevCredentialVerifier,
3
3
  mlsDevDeviceHttpConfiguration,
4
4
  parseMlsDevClientConfiguration
5
- } from "./chunk-5MI6Q3VS.js";
5
+ } from "./chunk-GWQF4WFT.js";
6
6
  export {
7
7
  createMlsDevCredentialVerifier,
8
8
  mlsDevDeviceHttpConfiguration,
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  MLS_SETUP_ACTIONS,
4
4
  assertPrivateHome,
5
5
  awaitErrand,
6
+ bundledMlsCustodyHelper,
6
7
  checkCodex,
7
8
  claudeDriver,
8
9
  codeDriver,
@@ -15,6 +16,7 @@ import {
15
16
  loadInstructions,
16
17
  loadMlsDevConfiguration,
17
18
  loadRelayConfiguration,
19
+ managedMlsDevice,
18
20
  queueErrand,
19
21
  readRelayCredential,
20
22
  relayRunnerHome,
@@ -22,7 +24,7 @@ import {
22
24
  runnerLockName,
23
25
  startRelayChannel,
24
26
  writeRelayCredential
25
- } from "./chunks/chunk-JCBQCO3F.js";
27
+ } from "./chunks/chunk-NXL4C2R6.js";
26
28
  import {
27
29
  CanonApiError,
28
30
  CanonClient,
@@ -33,11 +35,12 @@ import {
33
35
  resolveCanonRuntimeConnection,
34
36
  runCli,
35
37
  verifyCanonRuntimeConnection
36
- } from "./chunks/chunk-RFNYPZXD.js";
38
+ } from "./chunks/chunk-6PKXASSK.js";
37
39
  import {
38
40
  __name,
39
- isCanonClientVersion
40
- } from "./chunks/chunk-5MI6Q3VS.js";
41
+ isCanonClientVersion,
42
+ isMlsEnvironmentId
43
+ } from "./chunks/chunk-GWQF4WFT.js";
41
44
 
42
45
  // src/cli.ts
43
46
  import { realpathSync } from "node:fs";
@@ -66,9 +69,9 @@ Usage:
66
69
  canon-agent run --driver codex [--cwd <dir>] [--model <id>] [--sandbox <mode>]
67
70
  [--approval-policy <policy>] [--codex-path <path>] [--profile <name>]
68
71
  [--environment <id>]
69
- canon-agent run --driver connector --relay-config <path> --mls-dev-config <path>
72
+ canon-agent run --driver connector --relay-config <path> [--mls-dev-config <path>]
70
73
  [--profile <name>] [--environment <id>]
71
- canon-agent mls --action <action> --mls-dev-config <path> [--room <id>]
74
+ canon-agent mls --action <action> [--mls-dev-config <path>] [--room <id>]
72
75
  [--relay-config <path>] [--profile <name>] [--environment <id>]
73
76
  canon-agent tell --room <id> --text <text> (--agent <id> | --profile <name>)
74
77
  [--environment <id>] [--wait]
@@ -80,10 +83,12 @@ Usage:
80
83
  canon-agent errands [--follow] [--cancel <errand id>] (--agent <id> | --profile <name>)
81
84
  [--environment <id>]
82
85
 
83
- Every run command also takes --mls-dev-config <path>, which gives the agent its MLS
84
- device so it can take part in encrypted rooms. Without it, the agent refuses encrypted
85
- rooms and logs each one once; where Canon encrypts every new conversation, it does not
86
- start.
86
+ Encrypted rooms need the agent's MLS device. On macOS (Apple silicon) and Linux
87
+ (x64, arm64) the runner keeps it in $CANON_HOME with the custody helper this package
88
+ ships, and sets it up on the agent's first start: nothing to configure. Every run
89
+ command also takes --mls-dev-config <path>, an operator's own custody instead. With
90
+ neither, the agent refuses encrypted rooms, and where Canon encrypts every new
91
+ conversation it does not start.
87
92
 
88
93
  Team rooms (an owner's agents working together): claude and codex also take
89
94
  [--instructions <file>] [--trust-agent <agentId>]...; every run command takes
@@ -146,20 +151,22 @@ Flags:
146
151
  marks destructive (Claude Code's default-to-no prompts,
147
152
  Codex's risky commands and deletions) for the rest of the
148
153
  session. Off by default: each such action asks.
149
- --relay-config <path> connector (Dev only): run as Canon's class-C relay for one agent
154
+ --relay-config <path> connector: run as Canon's class-C relay for one agent
150
155
  connected through Canon's MCP connector. A private JSON file
151
156
  naming the agent, where the channel to Canon's Functions
152
157
  listens, and the path of the channel secret. Canon delivers
153
158
  the relay's own credential over that channel when the owner
154
159
  enables the relay (no API key is used); it is kept in
155
160
  $CANON_HOME/relay. The relay serves only encrypted rooms,
156
- which are then not end-to-end encrypted to that agent. With
157
- "mls", it sets up the relay's device with that credential.
158
- --mls-dev-config <p> The operator's MLS configuration: an absolute path to a private
159
- JSON file with the environment's issuer pins and the custody
160
- helper (no keys or credentials).
161
- --action <action> mls: status (public device state), provision (create the
162
- portable vault, once), enroll, renew, publish (this device's
161
+ which are then not end-to-end encrypted to that agent. It sets
162
+ up the relay's device with that credential.
163
+ --mls-dev-config <p> An operator's own MLS configuration, instead of the device the
164
+ runner keeps itself: an absolute path to a private JSON file
165
+ with the environment's issuer pins and the custody helper (no
166
+ keys or credentials).
167
+ --action <action> mls: status (public device state), setup (whatever is left of
168
+ provision then enroll), provision (create the portable vault,
169
+ once), enroll, renew, publish (this device's
163
170
  KeyPackage for --room) or refresh (catch up --room). It never
164
171
  starts a model, and uses the same home as "run": stop the
165
172
  agent first.
@@ -232,7 +239,6 @@ async function main() {
232
239
  }
233
240
  if (values.driver === "code" && !values.module) throw new UsageError("--driver code needs --module <path>.");
234
241
  if (values.driver === "connector" !== !!values["relay-config"]) throw new UsageError("--relay-config goes with --driver connector, which needs it.");
235
- if (values.driver === "connector" && !values["mls-dev-config"]) throw new UsageError("--driver connector needs --mls-dev-config <path>: the relay holds the agent's MLS device.");
236
242
  if (!["claude", "codex"].includes(values.driver) && (values.instructions !== void 0 || values["trust-agent"]?.length)) {
237
243
  throw new UsageError("--instructions and --trust-agent go with --driver claude or codex.");
238
244
  }
@@ -254,7 +260,7 @@ async function main() {
254
260
  if (trustedAgents.some((id) => !/^[\w.:-]{1,200}$/.test(id))) throw new UsageError("--trust-agent takes an agent id.");
255
261
  const instructions = values.instructions !== void 0 ? await loadInstructions(resolve(values.instructions)) : void 0;
256
262
  const { environmentId, home, profile, apiKey } = await settings(values);
257
- const mls = values["mls-dev-config"] ? await loadMlsDevConfiguration(values["mls-dev-config"], { environmentId }) : void 0;
263
+ const mls = await mlsDevice(values, environmentId, home);
258
264
  const baseUrl = process.env.CANON_BASE_URL || profile?.baseUrl;
259
265
  const streamUrl = process.env.CANON_STREAM_URL || profile?.streamUrl;
260
266
  let driver;
@@ -352,7 +358,8 @@ async function runRelay(values, team) {
352
358
  channel.serve(void 0);
353
359
  await current?.stop();
354
360
  current = void 0;
355
- const mls = await loadMlsDevConfiguration(values["mls-dev-config"], { environmentId });
361
+ const mls = await mlsDevice(values, environmentId, relayRunnerHome(home, stored2.installationId));
362
+ if (!mls) throw new Error(`this host has no MLS custody helper (${process.platform}-${process.arch}): give the relay --mls-dev-config <path>`);
356
363
  const driver = connectorDriver();
357
364
  const runner = await createRunner({
358
365
  apiKey: stored2.credential,
@@ -455,11 +462,17 @@ async function settings(values, { key = true } = {}) {
455
462
  return { environmentId, home, profile, apiKey: process.env.CANON_API_KEY || profile?.apiKey || "" };
456
463
  }
457
464
  __name(settings, "settings");
465
+ async function mlsDevice(values, environmentId, home) {
466
+ if (values["mls-dev-config"]) return loadMlsDevConfiguration(values["mls-dev-config"], { environmentId });
467
+ if (!isMlsEnvironmentId(environmentId)) return void 0;
468
+ const helper = bundledMlsCustodyHelper();
469
+ return helper ? managedMlsDevice({ home, environmentId, helper }) : void 0;
470
+ }
471
+ __name(mlsDevice, "mlsDevice");
458
472
  async function mlsSetup(values) {
459
473
  const action = values.action;
460
474
  if (!action || !MLS_SETUP_ACTIONS.includes(action)) throw new UsageError(`--action must be one of ${MLS_SETUP_ACTIONS.join(", ")}.`);
461
475
  if ((action === "publish" || action === "refresh") !== !!values.room) throw new UsageError("--room is needed for publish and refresh, and only for them.");
462
- if (!values["mls-dev-config"]) throw new UsageError("--mls-dev-config <path> is required.");
463
476
  const settled = await settings(values, { key: !values["relay-config"] });
464
477
  const { environmentId, profile } = settled;
465
478
  const relay = values["relay-config"] ? await loadRelayConfiguration(values["relay-config"], { environmentId }) : void 0;
@@ -469,7 +482,8 @@ async function mlsSetup(values) {
469
482
  }
470
483
  const apiKey = stored ? stored.credential : settled.apiKey;
471
484
  const home = stored ? relayRunnerHome(settled.home, stored.installationId) : settled.home;
472
- const config = await loadMlsDevConfiguration(values["mls-dev-config"], { environmentId });
485
+ const config = await mlsDevice(values, environmentId, home);
486
+ if (!config) throw new Error(`@canonmsg/agent-sdk ships no MLS custody helper for ${process.platform}-${process.arch}: give --mls-dev-config <path>.`);
473
487
  let client;
474
488
  let lock;
475
489
  try {
@@ -489,7 +503,7 @@ async function mlsSetup(values) {
489
503
  });
490
504
  const me = await client.getAgentMe();
491
505
  lock = acquireLock(runnerLockName(environmentId, me.agentId), home);
492
- console.log(JSON.stringify(await runMlsDevSetup(await client.getEndpoint(), action, values.room, config.provision)));
506
+ console.log(JSON.stringify(await runMlsDevSetup(await client.getEndpoint(), action, values.room, config.provision, config.provisioned)));
493
507
  } finally {
494
508
  try {
495
509
  await client?.close();
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export type { Runner, RunnerOptions } from './runner/runner.js';
3
3
  export { ConnectorRelayError, DriverSessionError } from './runner/types.js';
4
4
  export { loadMlsDevConfiguration, MLS_SETUP_ACTIONS, mlsDevRoomPackageId, runMlsDevSetup } from './runner/mls.js';
5
5
  export type { MlsSetupAction, RunnerMlsOptions } from './runner/mls.js';
6
+ export { bundledMlsCustodyHelper, MLS_ENVIRONMENT_PINS } from './runner/mls-bundled.js';
7
+ export { managedMlsDevice, type ManagedMlsDevice, type ManagedMlsDeviceOptions } from './runner/mls-managed.js';
6
8
  export type { ActivityEvent, AskAnswer, AskRequest, DriverCapabilities, DriverResult, DriverState, DriverTurn, ErrandFacts, RuntimeDriver, ToolDefinition, ToolResult, TurnInput, TurnInputMessage, TurnTools, } from './runner/types.js';
7
9
  export { errandJournal, errandsDirectory, queueErrand } from './runner/errands.js';
8
10
  export type { ErrandCommand, ErrandInput, ErrandKind, ErrandRecord, ErrandStatus } from './runner/errands.js';
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  ConnectorRelayError,
3
3
  DriverSessionError,
4
+ MLS_ENVIRONMENT_PINS,
4
5
  MLS_SETUP_ACTIONS,
6
+ bundledMlsCustodyHelper,
5
7
  checkCodex,
6
8
  claudeDriver,
7
9
  codeDriver,
@@ -13,12 +15,13 @@ import {
13
15
  loadClaudeQuery,
14
16
  loadMlsDevConfiguration,
15
17
  loadRelayConfiguration,
18
+ managedMlsDevice,
16
19
  mlsDevRoomPackageId,
17
20
  queueErrand,
18
21
  runMlsDevSetup,
19
22
  runtimeDescriptor,
20
23
  startRelayChannel
21
- } from "./chunks/chunk-JCBQCO3F.js";
24
+ } from "./chunks/chunk-NXL4C2R6.js";
22
25
  import {
23
26
  CanonApiError,
24
27
  DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
@@ -43,8 +46,8 @@ import {
43
46
  toAnthropicImageBlock,
44
47
  toAnthropicImageBlocksWithinBudget,
45
48
  uploadMediaFile
46
- } from "./chunks/chunk-RFNYPZXD.js";
47
- import "./chunks/chunk-5MI6Q3VS.js";
49
+ } from "./chunks/chunk-6PKXASSK.js";
50
+ import "./chunks/chunk-GWQF4WFT.js";
48
51
  export {
49
52
  CanonApiError,
50
53
  ConnectorRelayError,
@@ -55,7 +58,9 @@ export {
55
58
  MAX_ANTHROPIC_IMAGE_RAW_BYTES,
56
59
  MAX_ANTHROPIC_REQUEST_BYTES,
57
60
  MAX_CANON_MEDIA_BYTES,
61
+ MLS_ENVIRONMENT_PINS,
58
62
  MLS_SETUP_ACTIONS,
63
+ bundledMlsCustodyHelper,
59
64
  checkCodex,
60
65
  claudeDriver,
61
66
  clearPendingRegistration,
@@ -74,6 +79,7 @@ export {
74
79
  loadClaudeQuery,
75
80
  loadMlsDevConfiguration,
76
81
  loadRelayConfiguration,
82
+ managedMlsDevice,
77
83
  materializeAttachment,
78
84
  materializeMessageMedia,
79
85
  materializeReplyContextMedia,
@@ -8,6 +8,11 @@ export type EndpointContentLane = 'operation.request' | 'operation.receipt' | 'q
8
8
  * authenticated encrypted protocol before they can operate on a protected room.
9
9
  * Their routing fields remain public metadata; classification is not authority. */
10
10
  export declare const MLS_DEV_UNSUPPORTED_CONTENT_ACTIONS: readonly ["forward_message"];
11
+ /** Operations that send their private content to a named processor in an encrypted room too, disclosed where the
12
+ * app offers them, with the fields reviewed for that: an image's description and source photo go through Canon to
13
+ * Google (image-generation.md). The fields stay classified as private; only the encrypted room's plaintext fence lets
14
+ * them through, and any other field still keeps the operation out. */
15
+ export declare const MLS_DEV_DISCLOSED_PROCESSING_FIELDS: Readonly<Record<string, readonly string[]>>;
11
16
  export declare const ENDPOINT_CONTENT_ACTIONS: readonly ["mls_application", "mls_group_control", "initialize_endpoint", "append_artifact_command", "retain_artifact_asset", "sync_phone_contacts", "update_agent_behavior_policy", "update_conversation_agent_behavior_policy", "send_message", "delete_message", "forward_message", "react", "verb_execute", "send_contextual_message", "create_interaction", "consume_interaction", "start_conversation", "create_conversation", "add_member", "remove_member", "leave_conversation", "update_member_role", "update_conversation", "rename_conversation", "update_topic", "update_group_avatar", "mark_read", "cancel_queued_message", "set_conversation_preferences", "set_block", "save_contact", "remove_contact", "respond_interaction", "session_control", "approve_contact_request", "reject_contact_request", "cancel_direct_request", "update_message_disposition", "create_upload", "finalize_upload", "delete_upload", "retry_video_processing", "stage_media_message", "cancel_media_message", "renew_media_message_upload", "finalize_memo_stream", "abort_memo_stream", "create_voice_session", "join_voice_session", "decline_voice_session", "end_voice_session", "consume_runtime_signal", "consume_runtime_primitive", "generate_image"];
12
17
  export declare const ENDPOINT_CONTENT_FUNCTION_QUERIES: readonly ["mls_group_feed", "mls_group_controls", "mls_group_admissions", "mls_interactions", "list_artifacts", "get_artifact_log", "get_artifact_asset", "discover_agents", "get_unread_summary", "peek_runtime_signal", "list_runtime_primitives", "get_agent_behavior_policy", "get_conversation_agent_behavior_policy", "get_ephemeral", "list_voice_sessions", "list_contact_requests", "list_conversations", "list_messages", "get_messages", "admission_fences", "list_contacts", "get_contact", "get_members", "resolve_admission", "resolve_group_admission", "get_runtime_card_state", "conversation", "conversation_changes", "get_upload", "get_media_message", "get_voice_session", "get_image_generation", "get_image_generation_status", "same_owner_agents"];
13
18
  export declare const ENDPOINT_CONTENT_QUERIES: readonly ["mls_group_feed", "mls_group_controls", "mls_group_admissions", "mls_interactions", "list_artifacts", "get_artifact_log", "get_artifact_asset", "discover_agents", "get_unread_summary", "peek_runtime_signal", "list_runtime_primitives", "get_agent_behavior_policy", "get_conversation_agent_behavior_policy", "get_ephemeral", "list_voice_sessions", "list_contact_requests", "list_conversations", "list_messages", "get_messages", "admission_fences", "list_contacts", "get_contact", "get_members", "resolve_admission", "resolve_group_admission", "get_runtime_card_state", "conversation", "conversation_changes", "get_upload", "get_media_message", "get_voice_session", "get_image_generation", "get_image_generation_status", "same_owner_agents", "admit_messages"];
package/dist/media.js CHANGED
@@ -18,8 +18,8 @@ import {
18
18
  toAnthropicImageBlock,
19
19
  toAnthropicImageBlocksWithinBudget,
20
20
  uploadMediaFile
21
- } from "./chunks/chunk-RFNYPZXD.js";
22
- import "./chunks/chunk-5MI6Q3VS.js";
21
+ } from "./chunks/chunk-6PKXASSK.js";
22
+ import "./chunks/chunk-GWQF4WFT.js";
23
23
  export {
24
24
  DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
25
25
  DEFAULT_MEDIA_CACHE_DIR,
@@ -0,0 +1,25 @@
1
+ import type { MlsEnvironmentId } from '../internal/backend-contracts/index.js';
2
+ /**
3
+ * Each environment's Cloud KMS issuer and feed-receipt key versions (public keys, raw Ed25519 in base64url). During an
4
+ * issuer rotation `issuerKeys` holds both versions (docs/mls-dev-activation.md, "Planned issuer rotation"), and an SDK
5
+ * release carrying the new one ships before issuance switches.
6
+ */
7
+ export declare const MLS_ENVIRONMENT_PINS: Readonly<Record<MlsEnvironmentId, {
8
+ issuerKeys: ReadonlyArray<{
9
+ keyVersion: string;
10
+ signaturePublicKey: string;
11
+ }>;
12
+ receiptKeys: ReadonlyArray<{
13
+ keyVersion: string;
14
+ signaturePublicKey: string;
15
+ }>;
16
+ }>>;
17
+ /**
18
+ * The custody helper this package ships for this host (`dist/mls/<platform>/canon-mls-custody`), checked against its
19
+ * pin, or undefined where it ships none: an Intel Mac, musl, Windows, or a build of this package without helpers. A
20
+ * file that is there but differs from its pin is refused.
21
+ */
22
+ export declare function bundledMlsCustodyHelper(): {
23
+ path: string;
24
+ sha256: string;
25
+ } | undefined;
@@ -0,0 +1,31 @@
1
+ import type { MlsDevDeviceContext, MlsEnvironmentId } from '../internal/backend-contracts/index.js';
2
+ import type { RunnerMlsOptions } from './mls.js';
3
+ export interface ManagedMlsDeviceOptions {
4
+ /** The runner's home (CANON_HOME, or a relay installation's home). */
5
+ home: string;
6
+ environmentId: MlsEnvironmentId;
7
+ /** Another helper than this package's for the host, such as one built from mls/native. */
8
+ helper?: {
9
+ path: string;
10
+ sha256: string;
11
+ };
12
+ /** Other public pins than this package's for the environment. */
13
+ pins?: {
14
+ issuerKeys: ReadonlyArray<{
15
+ keyVersion: string;
16
+ signaturePublicKey: string;
17
+ }>;
18
+ receiptKeys: ReadonlyArray<{
19
+ keyVersion: string;
20
+ signaturePublicKey: string;
21
+ }>;
22
+ };
23
+ timeoutMs?: number;
24
+ }
25
+ export type ManagedMlsDevice = RunnerMlsOptions & {
26
+ provision(context: MlsDevDeviceContext): Promise<void>;
27
+ /** Whether this context's vault exists: its device was provisioned here. */
28
+ provisioned(context: MlsDevDeviceContext): boolean;
29
+ close(): Promise<void>;
30
+ };
31
+ export declare function managedMlsDevice(options: ManagedMlsDeviceOptions): ManagedMlsDevice;
@@ -1,9 +1,14 @@
1
1
  import { type CanonEndpoint, type CanonMlsDevOptions, type MlsDevCurrentAudience } from '../internal/core/index.js';
2
2
  import { type MlsDevDeviceContext } from '../internal/backend-contracts/index.js';
3
3
  import type { ToolDefinition } from './types.js';
4
- /** What `createRunner({ mls })` takes: issuer pins and custody, plus the custody's own shutdown. */
4
+ /**
5
+ * What `createRunner({ mls })` takes: issuer pins and custody, plus the custody's own shutdown. With `provisioned`
6
+ * (managed custody, `managedMlsDevice`) the runner sets the device up itself at start.
7
+ */
5
8
  export type RunnerMlsOptions = CanonMlsDevOptions & {
6
9
  close?(): Promise<void>;
10
+ provision?(context: MlsDevDeviceContext): Promise<void>;
11
+ provisioned?(context: MlsDevDeviceContext): boolean;
7
12
  };
8
13
  /** A closed copy of the public audience (no provider input or key material), in a fixed field order. */
9
14
  export declare function snapshotMlsAudience(value: unknown): MlsDevCurrentAudience;
@@ -70,7 +75,7 @@ export declare function loadMlsDevConfiguration(filePath: string, input: {
70
75
  provision?: (context: MlsDevDeviceContext) => Promise<void>;
71
76
  close(): Promise<void>;
72
77
  }>;
73
- export declare const MLS_SETUP_ACTIONS: readonly ["status", "provision", "enroll", "renew", "publish", "refresh"];
78
+ export declare const MLS_SETUP_ACTIONS: readonly ["status", "setup", "provision", "enroll", "renew", "publish", "refresh"];
74
79
  export type MlsSetupAction = typeof MLS_SETUP_ACTIONS[number];
75
80
  /** The KeyPackage an agent publishes for one room: stable for its installation, so a repeated publish is the same intent. */
76
81
  export declare function mlsDevRoomPackageId(identity: {
@@ -81,13 +86,15 @@ export declare function mlsDevRoomPackageId(identity: {
81
86
  }, room: string): string;
82
87
  /**
83
88
  * One operator step for the agent's device (`canon-agent mls`), on the same engine store the runner uses. It never
84
- * starts a model, and prints only public device state: no credentials, proofs, envelopes or history.
89
+ * starts a model, and prints only public device state: no credentials, proofs, envelopes or history. `setup` does what
90
+ * is left of provision then enroll, so it also finishes a setup that stopped part way; it never replaces a vault, and
91
+ * with `provisioned` (managed custody) it provisions only where this installation has none.
85
92
  */
86
- export declare function runMlsDevSetup(endpoint: CanonEndpoint, action: MlsSetupAction, room?: string, provision?: (context: MlsDevDeviceContext) => Promise<void>): Promise<{
93
+ export declare function runMlsDevSetup(endpoint: CanonEndpoint, action: MlsSetupAction, room?: string, provision?: (context: MlsDevDeviceContext) => Promise<void>, provisioned?: (context: MlsDevDeviceContext) => boolean): Promise<{
87
94
  conversationId?: string | undefined;
88
95
  version: number;
89
- action: "status" | "publish" | "enroll" | "renew" | "provision" | "refresh";
90
- environmentId: "canon-dev-v1" | "canon-prod-v1";
96
+ action: "status" | "publish" | "enroll" | "renew" | "provision" | "setup" | "refresh";
97
+ environmentId: string;
91
98
  principalId: string;
92
99
  installationId: string;
93
100
  ownershipVersion: number | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "12.3.2",
3
+ "version": "12.4.1",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "scripts": {
25
25
  "build": "node ../../scripts/build-agent-sdk.mjs",
26
26
  "dev": "tsc --watch --noEmit",
27
- "prepack": "npm run build"
27
+ "prepack": "node ../../scripts/build-agent-sdk.mjs --pack"
28
28
  },
29
29
  "engines": {
30
30
  "node": ">=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1 <27"