@canonmsg/agent-sdk 12.4.1 → 12.5.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
@@ -192,7 +192,7 @@ Inbound attachments are downloaded before the turn starts, up to 10 MiB each, an
192
192
  | `askExpiryMs` | 1 h | How long an `ask` waits for its answer |
193
193
  | `shutdownGraceMs` | 10 s | How long running turns may finish after `stop()` |
194
194
  | `log` | stderr | Receives operational lines, never prompt or reply text |
195
- | `mls` | none | The agent's MLS device for encrypted rooms; see [Encrypted rooms](#encrypted-rooms) |
195
+ | `mls` | managed custody, where this package ships a helper for the host | The agent's MLS device for encrypted rooms, or `false` for none; see [Encrypted rooms](#encrypted-rooms) |
196
196
  | `relay`, `installationId` | off | Canon's class-C relay; see [The class-C relay](#the-class-c-relay-canon-operated) |
197
197
  | `trustedAgents` | none | Agent ids whose messages count as the owner's for tool use while Canon confirms each is an active agent of the same owner; see [Team rooms](#team-rooms) |
198
198
  | `agentTurnCap` | 6 | Encrypted rooms: how many agent @mentions in a row wake the agent in a room before a person writes there again |
@@ -213,7 +213,7 @@ The CLI is `canon-agent run --driver code --module <path> [--profile <name>] [--
213
213
 
214
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
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`.
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. `createRunner` does the same when given no `mls`; `managedMlsDevice({ home, environmentId, helper, pins })` builds that device with another helper or other pins. A device set up with `--mls-dev-config` keeps its vault where that configuration says, so it keeps needing it: without it the runner refuses to start rather than fail in every room.
217
217
 
218
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:
219
219
 
@@ -236,7 +236,7 @@ In an encrypted room:
236
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.
237
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.
238
238
 
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.
239
+ Without a device (no helper for the host and no `--mls-dev-config`, or `mls: false` 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.
240
240
 
241
241
  ### The class-C relay (Canon-operated)
242
242
 
@@ -30,7 +30,7 @@ import {
30
30
  sendMessageWithRetryChunked,
31
31
  splitTextByUtf8Bytes,
32
32
  verifyCanonRuntimeConnection
33
- } from "./chunk-6PKXASSK.js";
33
+ } from "./chunk-TFSURFF6.js";
34
34
  import {
35
35
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
36
36
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
@@ -59,7 +59,7 @@ import {
59
59
  parseMlsDevGroupHead,
60
60
  parseMlsDevGroupRoster,
61
61
  resolveAllowedUploadMimeType
62
- } from "./chunk-GWQF4WFT.js";
62
+ } from "./chunk-C2WJT3VX.js";
63
63
 
64
64
  // src/runner/types.ts
65
65
  var DriverSessionError = class extends Error {
@@ -913,31 +913,43 @@ function connectorDriver() {
913
913
  },
914
914
  async claim(inputId, requestId, options = {}) {
915
915
  const entry = find(inputId);
916
- if (entry.state === "claiming") {
917
- if (entry.requestId !== requestId) throw new ConnectorRelayError(409, "CONNECTOR_CLAIM_CONFLICT", "This input belongs to another claim");
918
- throw new ConnectorRelayError(503, "CONNECTOR_RELAY_BUSY", "This claim is still being confirmed; try again");
916
+ if (entry.state === "claiming" && entry.requestId !== requestId) {
917
+ throw new ConnectorRelayError(409, "CONNECTOR_CLAIM_CONFLICT", "This input belongs to another claim");
919
918
  }
920
- if (entry.state !== "pending") {
919
+ if (entry.state !== "pending" && entry.state !== "claiming") {
921
920
  if (entry.requestId !== void 0 && entry.requestId !== requestId) {
922
921
  throw new ConnectorRelayError(409, "CONNECTOR_CLAIM_CONFLICT", "This input belongs to another claim");
923
922
  }
924
923
  return { inputId, requestId, runAllowed: false, state: entry.state };
925
924
  }
926
- entry.state = "claiming";
927
- entry.requestId = requestId;
925
+ if (entry.confirmation) throw new ConnectorRelayError(503, "CONNECTOR_RELAY_BUSY", "This claim is still being confirmed; retry the same request id");
926
+ if (entry.state === "pending") {
927
+ entry.state = "claiming";
928
+ entry.requestId = requestId;
929
+ }
930
+ const confirmation = entry.confirm().then(() => "ok", () => {
931
+ if (entry.state === "claiming") {
932
+ entry.state = "pending";
933
+ entry.requestId = void 0;
934
+ }
935
+ return "refused";
936
+ }).finally(() => {
937
+ entry.confirmation = void 0;
938
+ });
939
+ entry.confirmation = confirmation;
928
940
  const deadline = options.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
929
941
  let timer;
930
942
  const late = new Promise((resolve3) => {
931
943
  timer = setTimeout(resolve3, Math.max(0, deadline - Date.now()), "late");
932
944
  });
933
- const confirmed = await Promise.race([entry.confirm().then(() => "ok", () => "refused"), late]).finally(() => clearTimeout(timer));
934
- if (entry.state === "claiming" && confirmed === "ok") {
945
+ const confirmed = await Promise.race([confirmation, late]).finally(() => clearTimeout(timer));
946
+ const state = entry.state;
947
+ if (state === "claiming" && confirmed === "ok") {
935
948
  entry.state = "claimed";
936
949
  return { inputId, requestId, runAllowed: true, state: "claimed", input: entry.input };
937
950
  }
938
- if (entry.state !== "claiming") throw withdrawn();
939
- entry.state = "pending";
940
- entry.requestId = void 0;
951
+ if (state === "claimed") return { inputId, requestId, runAllowed: false, state: "claimed" };
952
+ if (state === "withdrawn" || state === "answered") throw withdrawn();
941
953
  throw new ConnectorRelayError(503, "CONNECTOR_RELAY_BUSY", "Canon could not confirm this claim now; try again");
942
954
  },
943
955
  reply: /* @__PURE__ */ __name((inputId, requestId, text3) => answer(inputId, requestId, { outcome: "reply", text: text3 }), "reply"),
@@ -952,7 +964,10 @@ function connectorDriver() {
952
964
  activity: false,
953
965
  tools: false,
954
966
  outboundFiles: false,
955
- remote: true
967
+ remote: true,
968
+ // A turn here is an input waiting for (or with) the assistant, not work on this host: one per room, so a room
969
+ // whose assistant is away never keeps another room's input from being offered.
970
+ maxConcurrentTurns: Number.POSITIVE_INFINITY
956
971
  },
957
972
  inbox,
958
973
  runTurn(turn) {
@@ -1503,11 +1518,18 @@ function mlsDevRoomPackageId(identity, room) {
1503
1518
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
1504
1519
  }
1505
1520
  __name(mlsDevRoomPackageId, "mlsDevRoomPackageId");
1521
+ async function assertMlsVaultInHome(endpoint, provisioned) {
1522
+ const { status, binding } = await endpoint.mlsDevDevice.read();
1523
+ if (status === "absent" || status === "revoked" || provisioned({ ...binding, revision: 0 })) return;
1524
+ 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.");
1525
+ }
1526
+ __name(assertMlsVaultInHome, "assertMlsVaultInHome");
1506
1527
  async function runMlsDevSetup(endpoint, action, room, provision, provisioned) {
1507
1528
  const identity = await endpoint.ready();
1508
1529
  const { environmentId } = identity, devices = endpoint.mlsDevDevice;
1509
1530
  if (!isMlsEnvironmentId(environmentId) || !devices || !endpoint.mlsDevKeyPackages || !endpoint.mlsDevGroups) throw new Error("MLS_DEV_SETUP_UNAVAILABLE");
1510
1531
  if ((action === "publish" || action === "refresh") !== !!room) throw new Error("MLS_DEV_SETUP_ARGUMENTS_INVALID");
1532
+ if (provisioned && action !== "status") await assertMlsVaultInHome(endpoint, provisioned);
1511
1533
  const enroll = /* @__PURE__ */ __name(async () => {
1512
1534
  await devices.enroll();
1513
1535
  if ((await devices.read()).status !== "credential_committed") await devices.renew();
@@ -1689,6 +1711,12 @@ import { createHash as createHash6 } from "node:crypto";
1689
1711
  import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2 } from "node:fs";
1690
1712
  import { dirname as dirname5, join as join3 } from "node:path";
1691
1713
  var sha256 = /* @__PURE__ */ __name((bytes) => createHash6("sha256").update(bytes).digest("hex"), "sha256");
1714
+ function defaultMlsDevice(home, environmentId) {
1715
+ if (!isMlsEnvironmentId(environmentId)) return void 0;
1716
+ const helper = bundledMlsCustodyHelper();
1717
+ return helper ? managedMlsDevice({ home, environmentId, helper }) : void 0;
1718
+ }
1719
+ __name(defaultMlsDevice, "defaultMlsDevice");
1692
1720
  function managedMlsDevice(options) {
1693
1721
  const { environmentId } = options;
1694
1722
  const helper = options.helper ?? bundledMlsCustodyHelper();
@@ -1849,6 +1877,8 @@ async function startRelayChannel(options) {
1849
1877
  return served.runner.relayRead(request.args.conversationId, { limit: request.args.limit, before: request.args.before, deadline });
1850
1878
  case "send":
1851
1879
  return served.runner.relaySend(request.args.conversationId, request.args.messageId, request.args.text, { deadline });
1880
+ case "start":
1881
+ return served.runner.relayStart(request.args.targetUserId, request.args.messageId, request.args.text, request.args.selection, { deadline });
1852
1882
  }
1853
1883
  }
1854
1884
  __name(dispatch, "dispatch");
@@ -3214,6 +3244,7 @@ __name(runtimeDescriptor, "runtimeDescriptor");
3214
3244
  async function createRunner(options) {
3215
3245
  const { driver, home } = options;
3216
3246
  await assertPrivateHome(home);
3247
+ const mls = options.mls === false ? void 0 : options.mls ?? (options.endpoint ? void 0 : defaultMlsDevice(home, options.environmentId));
3217
3248
  const log = options.log ?? ((line) => console.error(`[canon-runner] ${line}`));
3218
3249
  const warn = /* @__PURE__ */ __name((what) => (error) => log(`${what}: ${error instanceof Error ? error.message : String(error)}`), "warn");
3219
3250
  const abortGraceMs = options.abortGraceMs ?? 1e4;
@@ -3385,6 +3416,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3385
3416
  if (input.mls) target.mls = true;
3386
3417
  target.queue.push(input);
3387
3418
  target.turn?.live.refresh();
3419
+ requeue(target);
3388
3420
  pump();
3389
3421
  }
3390
3422
  __name(offer, "offer");
@@ -3492,7 +3524,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3492
3524
  const inputs = candidate.queue.splice(0, Math.min(MAX_INPUTS_PER_TURN, other < 0 ? MAX_INPUTS_PER_TURN : other));
3493
3525
  const turn = newTurn(inputs, turnId(candidate.id, inputs), candidate.id);
3494
3526
  candidate.turn = turn;
3495
- (turn.live = livePublisher(candidate, turn, encrypted ? "presence" : "status")).open();
3527
+ (turn.live = driver.capabilities.remote ? SILENT : livePublisher(candidate, turn, encrypted ? "presence" : "status")).open();
3496
3528
  const done = execute(candidate, turn).catch((error) => {
3497
3529
  warn("turn failed")(error);
3498
3530
  return dispose(queued(turn), "release", "turn-failed");
@@ -3522,14 +3554,28 @@ ${error instanceof Error ? error.message : String(error)}`;
3522
3554
  if (turn.inputs[0].mls && !await beforeTurn(current, turn)) return;
3523
3555
  turn.trusted = await confirmTrusted(turn, conversation.memberIds);
3524
3556
  turn.notes = pendingNotes(current.id);
3525
- turn.contexts = await Promise.all(openErrands(current.id).map(async (record) => ({ record, encryptedOrigin: !!record.origin && await endpoint.getConversationContentMode(record.origin).then((mode) => mode === "mls-dev-text", () => false) })));
3526
- turn.live.setMode(turn.mls ? "presence" : driver.capabilities.streaming && resolveTurnVerbosity({ conversationType: kind }) === "verbose" ? "full" : "status");
3557
+ turn.contexts = await Promise.all(openErrands(current.id).map(async (record) => ({ record, encryptedOrigin: !!record.origin && await endpoint.getConversationContentMode(record.origin).then((mode2) => mode2 === "mls-dev-text", () => false) })));
3558
+ const mode = turn.mls ? "presence" : driver.capabilities.streaming && resolveTurnVerbosity({ conversationType: kind }) === "verbose" ? "full" : "status";
3559
+ turn.live.setMode(mode);
3527
3560
  const history = driver.capabilities.sessionMemory === "none" ? await recentHistory(current, turn) : [];
3528
3561
  await materialize(current, turn);
3529
3562
  const bound = bindTools(current, turn);
3530
3563
  try {
3531
3564
  let work;
3532
- for (let live = queued(turn), waited = 0; live.length && !turn.controller.signal.aborted; ) {
3565
+ if (driver.capabilities.remote) {
3566
+ const live = queued(turn);
3567
+ if (live.length && !turn.controller.signal.aborted) {
3568
+ turn.inputs = live;
3569
+ turn.id = turnId(current.id, live);
3570
+ turn.pickUp = () => pickUp(current, turn, mode);
3571
+ try {
3572
+ work = Promise.resolve(driver.runTurn(buildTurn(current.id, turn, kind, history, bound.tools)));
3573
+ } catch (error) {
3574
+ work = Promise.reject(error);
3575
+ }
3576
+ requeue(current);
3577
+ }
3578
+ } else for (let live = queued(turn), waited = 0; live.length && !turn.controller.signal.aborted; ) {
3533
3579
  turn.inputs = live;
3534
3580
  turn.id = turnId(current.id, live);
3535
3581
  let driverTurn = buildTurn(current.id, turn, kind, history, bound.tools);
@@ -3552,15 +3598,17 @@ ${error instanceof Error ? error.message : String(error)}`;
3552
3598
  live = next;
3553
3599
  }
3554
3600
  if (!work) return;
3555
- turn.submitted = true;
3556
- progress(turn);
3557
- if (!turn.mls) void client.markAsRead(current.id).catch(warn("mark read"));
3558
- for (const { payload } of turn.mls ? [] : turn.inputs) {
3559
- if (payload.message.metadata?.inboundDisposition === "queued") {
3560
- void client.updateMessageDisposition(current.id, payload.message.id, "accepted_now").catch(warn("accepted_now"));
3561
- }
3562
- }
3601
+ if (!driver.capabilities.remote) started(current, turn);
3563
3602
  const outcome = await outcomeOf(turn, work);
3603
+ if (!turn.submitted) await turn.claiming?.catch(() => void 0);
3604
+ if (!turn.submitted) {
3605
+ turn.pickUp = void 0;
3606
+ if (turn.reason === "requeue") return void current.queue.unshift(...queued(turn));
3607
+ if (turn.reason) return;
3608
+ const claim = await endpoint.startInbound(queued(turn).map((input) => input.handle), () => void 0);
3609
+ if (claim.status !== "submitted") return await dispose(queued(turn), "release", "claim-deferred");
3610
+ started(current, turn);
3611
+ }
3564
3612
  turn.open = false;
3565
3613
  turn.ended.abort();
3566
3614
  bound.release();
@@ -3573,6 +3621,46 @@ ${error instanceof Error ? error.message : String(error)}`;
3573
3621
  }
3574
3622
  }
3575
3623
  __name(execute, "execute");
3624
+ function started(current, turn) {
3625
+ turn.submitted = true;
3626
+ progress(turn);
3627
+ if (!turn.mls) void client.markAsRead(current.id).catch(warn("mark read"));
3628
+ for (const { payload } of turn.mls ? [] : turn.inputs) {
3629
+ if (payload.message.metadata?.inboundDisposition === "queued") {
3630
+ void client.updateMessageDisposition(current.id, payload.message.id, "accepted_now").catch(warn("accepted_now"));
3631
+ }
3632
+ }
3633
+ }
3634
+ __name(started, "started");
3635
+ async function pickUp(current, turn, mode) {
3636
+ const live = queued(turn);
3637
+ if (live.length !== turn.inputs.length) {
3638
+ abort(turn, "requeue");
3639
+ throw turnEnded2();
3640
+ }
3641
+ const claiming = endpoint.startInbound(live.map((input) => input.handle), () => void 0).then((result) => {
3642
+ if (result.status === "submitted") {
3643
+ turn.pickUp = void 0;
3644
+ started(current, turn);
3645
+ if (!turn.controller.signal.aborted) (turn.live = livePublisher(current, turn, mode)).open();
3646
+ }
3647
+ return result;
3648
+ });
3649
+ turn.claiming = claiming;
3650
+ const claim = await claiming;
3651
+ if (claim.status !== "submitted") {
3652
+ if (claim.status !== "deferred") abort(turn, "requeue");
3653
+ throw new Error("The input could not be claimed now");
3654
+ }
3655
+ }
3656
+ __name(pickUp, "pickUp");
3657
+ function requeue(current) {
3658
+ const turn = current.turn, next = current.queue[0];
3659
+ if (!turn?.pickUp || turn.pickingUp || turn.controller.signal.aborted || !next) return;
3660
+ if (!!next.mls !== !!turn.inputs[0].mls || turn.inputs.length >= MAX_INPUTS_PER_TURN) return;
3661
+ abort(turn, "requeue");
3662
+ }
3663
+ __name(requeue, "requeue");
3576
3664
  async function confirmTrusted(turn, memberIds) {
3577
3665
  if (!trustedAgents.size || driver.capabilities.remote || options.relay) return /* @__PURE__ */ new Set();
3578
3666
  const candidates = turn.inputs.map(({ payload }) => payload.message).filter((message2) => message2.senderType === "ai_agent" && message2.senderId !== agentId && trustedAgents.has(message2.senderId) && (turn.mls ? true : message2.metadata?.[ACTED_FOR_OWNER] === true && message2.metadata.turnSemantics === "turn_complete" && message2.forwarded !== true));
@@ -3805,7 +3893,7 @@ ${JSON.stringify(result, null, 2)}`);
3805
3893
  let sending = turn.said.get(messageId);
3806
3894
  if (!sending) {
3807
3895
  turn.said.set(messageId, sending = (async () => {
3808
- const mls = audience ?? (await currentAudience(roomId)).audience;
3896
+ const mls2 = audience ?? (await currentAudience(roomId)).audience;
3809
3897
  const record = {
3810
3898
  version: 1,
3811
3899
  turnId: messageId,
@@ -3816,7 +3904,7 @@ ${JSON.stringify(result, null, 2)}`);
3816
3904
  text: text3,
3817
3905
  options: { messageId },
3818
3906
  notice: false,
3819
- mls
3907
+ mls: mls2
3820
3908
  };
3821
3909
  journal.save(record);
3822
3910
  const sent2 = await publish(record);
@@ -4052,21 +4140,21 @@ ${JSON.stringify(result, null, 2)}`);
4052
4140
  return await send(record.conversationId, STOPS_SHORT_NOTICE, notice, retire) === "pending" ? "pending" : "refused";
4053
4141
  }
4054
4142
  __name(publish, "publish");
4055
- async function send(conversationId, text3, options2, retire, record, mls = record?.mls) {
4143
+ async function send(conversationId, text3, options2, retire, record, mls2 = record?.mls) {
4056
4144
  const sent = await (async () => {
4057
4145
  for (let attempt = 0; ; attempt += 1) {
4058
4146
  const files = record?.attachments?.length ?? 0;
4059
- const result = await untilHalt((mls ? sendEncryptedItems(mls, record?.items ?? [], retire).then(() => sendEncrypted(mls, options2.messageId, text3, retire)) : files ? sendWithFiles(record, fitGroup(text3, files), options2, retire) : sendMessageWithRetryChunked(client, conversationId, fitGroup(text3, 0), options2, { signal: retire })).then(() => "delivered", (error) => error));
4147
+ const result = await untilHalt((mls2 ? sendEncryptedItems(mls2, record?.items ?? [], retire).then(() => sendEncrypted(mls2, options2.messageId, text3, retire)) : files ? sendWithFiles(record, fitGroup(text3, files), options2, retire) : sendMessageWithRetryChunked(client, conversationId, fitGroup(text3, 0), options2, { signal: retire })).then(() => "delivered", (error) => error));
4060
4148
  if (result === "delivered" || result === "pending") return result;
4061
4149
  if (retire?.aborted || isPendingCanonOperation(result)) return "pending";
4062
- if (mls ? result.retryable === false : !isRetryableCanonDeliveryError(result)) {
4150
+ if (mls2 ? result.retryable === false : !isRetryableCanonDeliveryError(result)) {
4063
4151
  warn("Canon refused a reply")(result);
4064
4152
  return "refused";
4065
4153
  }
4066
4154
  if (await untilHalt(pause2(Math.min(3e4, 1e3 * 2 ** attempt))) === "pending") return "pending";
4067
4155
  }
4068
4156
  })();
4069
- if (mls) forgetGuard(mls, options2.messageId, sent);
4157
+ if (mls2) forgetGuard(mls2, options2.messageId, sent);
4070
4158
  return sent;
4071
4159
  }
4072
4160
  __name(send, "send");
@@ -4200,21 +4288,28 @@ ${JSON.stringify(result, null, 2)}`);
4200
4288
  __name(buildTurn, "buildTurn");
4201
4289
  async function confirmPickup(roomId, turn, open4) {
4202
4290
  if (!open4()) throw turnEnded2();
4203
- const current = await client.getConversation(roomId);
4204
- if (!open4()) throw turnEnded2();
4205
- if (!current?.memberIds?.includes(agentId)) {
4206
- loseMembership(room(roomId));
4207
- throw new Error("The agent is no longer a member of this conversation");
4208
- }
4209
- if (turn.mls && await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4210
- if (!turn.controller.signal.aborted) {
4211
- void dispose(queued(turn), "settle", "mls-obsolete-audience");
4212
- abort(turn, "audience");
4291
+ turn.pickingUp = true;
4292
+ try {
4293
+ const current = await client.getConversation(roomId);
4294
+ if (!open4()) throw turnEnded2();
4295
+ if (!current?.memberIds?.includes(agentId)) {
4296
+ loseMembership(room(roomId));
4297
+ throw new Error("The agent is no longer a member of this conversation");
4213
4298
  }
4214
- throw new Error("Who can read this encrypted conversation changed");
4299
+ if (turn.mls && await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4300
+ if (!turn.controller.signal.aborted) {
4301
+ void dispose(queued(turn), "settle", "mls-obsolete-audience");
4302
+ abort(turn, "audience");
4303
+ }
4304
+ throw new Error("Who can read this encrypted conversation changed");
4305
+ }
4306
+ if (!open4()) throw turnEnded2();
4307
+ if (turn.pickUp) await turn.pickUp();
4308
+ progress(turn);
4309
+ } finally {
4310
+ turn.pickingUp = false;
4311
+ requeue(room(roomId));
4215
4312
  }
4216
- if (!open4()) throw turnEnded2();
4217
- progress(turn);
4218
4313
  }
4219
4314
  __name(confirmPickup, "confirmPickup");
4220
4315
  function ask(roomId, turn, sender, request) {
@@ -4396,11 +4491,11 @@ ${JSON.stringify(result, null, 2)}`);
4396
4491
  const mode = await endpoint.getConversationContentMode(roomId);
4397
4492
  if (mode !== "plaintext") target.mls = true;
4398
4493
  if (mode !== "plaintext" && (!mlsOn || mode !== "mls-dev-text")) refuse(roomId, mlsOn ? "protocol" : "no-device");
4399
- const mls = mode === "mls-dev-text" && mlsOn ? (await currentAudience(roomId)).audience : void 0;
4494
+ const mls2 = mode === "mls-dev-text" && mlsOn ? (await currentAudience(roomId)).audience : void 0;
4400
4495
  const legacy = `${id}:restart`;
4401
- const messageId = mls && !await endpoint.getOperation(encryptedOperation(mls, legacy)) ? keyed().text("restart", roomId, id) : legacy;
4402
- if (mode !== "plaintext" && !mls) await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4403
- else if (await send(roomId, RESTART_NOTICE, outputOptions(id, trigger, "restart", false, messageId), void 0, void 0, mls) !== "pending") {
4496
+ const messageId = mls2 && !await endpoint.getOperation(encryptedOperation(mls2, legacy)) ? keyed().text("restart", roomId, id) : legacy;
4497
+ if (mode !== "plaintext" && !mls2) await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4498
+ else if (await send(roomId, RESTART_NOTICE, outputOptions(id, trigger, "restart", false, messageId), void 0, void 0, mls2) !== "pending") {
4404
4499
  await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4405
4500
  }
4406
4501
  }
@@ -4933,7 +5028,7 @@ ${JSON.stringify(result, null, 2)}`);
4933
5028
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
4934
5029
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
4935
5030
  ...options.installationId ? { installationId: options.installationId } : {},
4936
- ...options.mls ? { mlsDev: { configuration: options.mls.configuration, custody: options.mls.custody } } : {},
5031
+ ...mls ? { mlsDev: { configuration: mls.configuration, custody: mls.custody } } : {},
4937
5032
  ...options.endpoint ? { endpoint: options.endpoint } : {},
4938
5033
  ...options.fetch ? { fetch: options.fetch } : {}
4939
5034
  });
@@ -4949,14 +5044,17 @@ ${JSON.stringify(result, null, 2)}`);
4949
5044
  if (options.wake !== void 0) await client.configureWake(options.wake);
4950
5045
  mlsOn = !!(endpoint.mlsDevDevice && endpoint.mlsDevGroups);
4951
5046
  if (!mlsOn && (await endpoint.capabilities().catch(() => null))?.mls?.roomMode === "default") {
4952
- throw new Error("Canon encrypts every new conversation here, and this runner has no MLS device: give it mls (managedMlsDevice, or an operator's configuration: --mls-dev-config <path>).");
5047
+ 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
5048
  }
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}`);
5049
+ if (mlsOn && mls?.provisioned) {
5050
+ await assertMlsVaultInHome(endpoint, mls.provisioned);
5051
+ if ((await endpoint.mlsDevDevice.read()).status !== "credential_committed") {
5052
+ log("Setting up this agent's MLS device");
5053
+ const device = await runMlsDevSetup(endpoint, "setup", void 0, mls.provision, mls.provisioned).catch((error) => {
5054
+ throw new Error(`Could not set up this agent's MLS device: ${error instanceof Error ? error.message : String(error)}`);
5055
+ });
5056
+ log(`MLS device ${device.deviceId} is ${device.status}`);
5057
+ }
4960
5058
  }
4961
5059
  if (mlsOn) await endpoint.mlsDevDevice.ensureReady().catch(warn("MLS device not ready; encrypted rooms wait for it"));
4962
5060
  const keepPool = /* @__PURE__ */ __name(() => endpoint.mlsDevDevice.read().then(async (view) => {
@@ -5011,10 +5109,8 @@ ${JSON.stringify(result, null, 2)}`);
5011
5109
  roots = outbound ? (await Promise.all(outbound.roots.map((root) => realpath5(root).catch(() => "")))).filter(Boolean) : [];
5012
5110
  if (driver.capabilities.tools) tools = await startToolServer();
5013
5111
  await recover();
5014
- if (!options.relay) {
5015
- const publisher = createRuntimeStatePublisher({ agentId, clientType: CLIENT_TYPES[driver.name], hostMode: true, rtdb: initRTDBAuth(client) });
5016
- heartbeat = createRuntimeHeartbeat({ publisher, getRuntime: /* @__PURE__ */ __name(() => ({ runtimeDescriptor: descriptor }), "getRuntime"), onError: warn("runtime heartbeat") });
5017
- }
5112
+ const publisher = createRuntimeStatePublisher({ agentId, clientType: CLIENT_TYPES[driver.name], hostMode: true, rtdb: initRTDBAuth(client) });
5113
+ heartbeat = createRuntimeHeartbeat({ publisher, getRuntime: /* @__PURE__ */ __name(() => ({ runtimeDescriptor: descriptor }), "getRuntime"), onError: warn("runtime heartbeat") });
5018
5114
  inbound = endpoint.acceptInbound({ kind: "message.created", offer, onError: warn("inbox"), ...mlsOn ? { intervalMs: 1e3 } : {} });
5019
5115
  inbound.start();
5020
5116
  let reconnect = false;
@@ -5079,7 +5175,7 @@ ${JSON.stringify(result, null, 2)}`);
5079
5175
  for (const unregister of guards.values()) unregister();
5080
5176
  guards.clear();
5081
5177
  await client?.close();
5082
- await options.mls?.close?.().catch(warn("MLS custody close"));
5178
+ await mls?.close?.().catch(warn("MLS custody close"));
5083
5179
  })().finally(() => lock?.release());
5084
5180
  return stopping;
5085
5181
  },
@@ -5118,6 +5214,54 @@ ${JSON.stringify(result, null, 2)}`);
5118
5214
  const sent = await within(entry.sent, Math.max(0, deadline - Date.now()));
5119
5215
  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
5216
  return { conversationId, messageId, historyMessageId: id, status: sent === "delivered" ? "sent" : "pending" };
5217
+ },
5218
+ async relayStart(targetUserId, messageId, text3, selection, startOptions = {}) {
5219
+ 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) {
5220
+ throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A start needs a person or agent, a messageId and 1 byte to 32 KiB of text");
5221
+ }
5222
+ if (!options.relay || !client) throw new ConnectorRelayError(403, "CONNECTOR_RELAY_UNAVAILABLE", "This runner is not a relay");
5223
+ if (!mlsOn) throw new ConnectorRelayError(503, "CONNECTOR_RELAY_NO_DEVICE", "The relay has no MLS device");
5224
+ const deadline = startOptions.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
5225
+ const identity = { messageId, historyMessageId: `relay-start-${sha2563(messageId).slice(0, 32)}` };
5226
+ const answer = await within(
5227
+ endpoint.startWithheldOpener({
5228
+ targetUserId,
5229
+ text: text3,
5230
+ messageId: identity.historyMessageId,
5231
+ ...selection ? { selection } : {}
5232
+ }).then((result2) => ({ result: result2 }), (error) => ({ error })),
5233
+ Math.max(0, deadline - Date.now())
5234
+ );
5235
+ if (!answer || "error" in answer && answer.error?.retryable === true) {
5236
+ return { ...identity, status: "starting" };
5237
+ }
5238
+ if ("error" in answer) {
5239
+ const { status, code, message: message2 } = answer.error;
5240
+ if (code === "OPERATION_ID_COLLISION") {
5241
+ throw new ConnectorRelayError(409, "CONNECTOR_RELAY_MESSAGE_CONFLICT", "This messageId already started a conversation with other text or another person");
5242
+ }
5243
+ if ((status === 400 || status === 403 || status === 404 || status === 409) && typeof code === "string") {
5244
+ throw new ConnectorRelayError(status, code, typeof message2 === "string" ? message2 : "Canon refused the start");
5245
+ }
5246
+ throw answer.error;
5247
+ }
5248
+ const result = answer.result;
5249
+ if ("reason" in result) {
5250
+ if (result.reason === "opening-text-required") {
5251
+ throw new ConnectorRelayError(403, "CONNECTOR_RELAY_NOT_ENCRYPTED", "The relay serves only encrypted conversations its agent is in");
5252
+ }
5253
+ return { ...identity, status: result.status, reason: result.reason };
5254
+ }
5255
+ if ("requestId" in result) {
5256
+ return { ...identity, status: result.status, requestId: result.requestId, ...result.conversationId ? { conversationId: result.conversationId } : {} };
5257
+ }
5258
+ return {
5259
+ ...identity,
5260
+ status: "messaged",
5261
+ conversationId: result.conversationId,
5262
+ ...result.created !== void 0 ? { created: result.created } : {},
5263
+ ...result.reused !== void 0 ? { reused: result.reused } : {}
5264
+ };
5121
5265
  }
5122
5266
  };
5123
5267
  }
@@ -5157,6 +5301,7 @@ export {
5157
5301
  runMlsDevSetup,
5158
5302
  MLS_ENVIRONMENT_PINS,
5159
5303
  bundledMlsCustodyHelper,
5304
+ defaultMlsDevice,
5160
5305
  managedMlsDevice,
5161
5306
  startRelayChannel,
5162
5307
  loadRelayConfiguration,
@@ -4654,6 +4654,13 @@ function text4(value) {
4654
4654
  return value;
4655
4655
  }
4656
4656
  __name(text4, "text");
4657
+ function selection(value) {
4658
+ const args = record3(value, ["mode"], ["conversationId"]);
4659
+ if ((args.mode === "new" || args.mode === "latest_or_new") && args.conversationId === void 0) return { mode: args.mode };
4660
+ if (args.mode === "specific") return { mode: "specific", conversationId: room(args.conversationId) };
4661
+ return invalid11("selection");
4662
+ }
4663
+ __name(selection, "selection");
4657
4664
  function limit(value) {
4658
4665
  if (value === void 0) return void 0;
4659
4666
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 100) invalid11("limit");
@@ -4703,6 +4710,15 @@ function parseConnectorRelayRequest(value) {
4703
4710
  const args = record3(body.args, ["conversationId", "messageId", "text"]);
4704
4711
  return { ...head, op: "send", args: { conversationId: room(args.conversationId), messageId: id6(args.messageId, "messageId"), text: text4(args.text) } };
4705
4712
  }
4713
+ case "start": {
4714
+ const args = record3(body.args, ["targetUserId", "messageId", "text"], ["selection"]);
4715
+ return { ...head, op: "start", args: {
4716
+ targetUserId: doc(args.targetUserId, "targetUserId"),
4717
+ messageId: id6(args.messageId, "messageId"),
4718
+ text: text4(args.text),
4719
+ ...args.selection === void 0 ? {} : { selection: selection(args.selection) }
4720
+ } };
4721
+ }
4706
4722
  case "credential": {
4707
4723
  const args = record3(body.args, ["installationId", "generation", "credential"]);
4708
4724
  if (!Number.isSafeInteger(args.generation) || args.generation < 1) invalid11("generation");
@@ -110,7 +110,7 @@ import {
110
110
  readMembershipChangeMetadata,
111
111
  resolveAllowedUploadMimeType,
112
112
  sanitizeDownloadFileName
113
- } from "./chunk-GWQF4WFT.js";
113
+ } from "./chunk-C2WJT3VX.js";
114
114
 
115
115
  // src/media.ts
116
116
  import { createWriteStream as createWriteStream2 } from "node:fs";
@@ -5219,7 +5219,7 @@ function createMlsDevDeviceClient(options, context) {
5219
5219
  const store = guardedStore();
5220
5220
  const fence = /* @__PURE__ */ __name(() => store.transaction(async () => {
5221
5221
  }), "fence");
5222
- async function initialized() {
5222
+ async function initialized(storedPrincipalId) {
5223
5223
  context.assertOpen();
5224
5224
  const { identity, installationGeneration } = await context.ready();
5225
5225
  await fence();
@@ -5230,7 +5230,7 @@ function createMlsDevDeviceClient(options, context) {
5230
5230
  const binding = parseMlsDevDeviceBinding({
5231
5231
  version: 1,
5232
5232
  environmentId: identity.environmentId,
5233
- principal: { ...principal, id: await context.mlsPrincipalId() },
5233
+ principal: { ...principal, id: storedPrincipalId ?? await context.mlsPrincipalId() },
5234
5234
  installationId: identity.installationId
5235
5235
  });
5236
5236
  const coordinator = /* @__PURE__ */ __name((journal) => createMlsDevDeviceCoordinator({
@@ -5517,6 +5517,14 @@ function createMlsDevDeviceClient(options, context) {
5517
5517
  }
5518
5518
  }
5519
5519
  __name(ensureReady, "ensureReady");
5520
+ async function revoke() {
5521
+ const { coordinator, transport: http } = await initialized();
5522
+ const device = coordinator(store), view3 = await device.read();
5523
+ if (view3.status === "revoked" || !view3.registration) return view3;
5524
+ const receipt = await http.revokeCredential({ version: 1, deviceId: view3.registration.device.deviceId }, context.signal);
5525
+ return device.acceptRevocation(receipt);
5526
+ }
5527
+ __name(revoke, "revoke");
5520
5528
  function run(goal) {
5521
5529
  context.assertOpen();
5522
5530
  const preceding = running;
@@ -5543,12 +5551,26 @@ function createMlsDevDeviceClient(options, context) {
5543
5551
  return promise;
5544
5552
  }
5545
5553
  __name(run, "run");
5554
+ function read(options2) {
5555
+ return context.run(async () => {
5556
+ let storedPrincipalId;
5557
+ if (options2?.localOnly) {
5558
+ await context.ready();
5559
+ const saved = await store.transaction((tx) => tx.get("mls-dev-device/identity"));
5560
+ if (!saved) return void 0;
5561
+ storedPrincipalId = parseMlsDevDeviceBinding(saved.binding).principal.id;
5562
+ }
5563
+ return (await initialized(storedPrincipalId)).coordinator(store).read();
5564
+ });
5565
+ }
5566
+ __name(read, "read");
5546
5567
  return Object.freeze({
5547
- read: /* @__PURE__ */ __name(() => context.run(async () => (await initialized()).coordinator(store).read()), "read"),
5568
+ read,
5548
5569
  enroll: /* @__PURE__ */ __name(() => run("enroll"), "enroll"),
5549
5570
  renew: /* @__PURE__ */ __name(() => run("renew"), "renew"),
5550
5571
  reconcile: /* @__PURE__ */ __name(() => run("reconcile"), "reconcile"),
5551
- ensureReady: /* @__PURE__ */ __name(() => run("ready"), "ensureReady")
5572
+ ensureReady: /* @__PURE__ */ __name(() => run("ready"), "ensureReady"),
5573
+ revoke: /* @__PURE__ */ __name(() => context.run(revoke), "revoke")
5552
5574
  });
5553
5575
  }
5554
5576
  __name(createMlsDevDeviceClient, "createMlsDevDeviceClient");
@@ -7242,7 +7264,11 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
7242
7264
  const group = envelope((await readPreparationSnapshot(tx, scope4(conversationId))).protocolState, init.binding, conversationId);
7243
7265
  const accounts = mlsRoomAccounts(room, init.binding.environmentId);
7244
7266
  const admitted = new Set((group && !group.removed ? group.roster : []).map((entry) => mlsAccountOf(accounts, entry.device.principal)));
7245
- const waiting = [...Array.isArray(room.memberIds) ? room.memberIds : [], ...typeof room.mlsInviteeId === "string" ? [room.mlsInviteeId] : []].filter((memberId) => typeof memberId === "string" && !admitted.has(memberId));
7267
+ const waiting = [.../* @__PURE__ */ new Set([
7268
+ ...Array.isArray(room.memberIds) ? room.memberIds : [],
7269
+ ...typeof room.mlsInviteeId === "string" ? [room.mlsInviteeId] : [],
7270
+ ...Array.isArray(room.mlsPendingFounderIds) ? room.mlsPendingFounderIds : []
7271
+ ])].filter((memberId) => typeof memberId === "string" && !admitted.has(memberId));
7246
7272
  return Object.freeze({ held: waiting.length > 0, waitingPrincipalIds: Object.freeze(waiting) });
7247
7273
  });
7248
7274
  }
@@ -9514,6 +9540,13 @@ function createEndpoint(options) {
9514
9540
  createdAt: timestamp,
9515
9541
  updatedAt: timestamp
9516
9542
  };
9543
+ if (normalized.action === "start_conversation") {
9544
+ const opener = normalized.body.opener;
9545
+ if (opener?.withheld && typeof opener.messageId === "string") {
9546
+ const pending = await tx.get(`${openerPrefix()}/${encodeURIComponent(opener.messageId)}`);
9547
+ if (pending?.start?.operationId === operationId) created.openerFingerprint = pending.openerFingerprint;
9548
+ }
9549
+ }
9517
9550
  return commitOperationRecord(tx, created);
9518
9551
  }, "commitOperation");
9519
9552
  const operation = sensitive ? await store.transaction(async (tx) => {
@@ -9766,6 +9799,7 @@ function createEndpoint(options) {
9766
9799
  },
9767
9800
  async startWithheldOpener(input) {
9768
9801
  assertOpen();
9802
+ input = copyJson(input);
9769
9803
  await initialization;
9770
9804
  if (typeof input.messageId !== "string" || !input.messageId || typeof input.text !== "string")
9771
9805
  throw new EndpointError("Invalid deferred opening line", { status: 400, code: "INVALID_DEFERRED_OPENER", retryable: false });
@@ -9776,9 +9810,16 @@ function createEndpoint(options) {
9776
9810
  opener: { withheld: true, messageId: input.messageId },
9777
9811
  ...input.selection ? { selection: copyJson(input.selection) } : {}
9778
9812
  };
9813
+ const openerFingerprint = await hash3(canonicalEndpointJson([body, input.text]));
9779
9814
  const recordKey = `${openerPrefix()}/${encodeURIComponent(input.messageId)}`;
9780
9815
  await store.transaction(async (tx) => {
9781
9816
  await assertCurrentInstallation(tx);
9817
+ const operation = await tx.get(key(operationId));
9818
+ if (operation) {
9819
+ if (operation.openerFingerprint !== openerFingerprint)
9820
+ throw new EndpointError("Opening line ID was already used for different content", { status: 409, code: "OPERATION_ID_COLLISION", retryable: false, operationId });
9821
+ return;
9822
+ }
9782
9823
  const existing = await tx.get(recordKey);
9783
9824
  if (existing && (existing.text !== input.text || canonicalEndpointJson(existing.start?.body) !== canonicalEndpointJson(body)))
9784
9825
  throw new EndpointError("Opening line ID was already used for different content", { status: 409, code: "OPERATION_ID_COLLISION", retryable: false, operationId });
@@ -9787,6 +9828,7 @@ function createEndpoint(options) {
9787
9828
  messageId: input.messageId,
9788
9829
  text: input.text,
9789
9830
  requestId: null,
9831
+ openerFingerprint,
9790
9832
  operationId: `send-opener-${digest.slice(0, 48)}`,
9791
9833
  start: { operationId, body }
9792
9834
  });
@@ -10119,7 +10161,7 @@ async function acquireAgentEndpoint(input) {
10119
10161
  }
10120
10162
  const requestedMls = input.options.mlsDev;
10121
10163
  const mls = requestedMls ? await (async () => {
10122
- const crypto2 = await import("./mls-crypto-LGPCXM3K.js");
10164
+ const crypto2 = await import("./mls-crypto-Y44BOSEM.js");
10123
10165
  const configuration = crypto2.parseMlsDevClientConfiguration(requestedMls.configuration);
10124
10166
  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");
10125
10167
  const custody = requestedMls.custody;
@@ -2,7 +2,7 @@ import {
2
2
  createMlsDevCredentialVerifier,
3
3
  mlsDevDeviceHttpConfiguration,
4
4
  parseMlsDevClientConfiguration
5
- } from "./chunk-GWQF4WFT.js";
5
+ } from "./chunk-C2WJT3VX.js";
6
6
  export {
7
7
  createMlsDevCredentialVerifier,
8
8
  mlsDevDeviceHttpConfiguration,
package/dist/cli.js CHANGED
@@ -3,20 +3,19 @@ import {
3
3
  MLS_SETUP_ACTIONS,
4
4
  assertPrivateHome,
5
5
  awaitErrand,
6
- bundledMlsCustodyHelper,
7
6
  checkCodex,
8
7
  claudeDriver,
9
8
  codeDriver,
10
9
  codexDriver,
11
10
  connectorDriver,
12
11
  createRunner,
12
+ defaultMlsDevice,
13
13
  errandJournal,
14
14
  errandsDirectory,
15
15
  loadClaudeQuery,
16
16
  loadInstructions,
17
17
  loadMlsDevConfiguration,
18
18
  loadRelayConfiguration,
19
- managedMlsDevice,
20
19
  queueErrand,
21
20
  readRelayCredential,
22
21
  relayRunnerHome,
@@ -24,7 +23,7 @@ import {
24
23
  runnerLockName,
25
24
  startRelayChannel,
26
25
  writeRelayCredential
27
- } from "./chunks/chunk-NXL4C2R6.js";
26
+ } from "./chunks/chunk-3KFVBOVI.js";
28
27
  import {
29
28
  CanonApiError,
30
29
  CanonClient,
@@ -35,12 +34,11 @@ import {
35
34
  resolveCanonRuntimeConnection,
36
35
  runCli,
37
36
  verifyCanonRuntimeConnection
38
- } from "./chunks/chunk-6PKXASSK.js";
37
+ } from "./chunks/chunk-TFSURFF6.js";
39
38
  import {
40
39
  __name,
41
- isCanonClientVersion,
42
- isMlsEnvironmentId
43
- } from "./chunks/chunk-GWQF4WFT.js";
40
+ isCanonClientVersion
41
+ } from "./chunks/chunk-C2WJT3VX.js";
44
42
 
45
43
  // src/cli.ts
46
44
  import { realpathSync } from "node:fs";
@@ -464,9 +462,7 @@ async function settings(values, { key = true } = {}) {
464
462
  __name(settings, "settings");
465
463
  async function mlsDevice(values, environmentId, home) {
466
464
  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;
465
+ return defaultMlsDevice(home, environmentId);
470
466
  }
471
467
  __name(mlsDevice, "mlsDevice");
472
468
  async function mlsSetup(values) {
@@ -10,7 +10,7 @@ export interface ConnectorInbox {
10
10
  /**
11
11
  * The first claim gets `runAllowed` and the turn's input, once Canon confirms the agent may still act on it (its
12
12
  * membership and the room's audience, read now, never from a hint); a repeat gets its state, never the input again.
13
- * A claim that cannot be confirmed by `deadline` (epoch ms) is refused as busy and the input stays pending.
13
+ * A claim not confirmed by `deadline` (epoch ms) is busy; retry the same request id to finish that confirmation.
14
14
  */
15
15
  claim(inputId: string, requestId: string, options?: {
16
16
  deadline?: number;
@@ -27,8 +27,9 @@ export interface ConnectorDriver extends RuntimeDriver {
27
27
  /**
28
28
  * The `connector` driver: the runtime is an assistant its provider runs (ChatGPT, Claude.ai and the like), reached
29
29
  * through Canon's remote MCP connector, so a turn waits, as one pending input, for the assistant to claim it and answer
30
- * through the connector's pickup tools (spec §11.3). It keeps no timer: an assistant that never answers is the runner's
31
- * no-progress deadline, which a claim restarts. It never calls Canon itself; the relay channel calls its inbox, and the
32
- * runner confirms each claim with Canon.
30
+ * through the connector's pickup tools (spec §11.3). Such an assistant acts only when its user prompts it, so an input
31
+ * waits as long as it takes: the runner submits it, and starts its no-progress deadline, only at the claim. The driver
32
+ * keeps no timer. It never calls Canon itself; the relay channel calls its inbox, and the runner confirms (and submits)
33
+ * each claim with Canon.
33
34
  */
34
35
  export declare function connectorDriver(): ConnectorDriver;
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  runMlsDevSetup,
22
22
  runtimeDescriptor,
23
23
  startRelayChannel
24
- } from "./chunks/chunk-NXL4C2R6.js";
24
+ } from "./chunks/chunk-3KFVBOVI.js";
25
25
  import {
26
26
  CanonApiError,
27
27
  DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
@@ -46,8 +46,8 @@ import {
46
46
  toAnthropicImageBlock,
47
47
  toAnthropicImageBlocksWithinBudget,
48
48
  uploadMediaFile
49
- } from "./chunks/chunk-6PKXASSK.js";
50
- import "./chunks/chunk-GWQF4WFT.js";
49
+ } from "./chunks/chunk-TFSURFF6.js";
50
+ import "./chunks/chunk-C2WJT3VX.js";
51
51
  export {
52
52
  CanonApiError,
53
53
  ConnectorRelayError,
@@ -1,3 +1,4 @@
1
+ import type { DirectConversationSelection } from './communication.js';
1
2
  /**
2
3
  * The class-C relay (docs/design/agent-runner.md §11): a Canon-operated process that relays a connector agent's
3
4
  * encrypted rooms, holding an MLS device for the agent and decrypting and re-encrypting messages for the assistant (which
@@ -108,6 +109,14 @@ export type ConnectorRelayRequest = {
108
109
  messageId: string;
109
110
  text: string;
110
111
  };
112
+ } | {
113
+ op: 'start';
114
+ args: {
115
+ targetUserId: string;
116
+ messageId: string;
117
+ text: string;
118
+ selection?: DirectConversationSelection;
119
+ };
111
120
  } | {
112
121
  op: 'credential';
113
122
  args: ConnectorRelayCredentialDelivery;
@@ -172,6 +181,31 @@ export interface ConnectorRelaySent {
172
181
  historyMessageId: string;
173
182
  status: 'sent' | 'pending';
174
183
  }
184
+ /**
185
+ * A start (§11.5): the relay's device founds (or finds) the encrypted room and sends the opening line itself, as any
186
+ * device does with a withheld opener. `messageId` is the start's identity, as for a send; `historyMessageId` is the
187
+ * opening line's id in the room. `starting`: the outcome is unresolved, including whether local recording completed;
188
+ * retry with the same `messageId` until a terminal answer. Canon's answers otherwise: `messaged` (the line is going into `conversationId`),
189
+ * `requested` or `pending` (it goes once the request is approved), `blocked` or `unavailable`.
190
+ */
191
+ export type ConnectorRelayStarted = {
192
+ messageId: string;
193
+ historyMessageId: string;
194
+ } & ({
195
+ status: 'messaged';
196
+ conversationId: string;
197
+ created?: boolean;
198
+ reused?: boolean;
199
+ } | {
200
+ status: 'requested' | 'pending';
201
+ requestId: string;
202
+ conversationId?: string;
203
+ } | {
204
+ status: 'starting';
205
+ } | {
206
+ status: 'blocked' | 'unavailable';
207
+ reason: string;
208
+ });
175
209
  export declare class ConnectorRelayRequestError extends Error {
176
210
  name: string;
177
211
  }
@@ -97,7 +97,7 @@ export interface EndpointCapabilities {
97
97
  actions: string[];
98
98
  queries: string[];
99
99
  maintenance: boolean;
100
- /** Only for a principal eligible for MLS here: how new rooms are born (functions `rollout.ts`, `CANON_MLS_DEV_ROOMS`). */
100
+ /** Only while MLS is on here, where every valid principal is eligible: how new rooms are born (functions `devActivation.ts`). */
101
101
  mls?: {
102
102
  eligible: true;
103
103
  roomMode: 'default';
@@ -67,6 +67,8 @@ export interface EndpointOperation {
67
67
  preparedContent?: PreparedContent;
68
68
  /** Authored-input collision check, never recorded for memory-only answers. */
69
69
  inputFingerprint?: string;
70
+ /** Local-only binding of a withheld start's text; retained after the opener is sent or its payload is erased. */
71
+ openerFingerprint?: string;
70
72
  receipt?: EndpointOperationReceipt;
71
73
  attempts: number;
72
74
  attempted: boolean;
@@ -196,7 +198,9 @@ export interface Endpoint {
196
198
  text: string;
197
199
  requestId?: string | null;
198
200
  }): Promise<void>;
199
- /** Persist the opening line before starting a room; recover the start through its durable receipt. */
201
+ /**
202
+ * Persist the opening line before starting a room; recover the start through its durable receipt.
203
+ */
200
204
  startWithheldOpener(input: Omit<StartDirectInput, 'opener'> & {
201
205
  messageId: string;
202
206
  text: string;
@@ -17,6 +17,11 @@ export interface MlsDevDeviceClientOptions {
17
17
  /** Explicit synthetic Dev operations. No timers, eager enrollment or room authority. */
18
18
  export interface MlsDevDeviceClient {
19
19
  read(): Promise<MlsDevDeviceView>;
20
+ /** Read only committed local device state, without discovering or waiting for its remote alias.
21
+ * Returns undefined when no local device exists. Used to capture the device before logout erases it. */
22
+ read(options: {
23
+ localOnly: true;
24
+ }): Promise<MlsDevDeviceView | undefined>;
20
25
  /** Ensure local registration; an existing view is not an online status check. */
21
26
  enroll(): Promise<MlsDevDeviceView>;
22
27
  renew(): Promise<MlsDevDeviceView>;
@@ -28,6 +33,10 @@ export interface MlsDevDeviceClient {
28
33
  * current credential is valid, a failed renewal returns it and is retried
29
34
  * after a minute. Never creates an unrequested enrollment. */
30
35
  ensureReady(): Promise<MlsDevDeviceView>;
36
+ /** Retire this registered device at Canon for good (`POST /revoke`): it can no longer authenticate, renew or be offered
37
+ * to a room, and readiness refuses it. A device Canon never registered has
38
+ * nothing to revoke; a revoked one is returned as it is. Rooms keep its leaf until Canon can remove devices (step 5c). */
39
+ revoke(): Promise<MlsDevDeviceView>;
31
40
  }
32
41
  /** Whether readiness would renew this registered device: it has no credential, or less than half its lifetime remains. */
33
42
  export declare function isMlsDevCredentialRenewalDue(view: MlsDevDeviceView, now: number): boolean;
package/dist/media.js CHANGED
@@ -18,8 +18,8 @@ import {
18
18
  toAnthropicImageBlock,
19
19
  toAnthropicImageBlocksWithinBudget,
20
20
  uploadMediaFile
21
- } from "./chunks/chunk-6PKXASSK.js";
22
- import "./chunks/chunk-GWQF4WFT.js";
21
+ } from "./chunks/chunk-TFSURFF6.js";
22
+ import "./chunks/chunk-C2WJT3VX.js";
23
23
  export {
24
24
  DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
25
25
  DEFAULT_MEDIA_CACHE_DIR,
@@ -1,4 +1,4 @@
1
- import type { MlsDevDeviceContext, MlsEnvironmentId } from '../internal/backend-contracts/index.js';
1
+ import { type MlsDevDeviceContext, type MlsEnvironmentId } from '../internal/backend-contracts/index.js';
2
2
  import type { RunnerMlsOptions } from './mls.js';
3
3
  export interface ManagedMlsDeviceOptions {
4
4
  /** The runner's home (CANON_HOME, or a relay installation's home). */
@@ -28,4 +28,9 @@ export type ManagedMlsDevice = RunnerMlsOptions & {
28
28
  provisioned(context: MlsDevDeviceContext): boolean;
29
29
  close(): Promise<void>;
30
30
  };
31
+ /**
32
+ * A runner's device where it is given none (`createRunner` and the CLI alike): managed custody with this package's helper
33
+ * for the host, or undefined where it ships none or the environment has no MLS.
34
+ */
35
+ export declare function defaultMlsDevice(home: string, environmentId: string): ManagedMlsDevice | undefined;
31
36
  export declare function managedMlsDevice(options: ManagedMlsDeviceOptions): ManagedMlsDevice;
@@ -84,11 +84,17 @@ export declare function mlsDevRoomPackageId(identity: {
84
84
  installationId: string;
85
85
  ownershipVersion?: number;
86
86
  }, room: string): string;
87
+ /**
88
+ * Managed custody opens only the vaults it made in its home. A device the engine store holds with no vault there (set up
89
+ * with an operator's configuration, or its vault lost) is refused once, here, rather than failing in every room.
90
+ */
91
+ export declare function assertMlsVaultInHome(endpoint: CanonEndpoint, provisioned: (context: MlsDevDeviceContext) => boolean): Promise<void>;
87
92
  /**
88
93
  * One operator step for the agent's device (`canon-agent mls`), on the same engine store the runner uses. It never
89
94
  * starts a model, and prints only public device state: no credentials, proofs, envelopes or history. `setup` does what
90
95
  * 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.
96
+ * with `provisioned` (managed custody) it provisions only where this installation has none, and refuses a device whose
97
+ * vault is elsewhere.
92
98
  */
93
99
  export declare function runMlsDevSetup(endpoint: CanonEndpoint, action: MlsSetupAction, room?: string, provision?: (context: MlsDevDeviceContext) => Promise<void>, provisioned?: (context: MlsDevDeviceContext) => boolean): Promise<{
94
100
  conversationId?: string | undefined;
@@ -1,9 +1,9 @@
1
1
  import { type ConnectorRelayCredentialDelivery } from '../internal/backend-contracts/index.js';
2
2
  import type { ConnectorInbox } from '../drivers/connector.js';
3
3
  import type { Runner } from './runner.js';
4
- /** What the channel serves: the relay's runner (history and sends) and its connector driver's inbox (pickup). */
4
+ /** What the channel serves: the relay's runner (history, sends and starts) and its connector driver's inbox (pickup). */
5
5
  export interface RelayTarget {
6
- runner: Pick<Runner, 'relayRead' | 'relaySend'>;
6
+ runner: Pick<Runner, 'relayRead' | 'relaySend' | 'relayStart'>;
7
7
  inbox: ConnectorInbox;
8
8
  }
9
9
  /** Canon's credential for this relay, as delivered: the connection it serves is the delivering request's. */
@@ -12,7 +12,7 @@ export type RelayCredentialDelivery = ConnectorRelayCredentialDelivery & {
12
12
  };
13
13
  export interface RelayChannelOptions {
14
14
  /** The runner and inbox to serve from the start; `serve` sets or replaces them. Until then only `credential` is answered. */
15
- runner?: Pick<Runner, 'relayRead' | 'relaySend'>;
15
+ runner?: Pick<Runner, 'relayRead' | 'relaySend' | 'relayStart'>;
16
16
  inbox?: ConnectorInbox;
17
17
  /** The relayed agent: requests naming another agent are refused. */
18
18
  agentId: string;
@@ -1,5 +1,5 @@
1
1
  import type { AgentWakeConfiguration } from '../internal/backend-contracts/index.js';
2
- import { type ConnectorRelayPage, type ConnectorRelaySent } from '../internal/backend-contracts/index.js';
2
+ import { type ConnectorRelayPage, type ConnectorRelaySent, type ConnectorRelayStarted, type DirectConversationSelection } from '../internal/backend-contracts/index.js';
3
3
  import { type CanonEndpoint, type CanonRuntimeDescriptor, type CommunicateInput, type CommunicateResult } from '../internal/core/index.js';
4
4
  import { type RunnerMlsOptions } from './mls.js';
5
5
  import { type RuntimeDriver } from './types.js';
@@ -26,16 +26,18 @@ export interface RunnerOptions {
26
26
  baseUrl?: string;
27
27
  streamUrl?: string;
28
28
  /**
29
- * The agent's MLS device (Dev only, spec §6): issuer pins and custody, from `loadMlsDevConfiguration`. With it the
30
- * runner takes part in encrypted rooms; without it each encrypted room is refused, once, in the log. An injected
31
- * `endpoint` that already has an MLS device counts as configured. `close` runs last on shutdown.
29
+ * The agent's MLS device (spec §6): issuer pins and custody, from `loadMlsDevConfiguration` or `managedMlsDevice`. Left
30
+ * out, it is managed custody in `home` where this package ships a custody helper for the host, as the CLI's; `false` is
31
+ * none. With a device the runner takes part in encrypted rooms; without one each encrypted room is refused, once, in the
32
+ * log. An injected `endpoint` brings its own device, if any. `close` runs last on shutdown.
32
33
  */
33
- mls?: RunnerMlsOptions;
34
+ mls?: RunnerMlsOptions | false;
34
35
  /**
35
36
  * Relay mode, the class-C relay (spec §11.4): only encrypted rooms are served, which is what lets a `remote` driver
36
37
  * (the connector) into them. Plaintext input is cancelled with no turn (the connector's own installation serves it),
37
- * control hints are read only for encrypted rooms, no runtime descriptor is published, and `relayRead` and `relaySend`
38
- * serve the relay channel. It needs the agent's MLS device.
38
+ * control hints are read only for encrypted rooms, and `relayRead` and `relaySend` serve the relay channel. It publishes
39
+ * the runtime descriptor and heartbeat, so the apps show a relayed turn working and offer Stop. It needs the agent's MLS
40
+ * device.
39
41
  */
40
42
  relay?: boolean;
41
43
  /** The engine's installation: the relay's reserved one, from its configuration. A store opened under another refuses to start. */
@@ -95,6 +97,15 @@ export interface Runner {
95
97
  relaySend(conversationId: string, messageId: string, text: string, options?: {
96
98
  deadline?: number;
97
99
  }): Promise<ConnectorRelaySent>;
100
+ /**
101
+ * Relay mode (§11.5): a conversation started with a person or agent, its opening line withheld from Canon and kept
102
+ * here first; this device founds (or finds) the encrypted room and sends the line. The same `messageId` is the same
103
+ * start. It answers by `deadline`: `starting` means unresolved, even if recording has not finished; retry the same
104
+ * identity until another answer arrives. Only a persisted start resumes automatically after a restart.
105
+ */
106
+ relayStart(targetUserId: string, messageId: string, text: string, selection?: DirectConversationSelection, options?: {
107
+ deadline?: number;
108
+ }): Promise<ConnectorRelayStarted>;
98
109
  }
99
110
  /** RUN-27: generated once from the driver's declarations; no static capability table. */
100
111
  export declare function runtimeDescriptor(driver: RuntimeDriver): CanonRuntimeDescriptor;
@@ -36,8 +36,9 @@ export interface DriverTurn {
36
36
  tools: TurnTools;
37
37
  /**
38
38
  * For a `remote` runtime, before its input first leaves this host (the connector's claim): the runner reads the
39
- * agent's membership, and an encrypted room's audience, from Canon now and counts the pickup as progress (RUN-12).
40
- * It rejects, and ends the turn, when the agent may no longer act on this input; a transient failure only rejects.
39
+ * agent's membership, and an encrypted room's audience, from Canon now, submits the input on the first confirmed
40
+ * pickup (until then it is only offered, with no deadline) and counts each pickup as progress (RUN-12). It rejects,
41
+ * and ends the turn, when the agent may no longer act on this input; a transient failure only rejects.
41
42
  */
42
43
  confirmPickup(): Promise<void>;
43
44
  }
@@ -65,7 +66,9 @@ export interface DriverCapabilities {
65
66
  /**
66
67
  * The runtime runs off the runner's host (the class-C connector, spec §11.3): it gets no local files (inbound
67
68
  * attachments are named in the prompt, never materialized), and since its plaintext leaves this host, the runner
68
- * takes it into encrypted rooms only in relay mode, whose installation discloses that.
69
+ * takes it into encrypted rooms only in relay mode, whose installation discloses that. Its input is offered first and
70
+ * submitted only when the runtime picks it up (`confirmPickup`); newer messages in the room join an input not yet
71
+ * picked up.
69
72
  */
70
73
  remote?: boolean;
71
74
  /** Across rooms; runner default 8. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "12.4.1",
3
+ "version": "12.5.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",