@canonmsg/agent-sdk 12.3.1 → 12.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -192,8 +192,8 @@ 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 (Dev only); see [Encrypted rooms](#encrypted-rooms-dev) |
196
- | `relay`, `installationId` | off | Canon's class-C relay (Dev only); see [The class-C relay](#the-class-c-relay-dev-canon-operated) |
195
+ | `mls` | none | The agent's MLS device for encrypted rooms; see [Encrypted rooms](#encrypted-rooms) |
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 |
199
199
  | `mentionAliases` | none | Encrypted rooms: other names that @mention the agent, such as its role |
@@ -209,9 +209,9 @@ The CLI is `canon-agent run --driver code --module <path> [--profile <name>] [--
209
209
 
210
210
  **The home.** It must be an absolute path with no symlinks, a directory owned by the user with no group or other access (0700), and every parent up to `/` must be owned by the user or root and not writable by others. A missing home is created with mode 0700. Otherwise the runner refuses to start and names the path. One instance of an agent runs per home; a second one fails to start. Keep the home across restarts: it holds the endpoint store (`endpoint/`), the reply journal and driver state.
211
211
 
212
- ## Encrypted rooms (Dev)
212
+ ## Encrypted rooms
213
213
 
214
- Encrypted (MLS) rooms are a Dev preview, for agents whose communication policies are all open. 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 Dev issuer public keys, never private keys or credentials. `issuerKeys` is a list, and during an issuer rotation it must hold both key versions (Dev's version 1 issues until 2026-11-07, version 2 after), 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. 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:
215
215
 
216
216
  ```sh
217
217
  canon-agent mls --action status --mls-dev-config /abs/mls-dev.json --profile dev-agent
@@ -226,15 +226,15 @@ In an encrypted room:
226
226
 
227
227
  - Turns take a person's messages, read from the device's verified history under the room's current audience, and another agent's only when it mentions this agent, a few times in a row at most (see [Team rooms](#team-rooms)). Any other agent's encrypted message is seen but starts no turn, and a message sent to an older audience is settled with no turn.
228
228
  - A change of membership stops the running turn and withholds its reply. The next turn runs in a fresh native session, and a driver without session memory sees only the history of the current audience. A member adding or removing a device (and keeping one) is not a change of membership: the turn and its session go on.
229
- - Canon receives encrypted text and a content-free turn state, nothing else. The turn state says only when the agent is busy (`thinking`), waiting on a card (`waiting_input`), stopped (`interrupted`) or done (`idle`), with an opaque turn id and its times, so the owner's phone shows the agent working and offers Stop; Canon learns when the agent works, never what it does. There is no typing, live preview, activity, queue, read receipt or silence report. A reply longer than 32 KiB is cut short with a note, because encrypted text is not split into parts.
230
- - The tools are `no_reply` and a `communicate` that lists conversations or agents and sends text to the same room only, plus `ask_question` in a room created for approvals and questions (`canon.mls.dev-text.v2`, and `v3`, which Canon creates). `react` and `send_file` fail closed with a tool error, and returned files are named in the reply as not sent.
229
+ - Canon receives encrypted text, a content-free turn state and, from `send_file` in a v3 room, the files themselves as unencrypted items; nothing else. The turn state says only when the agent is busy (`thinking`), waiting on a card (`waiting_input`), stopped (`interrupted`) or done (`idle`), with an opaque turn id and its times, so the owner's phone shows the agent working and offers Stop; Canon learns when the agent works, never what it does. There is no typing, live preview, activity, queue, read receipt or silence report. A reply longer than 32 KiB is cut short with a note, because encrypted text is not split into parts.
230
+ - The tools are `no_reply` and a `communicate` that lists conversations or agents and sends text to the same room only, plus `ask_question` in a room created for approvals and questions (`canon.mls.dev-text.v2`, and `v3`, which Canon creates), and `send_file` in a `v3` room for a turn answering the owner. Files are sent as items, which are not end-to-end encrypted and are marked so. `react` fails closed with a tool error. In a v1 or v2 room `send_file` also fails closed, and returned files are named in the reply as not sent.
231
231
  - In a room created for them, approvals and questions are encrypted cards that only the room's members can read, put to the person whose message the turn answers (the owner for a secret, and when an agent's message started the turn). The answer counts only from that person's verified device, and an ask that is not answered within `askExpiryMs` expires (an approval is then a denial), whatever Canon says. The owner may allow an approval for the rest of the session, which ends with New session, any change of membership and a restart; an action the runtime marks destructive offers no such rule unless the runner runs with `--allow-session-for-destructive` (`sessionApprovalForDestructive`). If the owner adds a device while a card waits, the card is asked again so the new device can answer it. Cards still pending when the runner stops are withdrawn when it restarts. In a room created as `canon.mls.dev-text.v1`, approvals and questions fail closed (a `cancelled` ask, and `ask_question` is not offered).
232
232
  - 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
233
  - 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
234
 
235
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.
236
236
 
237
- ### The class-C relay (Dev, Canon-operated)
237
+ ### The class-C relay (Canon-operated)
238
238
 
239
239
  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
240
 
@@ -29,7 +29,7 @@ import {
29
29
  sendMessageWithRetryChunked,
30
30
  splitTextByUtf8Bytes,
31
31
  verifyCanonRuntimeConnection
32
- } from "./chunk-SKFI2MJA.js";
32
+ } from "./chunk-RFNYPZXD.js";
33
33
  import {
34
34
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
35
35
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
@@ -8080,6 +8080,15 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
8080
8080
  return run(() => serializeInboundPreparation(store, id(data.conversationId), () => submit("remove", data)));
8081
8081
  }, "remove"),
8082
8082
  refresh: /* @__PURE__ */ __name((value) => run(() => serializeInboundPreparation(store, id(value), () => refresh(id(value)))), "refresh"),
8083
+ // Canon refuses a feed read unless the room's mlsFeedOrdinal is its feed's last entry, and the feed reports the
8084
+ // room's membershipRevision (devGroups.ts), so a fresh snapshot shows whether a fetch would bring anything.
8085
+ heldCurrent: /* @__PURE__ */ __name((value, snapshot) => run(async () => {
8086
+ const cid = id(value), latest = snapshot.mlsFeedOrdinal;
8087
+ if (typeof latest !== "number" || !Number.isSafeInteger(latest) || snapshot.mlsRemoval !== null && snapshot.mlsRemoval !== void 0 || !await textReady(cid)) return false;
8088
+ const held = await store.transaction(async (tx) => (await tx.get(feedKey(cid)) ?? 0) >= latest && (await tx.get(authorityKey(cid)))?.membershipRevision === snapshot.membershipRevision);
8089
+ if (held) caughtUpAt.set(cid, context.clock());
8090
+ return held;
8091
+ }, false), "heldCurrent"),
8083
8092
  read: /* @__PURE__ */ __name((value) => run(() => read(id(value)), false), "read"),
8084
8093
  readVerifiedMessageBinding: /* @__PURE__ */ __name((input) => {
8085
8094
  const data = record5(input, ["conversationId", "messageId"]);
@@ -8845,8 +8854,11 @@ function createEndpoint(options) {
8845
8854
  const local = await store.transaction((tx) => tx.get(`conversation/${conversationId}`));
8846
8855
  const knownProtected = await store.transaction((tx) => isMlsDevRoomProtected(tx, conversationId));
8847
8856
  await initialization;
8857
+ let snapshot = null;
8848
8858
  if (discover && (mlsDevGroups || !local && await inMlsEnvironment())) {
8859
+ const fresh = !reads.has(canonicalEndpointJson({ name: "conversation", parameters: { conversationId } }));
8849
8860
  const room = await endpoint.query("conversation", { conversationId });
8861
+ if (fresh) snapshot = room;
8850
8862
  if (room) await endpoint.applyConversationSnapshot(room);
8851
8863
  else if (!local && !knownProtected) throw new EndpointError("Conversation mode is unavailable", { code: "MLS_DEV_ROOM_MODE_UNAVAILABLE", retryable: true });
8852
8864
  else if (local && !(local.memberIds ?? []).includes(options.identity.principalId)) {
@@ -8864,7 +8876,9 @@ function createEndpoint(options) {
8864
8876
  await joinInvitedRoom(conversationId);
8865
8877
  return true;
8866
8878
  }
8867
- await mlsDevGroups.client.refresh(conversationId);
8879
+ if (!snapshot || !await mlsDevGroups.client.heldCurrent(conversationId, snapshot)) {
8880
+ await mlsDevGroups.client.refresh(conversationId);
8881
+ }
8868
8882
  await createBornGroup(conversationId);
8869
8883
  await admitDevices(conversationId).catch((error) => {
8870
8884
  if (!(error instanceof EndpointError) || ["ENDPOINT_CLOSED", "PREPARATION_INSTALLATION_FENCED"].includes(error.code)) throw error;
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  runnerLockName,
23
23
  startRelayChannel,
24
24
  writeRelayCredential
25
- } from "./chunks/chunk-HI2SVCUL.js";
25
+ } from "./chunks/chunk-JCBQCO3F.js";
26
26
  import {
27
27
  CanonApiError,
28
28
  CanonClient,
@@ -33,7 +33,7 @@ import {
33
33
  resolveCanonRuntimeConnection,
34
34
  runCli,
35
35
  verifyCanonRuntimeConnection
36
- } from "./chunks/chunk-SKFI2MJA.js";
36
+ } from "./chunks/chunk-RFNYPZXD.js";
37
37
  import {
38
38
  __name,
39
39
  isCanonClientVersion
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  runMlsDevSetup,
19
19
  runtimeDescriptor,
20
20
  startRelayChannel
21
- } from "./chunks/chunk-HI2SVCUL.js";
21
+ } from "./chunks/chunk-JCBQCO3F.js";
22
22
  import {
23
23
  CanonApiError,
24
24
  DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
@@ -43,7 +43,7 @@ import {
43
43
  toAnthropicImageBlock,
44
44
  toAnthropicImageBlocksWithinBudget,
45
45
  uploadMediaFile
46
- } from "./chunks/chunk-SKFI2MJA.js";
46
+ } from "./chunks/chunk-RFNYPZXD.js";
47
47
  import "./chunks/chunk-5MI6Q3VS.js";
48
48
  export {
49
49
  CanonApiError,
@@ -105,6 +105,14 @@ export interface MlsDevGroupClient {
105
105
  operationId?: string;
106
106
  }): Promise<EndpointOperation>;
107
107
  refresh(conversationId: string): Promise<MlsDevGroupView | undefined>;
108
+ /** Whether a room snapshot Canon just returned shows nothing this device lacks of the room's feed: no entry past its
109
+ * cursor, the membership it last fetched under, and no held removal. Then there is nothing to fetch, and this counts
110
+ * as catching up on the room. */
111
+ heldCurrent(conversationId: string, snapshot: {
112
+ mlsFeedOrdinal?: unknown;
113
+ membershipRevision?: unknown;
114
+ mlsRemoval?: unknown;
115
+ }): Promise<boolean>;
108
116
  read(conversationId: string): Promise<MlsDevGroupView | undefined>;
109
117
  readVerifiedMessageBinding(input: {
110
118
  conversationId: string;
package/dist/media.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  toAnthropicImageBlock,
19
19
  toAnthropicImageBlocksWithinBudget,
20
20
  uploadMediaFile
21
- } from "./chunks/chunk-SKFI2MJA.js";
21
+ } from "./chunks/chunk-RFNYPZXD.js";
22
22
  import "./chunks/chunk-5MI6Q3VS.js";
23
23
  export {
24
24
  DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "12.3.1",
3
+ "version": "12.3.2",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",