@canonmsg/agent-sdk 12.2.2 → 12.3.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 +1 -1
- package/dist/chunks/{chunk-J63YVAKA.js → chunk-5MI6Q3VS.js} +2 -0
- package/dist/chunks/{chunk-LFTYXZUY.js → chunk-HI2SVCUL.js} +6 -2
- package/dist/chunks/{chunk-FCG2Z57F.js → chunk-SKFI2MJA.js} +81 -20
- package/dist/chunks/{mls-crypto-ITVGUEP7.js → mls-crypto-D5YAGYJ3.js} +1 -1
- package/dist/cli.js +9 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -3
- package/dist/internal/backend-contracts/agentWake.d.ts +11 -0
- package/dist/internal/backend-contracts/endpointContentPolicy.d.ts +1 -1
- package/dist/internal/backend-contracts/index.d.ts +1 -0
- package/dist/internal/core/client.d.ts +6 -0
- package/dist/internal/endpoint/mls-dev-groups.d.ts +8 -1
- package/dist/internal/endpoint/mls-dev-text.d.ts +2 -2
- package/dist/media.js +2 -2
- package/dist/runner/runner.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -232,7 +232,7 @@ In an encrypted room:
|
|
|
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
|
-
Without `--mls-dev-config` (or `mls`), the runner refuses each encrypted room, logs it once, and runs nothing for it.
|
|
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
237
|
### The class-C relay (Dev, Canon-operated)
|
|
238
238
|
|
|
@@ -4159,6 +4159,7 @@ var ENDPOINT_CONTENT_FUNCTION_QUERIES = [
|
|
|
4159
4159
|
];
|
|
4160
4160
|
var ENDPOINT_CONTENT_QUERIES = [...ENDPOINT_CONTENT_FUNCTION_QUERIES, "admit_messages"];
|
|
4161
4161
|
var ENDPOINT_CONTENT_EPHEMERALS = [
|
|
4162
|
+
"prewarm_agents",
|
|
4162
4163
|
"create_memo_stream",
|
|
4163
4164
|
"publish_presence",
|
|
4164
4165
|
"publish_typing",
|
|
@@ -4364,6 +4365,7 @@ var turnPresence = {
|
|
|
4364
4365
|
var transientRouting = {
|
|
4365
4366
|
create_memo_stream: ["conversationId", "streamId"],
|
|
4366
4367
|
append_memo_chunk: ["streamId"],
|
|
4368
|
+
prewarm_agents: ["conversationId"],
|
|
4367
4369
|
publish_presence: [],
|
|
4368
4370
|
publish_typing: ["conversationId"],
|
|
4369
4371
|
publish_streaming: ["conversationId"],
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
sendMessageWithRetryChunked,
|
|
30
30
|
splitTextByUtf8Bytes,
|
|
31
31
|
verifyCanonRuntimeConnection
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-SKFI2MJA.js";
|
|
33
33
|
import {
|
|
34
34
|
CONNECTOR_RELAY_ANSWER_WITHIN_MS,
|
|
35
35
|
CONNECTOR_RELAY_INSTALLATION_PATTERN,
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
parseMlsDevGroupHead,
|
|
59
59
|
parseMlsDevGroupRoster,
|
|
60
60
|
resolveAllowedUploadMimeType
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-5MI6Q3VS.js";
|
|
62
62
|
|
|
63
63
|
// src/runner/types.ts
|
|
64
64
|
var DriverSessionError = class extends Error {
|
|
@@ -4701,7 +4701,11 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4701
4701
|
}
|
|
4702
4702
|
agentId = identity.principalId;
|
|
4703
4703
|
ownershipVersion = identity.ownershipVersion;
|
|
4704
|
+
if (options.wake !== void 0) await client.configureWake(options.wake);
|
|
4704
4705
|
mlsOn = !!(endpoint.mlsDevDevice && endpoint.mlsDevGroups);
|
|
4706
|
+
if (!mlsOn && (await endpoint.capabilities().catch(() => null))?.mls?.roomMode === "default") {
|
|
4707
|
+
throw new Error("Canon encrypts every new conversation here, and this runner has no MLS device: run it with --mls-dev-config <path> (set one up with canon-agent mls).");
|
|
4708
|
+
}
|
|
4705
4709
|
if (mlsOn) await endpoint.mlsDevDevice.ensureReady().catch(warn("MLS device not ready; encrypted rooms wait for it"));
|
|
4706
4710
|
const keepPool = /* @__PURE__ */ __name(() => endpoint.mlsDevDevice.read().then(async (view) => {
|
|
4707
4711
|
if (view.registration && view.status !== "revoked") {
|
|
@@ -109,7 +109,7 @@ import {
|
|
|
109
109
|
readMembershipChangeMetadata,
|
|
110
110
|
resolveAllowedUploadMimeType,
|
|
111
111
|
sanitizeDownloadFileName
|
|
112
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-5MI6Q3VS.js";
|
|
113
113
|
|
|
114
114
|
// src/media.ts
|
|
115
115
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
@@ -194,6 +194,7 @@ function createEndpointEventHub(input) {
|
|
|
194
194
|
let attempt = 0;
|
|
195
195
|
while (!signal.aborted && observers.size) {
|
|
196
196
|
let retryAfterMs = 0;
|
|
197
|
+
let handling = false;
|
|
197
198
|
try {
|
|
198
199
|
if (!input.transport.events) return;
|
|
199
200
|
for await (const event of input.transport.events({ lastEventId, signal, families: families(), principalScopes: principalScopes(), onConnected: /* @__PURE__ */ __name(() => {
|
|
@@ -204,8 +205,10 @@ function createEndpointEventHub(input) {
|
|
|
204
205
|
if (signal.aborted) break;
|
|
205
206
|
validateEndpointContentBoundary("event", event.kind, event.data);
|
|
206
207
|
if (event.kind === "error") throw decodeEndpointError(event.data, { stream: true });
|
|
207
|
-
|
|
208
|
+
handling = true;
|
|
208
209
|
await input.onEvent?.(event);
|
|
210
|
+
handling = false;
|
|
211
|
+
attempt = 0;
|
|
209
212
|
if (event.kind === "replay.expired") lastEventId = void 0;
|
|
210
213
|
else if (event.id) lastEventId = event.id;
|
|
211
214
|
for (const observer of observers) safe(() => observer.listener(event));
|
|
@@ -213,7 +216,7 @@ function createEndpointEventHub(input) {
|
|
|
213
216
|
}
|
|
214
217
|
} catch (error) {
|
|
215
218
|
if (!signal.aborted) {
|
|
216
|
-
terminalError = error instanceof EndpointError && !error.retryable;
|
|
219
|
+
terminalError = !handling && error instanceof EndpointError && !error.retryable;
|
|
217
220
|
if (error instanceof EndpointError && typeof error.retryAfterMs === "number" && Number.isFinite(error.retryAfterMs)) retryAfterMs = Math.max(0, Math.min(3e5, error.retryAfterMs));
|
|
218
221
|
for (const { hooks } of observers) safe(() => hooks?.onError?.(error instanceof Error ? error : new EndpointError("Stream connection failed")));
|
|
219
222
|
}
|
|
@@ -6609,6 +6612,7 @@ var view2 = /* @__PURE__ */ __name((value) => Object.freeze({
|
|
|
6609
6612
|
roster: value.roster,
|
|
6610
6613
|
pendingOperationId: value.pending?.operationId ?? null
|
|
6611
6614
|
}), "view");
|
|
6615
|
+
var CATCH_UP_MAX_AGE_MS = 6e4;
|
|
6612
6616
|
function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
6613
6617
|
const principal = parseMlsDevEnrollmentPrincipal(options.principal);
|
|
6614
6618
|
const prepareProtected = options.prepareProtectedGroup ?? fail6("MLS_DEV_GROUP_UNAVAILABLE");
|
|
@@ -6623,6 +6627,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
6623
6627
|
}), "transaction") };
|
|
6624
6628
|
const fence = /* @__PURE__ */ __name(() => store.transaction(async () => {
|
|
6625
6629
|
}), "fence");
|
|
6630
|
+
const caughtUpAt = /* @__PURE__ */ new Map();
|
|
6626
6631
|
async function initialized() {
|
|
6627
6632
|
const ready = await context.ready();
|
|
6628
6633
|
await fence();
|
|
@@ -6906,7 +6911,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
6906
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);
|
|
6907
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");
|
|
6908
6913
|
const existingMessage = await tx.get(mlsDevTextMessageKey(cid, wire.messageId));
|
|
6909
|
-
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");
|
|
6910
6915
|
if (!current.envelope.roster.some((member) => same6(member.device, sender) && member.admissionId === wire.preconditions.admissionId)) fail6("MLS_DEV_GROUP_MEMBER_REQUIRED");
|
|
6911
6916
|
ownIntent = sender.deviceId === current.device.registration.device.deviceId ? await tx.get(appIntentKey(receipt.operationId)) : void 0;
|
|
6912
6917
|
if (ownIntent && (!same6(ownIntent.envelope, wire) || ownIntent.envelopeHash !== receipt.envelopeHash)) fail6("MLS_DEV_APPLICATION_ID_CONFLICT");
|
|
@@ -6919,7 +6924,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
6919
6924
|
let message, integrity, wrappedState = group.wrappedState;
|
|
6920
6925
|
if (ownIntent) {
|
|
6921
6926
|
const local = await store.transaction((tx) => tx.get(mlsDevTextMessageKey(cid, wire.messageId))) ?? fail6();
|
|
6922
|
-
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);
|
|
6923
6928
|
} else {
|
|
6924
6929
|
const prepared = await native(init, current, state, cid, group.groupId, { op: "process_application", entry });
|
|
6925
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");
|
|
@@ -7027,7 +7032,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7027
7032
|
let operation;
|
|
7028
7033
|
if (prepared.own) {
|
|
7029
7034
|
const own = await tx.get(`operation/${receipt.operationId}`) ?? fail6();
|
|
7030
|
-
operation = await bridge.settle(tx, own, {
|
|
7035
|
+
if (own.status !== "succeeded") operation = await bridge.settle(tx, own, {
|
|
7031
7036
|
protocol: CANON_ENDPOINT_PROTOCOL,
|
|
7032
7037
|
operationId: own.operationId,
|
|
7033
7038
|
installationId: own.installationId,
|
|
@@ -7108,6 +7113,17 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7108
7113
|
}
|
|
7109
7114
|
__name(reduceInteraction, "reduceInteraction");
|
|
7110
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) {
|
|
7111
7127
|
if (!bridge.queryFeed) fail6("MLS_DEV_GROUP_UNAVAILABLE");
|
|
7112
7128
|
for (let page = 0; page < 8; page++) {
|
|
7113
7129
|
const captured = await store.transaction(async (tx) => capture(tx, await readPreparationSnapshot(tx, scope4(cid)), init, cid));
|
|
@@ -7146,6 +7162,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7146
7162
|
if (entry.kind === "control") {
|
|
7147
7163
|
if (!await applyAccepted(init, entry.accepted, void 0, entry)) fail6("MLS_DEV_GROUP_FEED_GAP", true);
|
|
7148
7164
|
} else await applyApplication(init, entry);
|
|
7165
|
+
applied.entries = true;
|
|
7149
7166
|
}
|
|
7150
7167
|
if (!response.hasMore) {
|
|
7151
7168
|
await store.transaction(async (tx) => {
|
|
@@ -7163,7 +7180,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7163
7180
|
}
|
|
7164
7181
|
return fail6("MLS_DEV_GROUP_CATCHUP_REQUIRED", true);
|
|
7165
7182
|
}
|
|
7166
|
-
__name(
|
|
7183
|
+
__name(catchUpFeed, "catchUpFeed");
|
|
7167
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");
|
|
7168
7185
|
function itemInput(attachment, caption) {
|
|
7169
7186
|
const data = record5(attachment, ["kind", "url", "mimeType", "sizeBytes", "uploadId"], ["fileName", "width", "height", "durationMs", "thumbnailUrl", "processingStatus"]);
|
|
@@ -7243,7 +7260,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7243
7260
|
__name(existing, "existing");
|
|
7244
7261
|
const previous = await store.transaction(existing);
|
|
7245
7262
|
if (previous) return previous;
|
|
7246
|
-
await refresh(cid);
|
|
7263
|
+
if (!await caughtUp(cid)) await refresh(cid);
|
|
7247
7264
|
await assertNotHeld(cid);
|
|
7248
7265
|
const messageId = input.messageId ?? id(context.ids());
|
|
7249
7266
|
let captured, replyTo;
|
|
@@ -7768,6 +7785,28 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7768
7785
|
}
|
|
7769
7786
|
}
|
|
7770
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");
|
|
7771
7810
|
async function existingOperation(tx, operationId, authored) {
|
|
7772
7811
|
const intent2 = await tx.get(`mls-dev-group/intent/${operationId}`), operation = await tx.get(`operation/${operationId}`);
|
|
7773
7812
|
if (!intent2 && !operation) return void 0;
|
|
@@ -7900,10 +7939,12 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
7900
7939
|
const verified = await store.transaction(async (tx) => {
|
|
7901
7940
|
const local = await tx.get(mlsDevTextMessageKey(conversationId, messageId));
|
|
7902
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;
|
|
7903
7944
|
const text = local.message.kind === "item" ? parseMlsDevItemPayload(local.message) : parseMlsDevTextPayload(local.message);
|
|
7904
7945
|
const protocol2 = await tx.get(mlsDevTextProtocolKey(conversationId));
|
|
7905
7946
|
if (!isMlsDevTextProtocol(protocol2)) fail6("MLS_DEV_VERIFIED_MESSAGE_MISMATCH");
|
|
7906
|
-
const entry = parseMlsDevGroupFeedEntry(
|
|
7947
|
+
const entry = parseMlsDevGroupFeedEntry(stored);
|
|
7907
7948
|
if (entry.kind !== "application") fail6("MLS_DEV_VERIFIED_MESSAGE_MISMATCH");
|
|
7908
7949
|
const { envelope: wire2, receipt: receipt2 } = entry;
|
|
7909
7950
|
const sender2 = parseMlsDevCredentialToken(wire2.senderCredential).unsigned.claims.device;
|
|
@@ -8102,9 +8143,12 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
8102
8143
|
const input = textInput(value), selectedId = id(operationId), selectedGuard = guardId === void 0 ? void 0 : id(guardId);
|
|
8103
8144
|
return run(() => serializeInboundPreparation(store, input.conversationId, () => sendText(input, selectedId, selectedGuard)));
|
|
8104
8145
|
},
|
|
8146
|
+
/** A room this device holds caught up is read as it holds it: what fetches the room's new entries says so
|
|
8147
|
+
* (bridge.historyChanged). Otherwise a read fetches the feed first, which the polling fallback and the connected
|
|
8148
|
+
* stream's minute-long safety poll rely on. */
|
|
8105
8149
|
queryTextHistory(conversationId, name, parameters) {
|
|
8106
8150
|
return run(() => serializeInboundPreparation(store, id(conversationId), async () => {
|
|
8107
|
-
await refresh(conversationId);
|
|
8151
|
+
if (!await caughtUp(conversationId)) await refresh(conversationId);
|
|
8108
8152
|
const init = await initialized();
|
|
8109
8153
|
await primeIdentities(conversationId, init);
|
|
8110
8154
|
return store.transaction(async (tx) => {
|
|
@@ -8128,12 +8172,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
8128
8172
|
});
|
|
8129
8173
|
}));
|
|
8130
8174
|
},
|
|
8131
|
-
|
|
8132
|
-
return store.transaction(async (tx) => {
|
|
8133
|
-
const ready = await tx.get(`mls-dev-group/feed-ready/${encodeURIComponent(conversationId)}`);
|
|
8134
|
-
return !!ready?.admitted && !ready.terminal && ready.ordinal === await tx.get(feedKey(conversationId)) && await tx.get(recoveryFailureKey(conversationId)) === void 0;
|
|
8135
|
-
});
|
|
8136
|
-
},
|
|
8175
|
+
isTextReady: textReady,
|
|
8137
8176
|
async readAcceptedReceipt(operation) {
|
|
8138
8177
|
if (!["mls_group_control", "mls_application"].includes(operation.action)) return void 0;
|
|
8139
8178
|
return store.transaction(async (tx) => {
|
|
@@ -8149,13 +8188,18 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
|
|
|
8149
8188
|
const cid = operation.conversationId;
|
|
8150
8189
|
const accepted2 = operation.action === "mls_application" ? parseMlsDevApplicationReceipt(receipt.result) : parseMlsDevGroupAcceptedControl(receipt.result);
|
|
8151
8190
|
if (accepted2.operationId !== operation.operationId || ("control" in accepted2 ? accepted2.control.conversationId : accepted2.conversationId) !== cid) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
8152
|
-
await store.transaction(async (tx) => {
|
|
8191
|
+
const own = await store.transaction(async (tx) => {
|
|
8153
8192
|
const current = await tx.get(`operation/${operation.operationId}`) ?? fail6();
|
|
8154
8193
|
if (receipt.installationId !== current.installationId || receipt.action !== current.action) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
8155
8194
|
const previous = await tx.get(acceptedReceiptKey(operation.operationId));
|
|
8156
8195
|
if (previous && !same6(previous, receipt)) fail6("MLS_DEV_GROUP_BINDING_MISMATCH");
|
|
8157
8196
|
if (!previous) await tx.put(acceptedReceiptKey(operation.operationId), receipt);
|
|
8197
|
+
return "control" in accepted2 ? void 0 : settleOwnApplication(tx, current, accepted2, receipt);
|
|
8158
8198
|
});
|
|
8199
|
+
if (own) {
|
|
8200
|
+
if (own.operation) await bridge.afterReceipt(own.operation);
|
|
8201
|
+
return true;
|
|
8202
|
+
}
|
|
8159
8203
|
await run(() => serializeInboundPreparation(store, cid, async () => {
|
|
8160
8204
|
try {
|
|
8161
8205
|
await refresh(cid);
|
|
@@ -8382,7 +8426,10 @@ function createEndpoint(options) {
|
|
|
8382
8426
|
clearTimeout(admissionTimers.get(conversationId));
|
|
8383
8427
|
admissionTimers.delete(conversationId);
|
|
8384
8428
|
scheduleAdmission(conversationId, 0);
|
|
8385
|
-
}, "controlSettled")
|
|
8429
|
+
}, "controlSettled"),
|
|
8430
|
+
// Whatever fetched them (a hint, the reconnect sweep, a send, recovery), the room's local reads show them now.
|
|
8431
|
+
historyChanged: /* @__PURE__ */ __name((conversationId) => queryObserver.invalidate((query, parameters) => parameters.conversationId === conversationId && ["list_messages", "get_messages", "get_runtime_card_state"].includes(query)), "historyChanged"),
|
|
8432
|
+
streamLive: /* @__PURE__ */ __name(() => events.isConnected(), "streamLive")
|
|
8386
8433
|
}) : void 0;
|
|
8387
8434
|
async function inMlsEnvironment() {
|
|
8388
8435
|
return isMlsEnvironmentId((await initialization).environmentId);
|
|
@@ -8834,7 +8881,8 @@ function createEndpoint(options) {
|
|
|
8834
8881
|
await refreshMlsRoom(conversationId, true);
|
|
8835
8882
|
} catch (error) {
|
|
8836
8883
|
if (isMlsDevGroupRoomRecoveryFailure(error)) return;
|
|
8837
|
-
throw error;
|
|
8884
|
+
if (error instanceof EndpointError && ["ENDPOINT_CLOSED", "PREPARATION_INSTALLATION_FENCED"].includes(error.code)) throw error;
|
|
8885
|
+
if (!(error instanceof EndpointError) || error.retryable) scheduleAdmission(conversationId, admissionRetry.initial, true);
|
|
8838
8886
|
}
|
|
8839
8887
|
}
|
|
8840
8888
|
__name(refreshMlsRoomFromHint, "refreshMlsRoomFromHint");
|
|
@@ -9047,6 +9095,8 @@ function createEndpoint(options) {
|
|
|
9047
9095
|
queryObserver.invalidate((query, parameters) => queryAffectedByOperation(updated, query, parameters));
|
|
9048
9096
|
if (terminal(updated.status)) scheduleRetry(0);
|
|
9049
9097
|
if (!terminal(updated.status) && updated.status !== "uncertain") scheduleRetry(Math.max(1e3, (updated.nextAttemptAt ?? clock()) - clock()));
|
|
9098
|
+
const made = updated.status === "succeeded" && ["start_conversation", "create_conversation"].includes(updated.action) ? updated.receipt?.result?.conversationId : void 0;
|
|
9099
|
+
if (mlsDevGroups && typeof made === "string" && ENDPOINT_ID_PATTERN.test(made)) void refreshMlsRoom(made, true).catch(() => void 0);
|
|
9050
9100
|
}
|
|
9051
9101
|
__name(afterReceipt, "afterReceipt");
|
|
9052
9102
|
async function persistReceipt(operation, receipt) {
|
|
@@ -10032,7 +10082,7 @@ async function acquireAgentEndpoint(input) {
|
|
|
10032
10082
|
}
|
|
10033
10083
|
const requestedMls = input.options.mlsDev;
|
|
10034
10084
|
const mls = requestedMls ? await (async () => {
|
|
10035
|
-
const crypto2 = await import("./mls-crypto-
|
|
10085
|
+
const crypto2 = await import("./mls-crypto-D5YAGYJ3.js");
|
|
10036
10086
|
const configuration = crypto2.parseMlsDevClientConfiguration(requestedMls.configuration);
|
|
10037
10087
|
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");
|
|
10038
10088
|
const custody = requestedMls.custody;
|
|
@@ -10326,6 +10376,17 @@ var CanonClient = class {
|
|
|
10326
10376
|
if (!res.ok) throw new CanonApiError(res.status, await res.text());
|
|
10327
10377
|
return res.json();
|
|
10328
10378
|
}
|
|
10379
|
+
/** Register (or explicitly remove) this installed agent's signed readiness callback. */
|
|
10380
|
+
async configureWake(configuration) {
|
|
10381
|
+
const response = await this.http(`${this.baseUrl}/agents/me/wake`, {
|
|
10382
|
+
method: configuration ? "POST" : "DELETE",
|
|
10383
|
+
headers: this.authHeaders(),
|
|
10384
|
+
signal: AbortSignal.timeout(15e3),
|
|
10385
|
+
...configuration ? { body: JSON.stringify(configuration) } : {}
|
|
10386
|
+
});
|
|
10387
|
+
if (!response.ok) throw new CanonApiError(response.status, "Wake callback registration failed");
|
|
10388
|
+
return response.json();
|
|
10389
|
+
}
|
|
10329
10390
|
async discoverAgents(input = {}) {
|
|
10330
10391
|
const result = await this.endpointQuery("discover_agents", { ...input });
|
|
10331
10392
|
if (!isDiscoverAgentsResult(result)) {
|
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-HI2SVCUL.js";
|
|
26
26
|
import {
|
|
27
27
|
CanonApiError,
|
|
28
28
|
CanonClient,
|
|
@@ -33,11 +33,11 @@ import {
|
|
|
33
33
|
resolveCanonRuntimeConnection,
|
|
34
34
|
runCli,
|
|
35
35
|
verifyCanonRuntimeConnection
|
|
36
|
-
} from "./chunks/chunk-
|
|
36
|
+
} from "./chunks/chunk-SKFI2MJA.js";
|
|
37
37
|
import {
|
|
38
38
|
__name,
|
|
39
39
|
isCanonClientVersion
|
|
40
|
-
} from "./chunks/chunk-
|
|
40
|
+
} from "./chunks/chunk-5MI6Q3VS.js";
|
|
41
41
|
|
|
42
42
|
// src/cli.ts
|
|
43
43
|
import { realpathSync } from "node:fs";
|
|
@@ -80,9 +80,10 @@ Usage:
|
|
|
80
80
|
canon-agent errands [--follow] [--cancel <errand id>] (--agent <id> | --profile <name>)
|
|
81
81
|
[--environment <id>]
|
|
82
82
|
|
|
83
|
-
Every run command also takes --mls-dev-config <path
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
Every run command also takes --mls-dev-config <path>, which gives the agent its MLS
|
|
84
|
+
device so it can take part in encrypted rooms. Without it, the agent refuses encrypted
|
|
85
|
+
rooms and logs each one once; where Canon encrypts every new conversation, it does not
|
|
86
|
+
start.
|
|
86
87
|
|
|
87
88
|
Team rooms (an owner's agents working together): claude and codex also take
|
|
88
89
|
[--instructions <file>] [--trust-agent <agentId>]...; every run command takes
|
|
@@ -155,8 +156,8 @@ Flags:
|
|
|
155
156
|
which are then not end-to-end encrypted to that agent. With
|
|
156
157
|
"mls", it sets up the relay's device with that credential.
|
|
157
158
|
--mls-dev-config <p> The operator's MLS configuration: an absolute path to a private
|
|
158
|
-
JSON file with the
|
|
159
|
-
keys or credentials).
|
|
159
|
+
JSON file with the environment's issuer pins and the custody
|
|
160
|
+
helper (no keys or credentials).
|
|
160
161
|
--action <action> mls: status (public device state), provision (create the
|
|
161
162
|
portable vault, once), enroll, renew, publish (this device's
|
|
162
163
|
KeyPackage for --room) or refresh (catch up --room). It never
|
package/dist/index.d.ts
CHANGED
|
@@ -20,3 +20,4 @@ export { CanonApiError, clearPendingRegistration, ensureAgentProfile, resumeRegi
|
|
|
20
20
|
export type { CommunicateInput, CommunicateResult, EnsureAgentProfileOptions, MediaAttachment, RegistrationCredentials, RegistrationProgress, RegistrationSession, RegistrationStatus, RegistrationStore, ResumableMediaUploadResult, ResumeRegistrationOptions, VideoProcessingErrorCode, VideoProcessingStatus, } from './internal/core/index.js';
|
|
21
21
|
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, conversationMediaCacheDir, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
|
22
22
|
export type { AnthropicImageBlock, AnthropicImageBudgetOptions, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
23
|
+
export type { AgentWakeConfiguration } from './internal/backend-contracts/index.js';
|
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-HI2SVCUL.js";
|
|
22
22
|
import {
|
|
23
23
|
CanonApiError,
|
|
24
24
|
DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
|
|
@@ -43,8 +43,8 @@ import {
|
|
|
43
43
|
toAnthropicImageBlock,
|
|
44
44
|
toAnthropicImageBlocksWithinBudget,
|
|
45
45
|
uploadMediaFile
|
|
46
|
-
} from "./chunks/chunk-
|
|
47
|
-
import "./chunks/chunk-
|
|
46
|
+
} from "./chunks/chunk-SKFI2MJA.js";
|
|
47
|
+
import "./chunks/chunk-5MI6Q3VS.js";
|
|
48
48
|
export {
|
|
49
49
|
CanonApiError,
|
|
50
50
|
ConnectorRelayError,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Operator-installed plugin callback. Signing material is restricted secret
|
|
2
|
+
* configuration and never part of conversations, catalogs or event payloads. */
|
|
3
|
+
export interface AgentWakeConfiguration {
|
|
4
|
+
callbackUrl: string;
|
|
5
|
+
secret: string;
|
|
6
|
+
conversationIds: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface AgentWakeHint {
|
|
9
|
+
type: 'wake';
|
|
10
|
+
reason: 'typing' | 'event';
|
|
11
|
+
}
|
|
@@ -11,7 +11,7 @@ export declare const MLS_DEV_UNSUPPORTED_CONTENT_ACTIONS: readonly ["forward_mes
|
|
|
11
11
|
export declare const ENDPOINT_CONTENT_ACTIONS: readonly ["mls_application", "mls_group_control", "initialize_endpoint", "append_artifact_command", "retain_artifact_asset", "sync_phone_contacts", "update_agent_behavior_policy", "update_conversation_agent_behavior_policy", "send_message", "delete_message", "forward_message", "react", "verb_execute", "send_contextual_message", "create_interaction", "consume_interaction", "start_conversation", "create_conversation", "add_member", "remove_member", "leave_conversation", "update_member_role", "update_conversation", "rename_conversation", "update_topic", "update_group_avatar", "mark_read", "cancel_queued_message", "set_conversation_preferences", "set_block", "save_contact", "remove_contact", "respond_interaction", "session_control", "approve_contact_request", "reject_contact_request", "cancel_direct_request", "update_message_disposition", "create_upload", "finalize_upload", "delete_upload", "retry_video_processing", "stage_media_message", "cancel_media_message", "renew_media_message_upload", "finalize_memo_stream", "abort_memo_stream", "create_voice_session", "join_voice_session", "decline_voice_session", "end_voice_session", "consume_runtime_signal", "consume_runtime_primitive", "generate_image"];
|
|
12
12
|
export declare const ENDPOINT_CONTENT_FUNCTION_QUERIES: readonly ["mls_group_feed", "mls_group_controls", "mls_group_admissions", "mls_interactions", "list_artifacts", "get_artifact_log", "get_artifact_asset", "discover_agents", "get_unread_summary", "peek_runtime_signal", "list_runtime_primitives", "get_agent_behavior_policy", "get_conversation_agent_behavior_policy", "get_ephemeral", "list_voice_sessions", "list_contact_requests", "list_conversations", "list_messages", "get_messages", "admission_fences", "list_contacts", "get_contact", "get_members", "resolve_admission", "resolve_group_admission", "get_runtime_card_state", "conversation", "conversation_changes", "get_upload", "get_media_message", "get_voice_session", "get_image_generation", "get_image_generation_status", "same_owner_agents"];
|
|
13
13
|
export declare const ENDPOINT_CONTENT_QUERIES: readonly ["mls_group_feed", "mls_group_controls", "mls_group_admissions", "mls_interactions", "list_artifacts", "get_artifact_log", "get_artifact_asset", "discover_agents", "get_unread_summary", "peek_runtime_signal", "list_runtime_primitives", "get_agent_behavior_policy", "get_conversation_agent_behavior_policy", "get_ephemeral", "list_voice_sessions", "list_contact_requests", "list_conversations", "list_messages", "get_messages", "admission_fences", "list_contacts", "get_contact", "get_members", "resolve_admission", "resolve_group_admission", "get_runtime_card_state", "conversation", "conversation_changes", "get_upload", "get_media_message", "get_voice_session", "get_image_generation", "get_image_generation_status", "same_owner_agents", "admit_messages"];
|
|
14
|
-
export declare const ENDPOINT_CONTENT_EPHEMERALS: readonly ["create_memo_stream", "publish_presence", "publish_typing", "publish_streaming", "publish_turn", "publish_runtime_status", "publish_session_state", "publish_runtime_info", "publish_runtime_activity", "append_memo_chunk"];
|
|
14
|
+
export declare const ENDPOINT_CONTENT_EPHEMERALS: readonly ["prewarm_agents", "create_memo_stream", "publish_presence", "publish_typing", "publish_streaming", "publish_turn", "publish_runtime_status", "publish_session_state", "publish_runtime_info", "publish_runtime_activity", "append_memo_chunk"];
|
|
15
15
|
export declare const ENDPOINT_CONTENT_EPHEMERAL_FAMILIES: readonly ["typing", "streaming", "turn-state", "agent-session", "runtime-card-state", "memo-streams", "runtime-info", "runtime-activity", "runtime-suppression", "runtime-silence", "runtime-attention", "call-attention", "agent-runtime", "presence"];
|
|
16
16
|
export declare const ENDPOINT_CONTENT_EVENTS: readonly ["connected", "heartbeat", "agent.context", "replay.expired", "error", "participation.suppressed", "artifact.changed", "message.created", "message.snapshot", "message.updated", "message.deleted", "message.media_terminal", "conversation.updated", "conversation.changed", "conversation.removed", "conversations.changed", "contacts.changed", "contact.added", "contact.updated", "contact.removed", "presence", "typing", "runtime.updated", "runtime.control", "turn.updated", "voice.session.started", "voice.session.ended", ...string[]];
|
|
17
17
|
export declare class EndpointContentPolicyError extends Error {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentWakeConfiguration } from '../backend-contracts/index.js';
|
|
1
2
|
import { type CanonMessage, type CanonConversation, type CanonUnifiedDiff, type CanonMessagesPage, type AgentContext, type MediaAttachment, type SendMessageOptions, type RegistrationStatus, type AgentOriginDeclaration, type SetRuntimeTurnOptions, type SetStreamingOptions } from './types.js';
|
|
2
3
|
import type { RuntimeInputKind } from './runtime-cards.js';
|
|
3
4
|
import type { ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, SessionRule } from './approval-types.js';
|
|
@@ -37,6 +38,11 @@ export declare class CanonClient {
|
|
|
37
38
|
private endpointPublish;
|
|
38
39
|
private authHeaders;
|
|
39
40
|
getAgentMe(): Promise<AgentContext>;
|
|
41
|
+
/** Register (or explicitly remove) this installed agent's signed readiness callback. */
|
|
42
|
+
configureWake(configuration: AgentWakeConfiguration | null): Promise<{
|
|
43
|
+
revision?: string;
|
|
44
|
+
disabled?: true;
|
|
45
|
+
}>;
|
|
40
46
|
discoverAgents(input?: DiscoverAgentsInput): Promise<DiscoverAgentsResult>;
|
|
41
47
|
/** Targeted membership read; session hosts must not poll the entire account. */
|
|
42
48
|
getConversation(conversationId: string): Promise<CanonConversation | null>;
|
|
@@ -149,6 +149,10 @@ export interface MlsDevGroupBridge {
|
|
|
149
149
|
/** 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
150
|
* announcing the room's change may have come while it was pending, and none may follow (D2 holds the room's sends). */
|
|
151
151
|
controlSettled?(conversationId: string): void;
|
|
152
|
+
/** A fetch applied entries of the room's feed: its encrypted history, read locally, has changed. */
|
|
153
|
+
historyChanged?(conversationId: string): void;
|
|
154
|
+
/** This device's event stream is connected, so each room's hints announce what Canon accepts into its feed. */
|
|
155
|
+
streamLive?(): boolean;
|
|
152
156
|
/** Reads and applies the room's conversation snapshot from Canon, for openings of members admitted since. */
|
|
153
157
|
readRoom?(conversationId: string): Promise<unknown>;
|
|
154
158
|
}
|
|
@@ -161,13 +165,16 @@ export declare function createMlsDevGroups(options: MlsDevDeviceClientOptions, c
|
|
|
161
165
|
* encrypted room (mls-payload-v3.md §4). Items and cards are not forwarded yet. */
|
|
162
166
|
forwardText(value: unknown, operationId: string, guardId?: string): Promise<EndpointOperation>;
|
|
163
167
|
sendText(value: unknown, operationId: string, guardId?: string): Promise<EndpointOperation>;
|
|
168
|
+
/** A room this device holds caught up is read as it holds it: what fetches the room's new entries says so
|
|
169
|
+
* (bridge.historyChanged). Otherwise a read fetches the feed first, which the polling fallback and the connected
|
|
170
|
+
* stream's minute-long safety poll rely on. */
|
|
164
171
|
queryTextHistory(conversationId: string, name: string, parameters: Record<string, unknown>): Promise<{
|
|
165
172
|
messages: Record<string, unknown>[];
|
|
166
173
|
nextBefore?: string | null;
|
|
167
174
|
missingMessageIds?: string[];
|
|
168
175
|
}>;
|
|
169
176
|
syncText(conversationId: string, options?: EndpointSyncOptions): Promise<EndpointSyncPage>;
|
|
170
|
-
isTextReady(conversationId: string)
|
|
177
|
+
isTextReady: (conversationId: string) => Promise<boolean>;
|
|
171
178
|
readAcceptedReceipt(operation: EndpointOperation): Promise<EndpointOperationReceipt | undefined>;
|
|
172
179
|
settleReceipt(operation: EndpointOperation, receipt: EndpointOperationReceipt): Promise<boolean>;
|
|
173
180
|
};
|
|
@@ -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,8 +18,8 @@ import {
|
|
|
18
18
|
toAnthropicImageBlock,
|
|
19
19
|
toAnthropicImageBlocksWithinBudget,
|
|
20
20
|
uploadMediaFile
|
|
21
|
-
} from "./chunks/chunk-
|
|
22
|
-
import "./chunks/chunk-
|
|
21
|
+
} from "./chunks/chunk-SKFI2MJA.js";
|
|
22
|
+
import "./chunks/chunk-5MI6Q3VS.js";
|
|
23
23
|
export {
|
|
24
24
|
DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
|
|
25
25
|
DEFAULT_MEDIA_CACHE_DIR,
|
package/dist/runner/runner.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentWakeConfiguration } from '../internal/backend-contracts/index.js';
|
|
1
2
|
import { type ConnectorRelayPage, type ConnectorRelaySent } from '../internal/backend-contracts/index.js';
|
|
2
3
|
import { type CanonEndpoint, type CanonRuntimeDescriptor, type CommunicateInput, type CommunicateResult } from '../internal/core/index.js';
|
|
3
4
|
import { type RunnerMlsOptions } from './mls.js';
|
|
@@ -11,6 +12,8 @@ export interface RunnerOptions {
|
|
|
11
12
|
version: string;
|
|
12
13
|
};
|
|
13
14
|
apiKey: string;
|
|
15
|
+
/** Signed, content-free wake callback installed by the agent. Null explicitly disables it. */
|
|
16
|
+
wake?: AgentWakeConfiguration | null;
|
|
14
17
|
environmentId: string;
|
|
15
18
|
/** CANON_HOME: a private 0700 directory, checked before anything is opened. */
|
|
16
19
|
home: string;
|