@canonmsg/agent-sdk 12.3.0 → 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 +7 -7
- package/dist/chunks/{chunk-HHODEJNL.js → chunk-JCBQCO3F.js} +1 -1
- package/dist/chunks/{chunk-GJO7KULY.js → chunk-RFNYPZXD.js} +74 -16
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/endpoint/mls-dev-groups.d.ts +16 -1
- package/dist/internal/endpoint/mls-dev-text.d.ts +2 -2
- package/dist/media.js +1 -1
- package/package.json +1 -1
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
|
|
196
|
-
| `relay`, `installationId` | off | Canon's class-C relay
|
|
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
|
|
212
|
+
## Encrypted rooms
|
|
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. 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
|
|
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)
|
|
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 (
|
|
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
|
|
|
@@ -6612,6 +6612,7 @@ var view2 = /* @__PURE__ */ __name((value) => Object.freeze({
|
|
|
6612
6612
|
roster: value.roster,
|
|
6613
6613
|
pendingOperationId: value.pending?.operationId ?? null
|
|
6614
6614
|
}), "view");
|
|
6615
|
+
var CATCH_UP_MAX_AGE_MS = 6e4;
|
|
6615
6616
|
function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
6616
6617
|
const principal = parseMlsDevEnrollmentPrincipal(options.principal);
|
|
6617
6618
|
const prepareProtected = options.prepareProtectedGroup ?? fail6("MLS_DEV_GROUP_UNAVAILABLE");
|
|
@@ -6626,6 +6627,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
6626
6627
|
}), "transaction") };
|
|
6627
6628
|
const fence = /* @__PURE__ */ __name(() => store.transaction(async () => {
|
|
6628
6629
|
}), "fence");
|
|
6630
|
+
const caughtUpAt = /* @__PURE__ */ new Map();
|
|
6629
6631
|
async function initialized() {
|
|
6630
6632
|
const ready = await context.ready();
|
|
6631
6633
|
await fence();
|
|
@@ -6909,7 +6911,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
6909
6911
|
if (!current.envelope?.head || current.envelope.removed || !same6(current.envelope.head, wire.expectedHead) || current.envelope.groupId !== wire.groupId) fail6("MLS_DEV_GROUP_HEAD_CHANGED", true);
|
|
6910
6912
|
if (await tx.get(appMessageKey(cid, wire.messageId)) !== void 0 && await tx.get(appMessageKey(cid, wire.messageId)) !== receipt.operationId) fail6("MLS_DEV_APPLICATION_ID_CONFLICT");
|
|
6911
6913
|
const existingMessage = await tx.get(mlsDevTextMessageKey(cid, wire.messageId));
|
|
6912
|
-
if (existingMessage?.feedOrdinal !== null && existingMessage?.feedOrdinal !== void 0) fail6("MLS_DEV_APPLICATION_ID_CONFLICT");
|
|
6914
|
+
if (existingMessage?.feedOrdinal !== null && existingMessage?.feedOrdinal !== void 0 && (existingMessage.feedOrdinal !== entry.ordinal || sender.deviceId !== current.device.registration.device.deviceId)) fail6("MLS_DEV_APPLICATION_ID_CONFLICT");
|
|
6913
6915
|
if (!current.envelope.roster.some((member) => same6(member.device, sender) && member.admissionId === wire.preconditions.admissionId)) fail6("MLS_DEV_GROUP_MEMBER_REQUIRED");
|
|
6914
6916
|
ownIntent = sender.deviceId === current.device.registration.device.deviceId ? await tx.get(appIntentKey(receipt.operationId)) : void 0;
|
|
6915
6917
|
if (ownIntent && (!same6(ownIntent.envelope, wire) || ownIntent.envelopeHash !== receipt.envelopeHash)) fail6("MLS_DEV_APPLICATION_ID_CONFLICT");
|
|
@@ -6922,7 +6924,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
6922
6924
|
let message, integrity, wrappedState = group.wrappedState;
|
|
6923
6925
|
if (ownIntent) {
|
|
6924
6926
|
const local = await store.transaction((tx) => tx.get(mlsDevTextMessageKey(cid, wire.messageId))) ?? fail6();
|
|
6925
|
-
message = local.message.kind === "text" ? parseMlsDevTextPayload(local.message) : parseMlsDevApplicationPayload(local.message);
|
|
6927
|
+
message = local.deleted ? local.message : local.message.kind === "text" ? parseMlsDevTextPayload(local.message) : parseMlsDevApplicationPayload(local.message);
|
|
6926
6928
|
} else {
|
|
6927
6929
|
const prepared = await native(init, current, state, cid, group.groupId, { op: "process_application", entry });
|
|
6928
6930
|
if (prepared.payload.kind !== "text" && prepared.payload.kind !== "item" && prepared.payload.kind !== "interaction" && prepared.payload.kind !== "invalid" || prepared.payload.messageId !== wire.messageId || prepared.payload.operationId !== receipt.operationId || prepared.payload.senderDeviceId !== sender2.deviceId || prepared.payload.feedOrdinal !== entry.ordinal) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
@@ -7030,7 +7032,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7030
7032
|
let operation;
|
|
7031
7033
|
if (prepared.own) {
|
|
7032
7034
|
const own = await tx.get(`operation/${receipt.operationId}`) ?? fail6();
|
|
7033
|
-
operation = await bridge.settle(tx, own, {
|
|
7035
|
+
if (own.status !== "succeeded") operation = await bridge.settle(tx, own, {
|
|
7034
7036
|
protocol: CANON_ENDPOINT_PROTOCOL,
|
|
7035
7037
|
operationId: own.operationId,
|
|
7036
7038
|
installationId: own.installationId,
|
|
@@ -7111,6 +7113,17 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7111
7113
|
}
|
|
7112
7114
|
__name(reduceInteraction, "reduceInteraction");
|
|
7113
7115
|
async function refreshFeed(cid, init) {
|
|
7116
|
+
const applied = { entries: false };
|
|
7117
|
+
try {
|
|
7118
|
+
const view3 = await catchUpFeed(cid, init, applied);
|
|
7119
|
+
caughtUpAt.set(cid, context.clock());
|
|
7120
|
+
return view3;
|
|
7121
|
+
} finally {
|
|
7122
|
+
if (applied.entries) bridge.historyChanged?.(cid);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
__name(refreshFeed, "refreshFeed");
|
|
7126
|
+
async function catchUpFeed(cid, init, applied) {
|
|
7114
7127
|
if (!bridge.queryFeed) fail6("MLS_DEV_GROUP_UNAVAILABLE");
|
|
7115
7128
|
for (let page = 0; page < 8; page++) {
|
|
7116
7129
|
const captured = await store.transaction(async (tx) => capture(tx, await readPreparationSnapshot(tx, scope4(cid)), init, cid));
|
|
@@ -7149,6 +7162,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7149
7162
|
if (entry.kind === "control") {
|
|
7150
7163
|
if (!await applyAccepted(init, entry.accepted, void 0, entry)) fail6("MLS_DEV_GROUP_FEED_GAP", true);
|
|
7151
7164
|
} else await applyApplication(init, entry);
|
|
7165
|
+
applied.entries = true;
|
|
7152
7166
|
}
|
|
7153
7167
|
if (!response.hasMore) {
|
|
7154
7168
|
await store.transaction(async (tx) => {
|
|
@@ -7166,7 +7180,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7166
7180
|
}
|
|
7167
7181
|
return fail6("MLS_DEV_GROUP_CATCHUP_REQUIRED", true);
|
|
7168
7182
|
}
|
|
7169
|
-
__name(
|
|
7183
|
+
__name(catchUpFeed, "catchUpFeed");
|
|
7170
7184
|
const canonItemReference = /* @__PURE__ */ __name((item, conversationId, environmentId) => isCanonUploadUrl(item.url, { environmentId, conversationId, uploadId: item.uploadId }) && (item.thumbnailUrl === void 0 || isCanonUploadUrl(item.thumbnailUrl, { environmentId, conversationId, uploadId: item.uploadId, poster: true })), "canonItemReference");
|
|
7171
7185
|
function itemInput(attachment, caption) {
|
|
7172
7186
|
const data = record5(attachment, ["kind", "url", "mimeType", "sizeBytes", "uploadId"], ["fileName", "width", "height", "durationMs", "thumbnailUrl", "processingStatus"]);
|
|
@@ -7246,7 +7260,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7246
7260
|
__name(existing, "existing");
|
|
7247
7261
|
const previous = await store.transaction(existing);
|
|
7248
7262
|
if (previous) return previous;
|
|
7249
|
-
await refresh(cid);
|
|
7263
|
+
if (!await caughtUp(cid)) await refresh(cid);
|
|
7250
7264
|
await assertNotHeld(cid);
|
|
7251
7265
|
const messageId = input.messageId ?? id(context.ids());
|
|
7252
7266
|
let captured, replyTo;
|
|
@@ -7771,6 +7785,28 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7771
7785
|
}
|
|
7772
7786
|
}
|
|
7773
7787
|
__name(refresh, "refresh");
|
|
7788
|
+
function textReady(conversationId) {
|
|
7789
|
+
return store.transaction(async (tx) => {
|
|
7790
|
+
const ready = await tx.get(`mls-dev-group/feed-ready/${encodeURIComponent(conversationId)}`);
|
|
7791
|
+
return !!ready?.admitted && !ready.terminal && ready.ordinal === await tx.get(feedKey(conversationId)) && await tx.get(recoveryFailureKey(conversationId)) === void 0;
|
|
7792
|
+
});
|
|
7793
|
+
}
|
|
7794
|
+
__name(textReady, "textReady");
|
|
7795
|
+
async function caughtUp(conversationId) {
|
|
7796
|
+
return !!bridge.streamLive?.() && context.clock() - (caughtUpAt.get(conversationId) ?? -Infinity) < CATCH_UP_MAX_AGE_MS && await textReady(conversationId);
|
|
7797
|
+
}
|
|
7798
|
+
__name(caughtUp, "caughtUp");
|
|
7799
|
+
async function settleOwnApplication(tx, operation, accepted, receipt) {
|
|
7800
|
+
if (operation.status === "succeeded" && same6(operation.receipt?.result, receipt.result)) return {};
|
|
7801
|
+
if (operation.status === "succeeded" || operation.status === "rejected") return void 0;
|
|
7802
|
+
const local = await tx.get(mlsDevTextMessageKey(accepted.conversationId, accepted.messageId));
|
|
7803
|
+
if (local?.message.kind !== "text" && local?.message.kind !== "item") return void 0;
|
|
7804
|
+
const intent2 = await tx.get(appIntentKey(operation.operationId));
|
|
7805
|
+
if (accepted.messageId !== operation.messageId || local.message.operationId !== operation.operationId || intent2?.envelopeHash !== accepted.envelopeHash || intent2.envelope.groupId !== accepted.groupId) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
7806
|
+
await putMlsDevTextMessage(tx, { ...local, status: "sent", createdAtMs: accepted.acceptedAtMs, feedOrdinal: accepted.feedOrdinal });
|
|
7807
|
+
return { operation: await bridge.settle(tx, operation, receipt, true) };
|
|
7808
|
+
}
|
|
7809
|
+
__name(settleOwnApplication, "settleOwnApplication");
|
|
7774
7810
|
async function existingOperation(tx, operationId, authored) {
|
|
7775
7811
|
const intent2 = await tx.get(`mls-dev-group/intent/${operationId}`), operation = await tx.get(`operation/${operationId}`);
|
|
7776
7812
|
if (!intent2 && !operation) return void 0;
|
|
@@ -7903,10 +7939,12 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7903
7939
|
const verified = await store.transaction(async (tx) => {
|
|
7904
7940
|
const local = await tx.get(mlsDevTextMessageKey(conversationId, messageId));
|
|
7905
7941
|
if (!local || local.feedOrdinal === null || local.message.kind !== "text" && local.message.kind !== "item" || local.deleted) return void 0;
|
|
7942
|
+
const stored = await tx.get(feedEntryKey(conversationId, local.feedOrdinal));
|
|
7943
|
+
if (stored === void 0) return void 0;
|
|
7906
7944
|
const text = local.message.kind === "item" ? parseMlsDevItemPayload(local.message) : parseMlsDevTextPayload(local.message);
|
|
7907
7945
|
const protocol2 = await tx.get(mlsDevTextProtocolKey(conversationId));
|
|
7908
7946
|
if (!isMlsDevTextProtocol(protocol2)) fail6("MLS_DEV_VERIFIED_MESSAGE_MISMATCH");
|
|
7909
|
-
const entry = parseMlsDevGroupFeedEntry(
|
|
7947
|
+
const entry = parseMlsDevGroupFeedEntry(stored);
|
|
7910
7948
|
if (entry.kind !== "application") fail6("MLS_DEV_VERIFIED_MESSAGE_MISMATCH");
|
|
7911
7949
|
const { envelope: wire2, receipt: receipt2 } = entry;
|
|
7912
7950
|
const sender2 = parseMlsDevCredentialToken(wire2.senderCredential).unsigned.claims.device;
|
|
@@ -8042,6 +8080,15 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
8042
8080
|
return run(() => serializeInboundPreparation(store, id(data.conversationId), () => submit("remove", data)));
|
|
8043
8081
|
}, "remove"),
|
|
8044
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"),
|
|
8045
8092
|
read: /* @__PURE__ */ __name((value) => run(() => read(id(value)), false), "read"),
|
|
8046
8093
|
readVerifiedMessageBinding: /* @__PURE__ */ __name((input) => {
|
|
8047
8094
|
const data = record5(input, ["conversationId", "messageId"]);
|
|
@@ -8105,9 +8152,12 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
8105
8152
|
const input = textInput(value), selectedId = id(operationId), selectedGuard = guardId === void 0 ? void 0 : id(guardId);
|
|
8106
8153
|
return run(() => serializeInboundPreparation(store, input.conversationId, () => sendText(input, selectedId, selectedGuard)));
|
|
8107
8154
|
},
|
|
8155
|
+
/** A room this device holds caught up is read as it holds it: what fetches the room's new entries says so
|
|
8156
|
+
* (bridge.historyChanged). Otherwise a read fetches the feed first, which the polling fallback and the connected
|
|
8157
|
+
* stream's minute-long safety poll rely on. */
|
|
8108
8158
|
queryTextHistory(conversationId, name, parameters) {
|
|
8109
8159
|
return run(() => serializeInboundPreparation(store, id(conversationId), async () => {
|
|
8110
|
-
await refresh(conversationId);
|
|
8160
|
+
if (!await caughtUp(conversationId)) await refresh(conversationId);
|
|
8111
8161
|
const init = await initialized();
|
|
8112
8162
|
await primeIdentities(conversationId, init);
|
|
8113
8163
|
return store.transaction(async (tx) => {
|
|
@@ -8131,12 +8181,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
8131
8181
|
});
|
|
8132
8182
|
}));
|
|
8133
8183
|
},
|
|
8134
|
-
|
|
8135
|
-
return store.transaction(async (tx) => {
|
|
8136
|
-
const ready = await tx.get(`mls-dev-group/feed-ready/${encodeURIComponent(conversationId)}`);
|
|
8137
|
-
return !!ready?.admitted && !ready.terminal && ready.ordinal === await tx.get(feedKey(conversationId)) && await tx.get(recoveryFailureKey(conversationId)) === void 0;
|
|
8138
|
-
});
|
|
8139
|
-
},
|
|
8184
|
+
isTextReady: textReady,
|
|
8140
8185
|
async readAcceptedReceipt(operation) {
|
|
8141
8186
|
if (!["mls_group_control", "mls_application"].includes(operation.action)) return void 0;
|
|
8142
8187
|
return store.transaction(async (tx) => {
|
|
@@ -8152,13 +8197,18 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
8152
8197
|
const cid = operation.conversationId;
|
|
8153
8198
|
const accepted2 = operation.action === "mls_application" ? parseMlsDevApplicationReceipt(receipt.result) : parseMlsDevGroupAcceptedControl(receipt.result);
|
|
8154
8199
|
if (accepted2.operationId !== operation.operationId || ("control" in accepted2 ? accepted2.control.conversationId : accepted2.conversationId) !== cid) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
8155
|
-
await store.transaction(async (tx) => {
|
|
8200
|
+
const own = await store.transaction(async (tx) => {
|
|
8156
8201
|
const current = await tx.get(`operation/${operation.operationId}`) ?? fail6();
|
|
8157
8202
|
if (receipt.installationId !== current.installationId || receipt.action !== current.action) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
8158
8203
|
const previous = await tx.get(acceptedReceiptKey(operation.operationId));
|
|
8159
8204
|
if (previous && !same6(previous, receipt)) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
8160
8205
|
if (!previous) await tx.put(acceptedReceiptKey(operation.operationId), receipt);
|
|
8206
|
+
return "control" in accepted2 ? void 0 : settleOwnApplication(tx, current, accepted2, receipt);
|
|
8161
8207
|
});
|
|
8208
|
+
if (own) {
|
|
8209
|
+
if (own.operation) await bridge.afterReceipt(own.operation);
|
|
8210
|
+
return true;
|
|
8211
|
+
}
|
|
8162
8212
|
await run(() => serializeInboundPreparation(store, cid, async () => {
|
|
8163
8213
|
try {
|
|
8164
8214
|
await refresh(cid);
|
|
@@ -8385,7 +8435,10 @@ function createEndpoint(options) {
|
|
|
8385
8435
|
clearTimeout(admissionTimers.get(conversationId));
|
|
8386
8436
|
admissionTimers.delete(conversationId);
|
|
8387
8437
|
scheduleAdmission(conversationId, 0);
|
|
8388
|
-
}, "controlSettled")
|
|
8438
|
+
}, "controlSettled"),
|
|
8439
|
+
// Whatever fetched them (a hint, the reconnect sweep, a send, recovery), the room's local reads show them now.
|
|
8440
|
+
historyChanged: /* @__PURE__ */ __name((conversationId) => queryObserver.invalidate((query, parameters) => parameters.conversationId === conversationId && ["list_messages", "get_messages", "get_runtime_card_state"].includes(query)), "historyChanged"),
|
|
8441
|
+
streamLive: /* @__PURE__ */ __name(() => events.isConnected(), "streamLive")
|
|
8389
8442
|
}) : void 0;
|
|
8390
8443
|
async function inMlsEnvironment() {
|
|
8391
8444
|
return isMlsEnvironmentId((await initialization).environmentId);
|
|
@@ -8801,8 +8854,11 @@ function createEndpoint(options) {
|
|
|
8801
8854
|
const local = await store.transaction((tx) => tx.get(`conversation/${conversationId}`));
|
|
8802
8855
|
const knownProtected = await store.transaction((tx) => isMlsDevRoomProtected(tx, conversationId));
|
|
8803
8856
|
await initialization;
|
|
8857
|
+
let snapshot = null;
|
|
8804
8858
|
if (discover && (mlsDevGroups || !local && await inMlsEnvironment())) {
|
|
8859
|
+
const fresh = !reads.has(canonicalEndpointJson({ name: "conversation", parameters: { conversationId } }));
|
|
8805
8860
|
const room = await endpoint.query("conversation", { conversationId });
|
|
8861
|
+
if (fresh) snapshot = room;
|
|
8806
8862
|
if (room) await endpoint.applyConversationSnapshot(room);
|
|
8807
8863
|
else if (!local && !knownProtected) throw new EndpointError("Conversation mode is unavailable", { code: "MLS_DEV_ROOM_MODE_UNAVAILABLE", retryable: true });
|
|
8808
8864
|
else if (local && !(local.memberIds ?? []).includes(options.identity.principalId)) {
|
|
@@ -8820,7 +8876,9 @@ function createEndpoint(options) {
|
|
|
8820
8876
|
await joinInvitedRoom(conversationId);
|
|
8821
8877
|
return true;
|
|
8822
8878
|
}
|
|
8823
|
-
await mlsDevGroups.client.
|
|
8879
|
+
if (!snapshot || !await mlsDevGroups.client.heldCurrent(conversationId, snapshot)) {
|
|
8880
|
+
await mlsDevGroups.client.refresh(conversationId);
|
|
8881
|
+
}
|
|
8824
8882
|
await createBornGroup(conversationId);
|
|
8825
8883
|
await admitDevices(conversationId).catch((error) => {
|
|
8826
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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;
|
|
@@ -149,6 +157,10 @@ export interface MlsDevGroupBridge {
|
|
|
149
157
|
/** A control of this device's settled, accepted or lost to another device's: what waited on it can go on now. A hint
|
|
150
158
|
* announcing the room's change may have come while it was pending, and none may follow (D2 holds the room's sends). */
|
|
151
159
|
controlSettled?(conversationId: string): void;
|
|
160
|
+
/** A fetch applied entries of the room's feed: its encrypted history, read locally, has changed. */
|
|
161
|
+
historyChanged?(conversationId: string): void;
|
|
162
|
+
/** This device's event stream is connected, so each room's hints announce what Canon accepts into its feed. */
|
|
163
|
+
streamLive?(): boolean;
|
|
152
164
|
/** Reads and applies the room's conversation snapshot from Canon, for openings of members admitted since. */
|
|
153
165
|
readRoom?(conversationId: string): Promise<unknown>;
|
|
154
166
|
}
|
|
@@ -161,13 +173,16 @@ export declare function createMlsDevGroups(options: MlsDevDeviceClientOptions, c
|
|
|
161
173
|
* encrypted room (mls-payload-v3.md §4). Items and cards are not forwarded yet. */
|
|
162
174
|
forwardText(value: unknown, operationId: string, guardId?: string): Promise<EndpointOperation>;
|
|
163
175
|
sendText(value: unknown, operationId: string, guardId?: string): Promise<EndpointOperation>;
|
|
176
|
+
/** A room this device holds caught up is read as it holds it: what fetches the room's new entries says so
|
|
177
|
+
* (bridge.historyChanged). Otherwise a read fetches the feed first, which the polling fallback and the connected
|
|
178
|
+
* stream's minute-long safety poll rely on. */
|
|
164
179
|
queryTextHistory(conversationId: string, name: string, parameters: Record<string, unknown>): Promise<{
|
|
165
180
|
messages: Record<string, unknown>[];
|
|
166
181
|
nextBefore?: string | null;
|
|
167
182
|
missingMessageIds?: string[];
|
|
168
183
|
}>;
|
|
169
184
|
syncText(conversationId: string, options?: EndpointSyncOptions): Promise<EndpointSyncPage>;
|
|
170
|
-
isTextReady(conversationId: string)
|
|
185
|
+
isTextReady: (conversationId: string) => Promise<boolean>;
|
|
171
186
|
readAcceptedReceipt(operation: EndpointOperation): Promise<EndpointOperationReceipt | undefined>;
|
|
172
187
|
settleReceipt(operation: EndpointOperation, receipt: EndpointOperationReceipt): Promise<boolean>;
|
|
173
188
|
};
|
|
@@ -29,8 +29,8 @@ export declare const mlsDevTextDeletedKey: (conversationId: string, messageId: s
|
|
|
29
29
|
export declare const mlsDevTextReactionsKey: (conversationId: string, messageId: string) => string;
|
|
30
30
|
/**
|
|
31
31
|
* Its sender deleted a message for everyone. The decrypted text is erased now, and the tombstone keeps it erased if the
|
|
32
|
-
* feed brings the message again, on catch-up or replay. An own send
|
|
33
|
-
*
|
|
32
|
+
* feed brings the message again, on catch-up or replay. An own send Canon has not accepted yet keeps its text until its
|
|
33
|
+
* receipt or echo settles it; the history already shows it deleted. Cards are not deleted this
|
|
34
34
|
* way: Canon refuses to delete an encrypted card, which is withdrawn instead. Returns whether anything changed.
|
|
35
35
|
*/
|
|
36
36
|
export declare function deleteMlsDevTextMessage(tx: EndpointStoreTransaction, conversationId: string, messageId: string): Promise<boolean>;
|
package/dist/media.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
toAnthropicImageBlock,
|
|
19
19
|
toAnthropicImageBlocksWithinBudget,
|
|
20
20
|
uploadMediaFile
|
|
21
|
-
} from "./chunks/chunk-
|
|
21
|
+
} from "./chunks/chunk-RFNYPZXD.js";
|
|
22
22
|
import "./chunks/chunk-5MI6Q3VS.js";
|
|
23
23
|
export {
|
|
24
24
|
DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
|