@canonmsg/agent-sdk 13.2.1 → 13.3.0

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.
@@ -8,7 +8,7 @@ import {
8
8
  wasmMlsCustody,
9
9
  windowsAclProblems,
10
10
  writeDurable
11
- } from "./chunk-POBMFS2K.js";
11
+ } from "./chunk-BQGXOAIW.js";
12
12
  import {
13
13
  BoundedFileReadError,
14
14
  CanonApiError,
@@ -30,17 +30,19 @@ import {
30
30
  renderCanonHostInboundContent,
31
31
  resolveCanonRuntimeConnection,
32
32
  splitTextWithinBytes,
33
+ utf8ByteLength,
33
34
  verifyCanonRuntimeConnection
34
- } from "./chunk-HTLKCUQG.js";
35
+ } from "./chunk-AVR7QCHK.js";
35
36
  import {
36
37
  NodeMlsDevCustodyError,
37
38
  createNodeMlsDevCustody,
38
39
  createPortableNodeMlsDevCustody
39
- } from "./chunk-BPSFHEAA.js";
40
+ } from "./chunk-HFT75BNB.js";
40
41
  import {
41
42
  parseMlsDevClientConfiguration
42
- } from "./chunk-2NVLVPSG.js";
43
+ } from "./chunk-NIORQKUI.js";
43
44
  import {
45
+ AGENT_HOME_ELSEWHERE,
44
46
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
45
47
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
46
48
  CONNECTOR_RELAY_INSTALLATION_PREFIX,
@@ -54,10 +56,13 @@ import {
54
56
  MLS_DEV_TEXT_PROTOCOL_V3,
55
57
  NO_REPLY_ACK_NOTE,
56
58
  PARTICIPATION_HISTORY_FETCH_LIMIT,
59
+ VERB_LIMITS,
57
60
  __name,
61
+ agentHomeElsewhereMessage,
58
62
  buildParticipationHistorySnapshot,
59
63
  connectorRelayResponseBytes,
60
64
  evaluateParticipationPolicy,
65
+ isCanonClientVersion,
61
66
  isConnectorRelayCredential,
62
67
  isConnectorRelayInstallation,
63
68
  isMlsDevTextProtocol,
@@ -70,7 +75,7 @@ import {
70
75
  parseMlsDevGroupRoster,
71
76
  resolveAllowedUploadMimeType,
72
77
  resolveConversationPolicyScope
73
- } from "./chunk-VIS5WXNH.js";
78
+ } from "./chunk-CUPSYLBV.js";
74
79
 
75
80
  // src/runner/types.ts
76
81
  var DriverSessionError = class extends Error {
@@ -1636,6 +1641,282 @@ import { createHash as createHash4, createHmac } from "node:crypto";
1636
1641
  import { constants as constants3 } from "node:fs";
1637
1642
  import { open as open2, realpath as realpath2 } from "node:fs/promises";
1638
1643
  import { isAbsolute, resolve } from "node:path";
1644
+
1645
+ // ../agent-tools/src/communication-tool.ts
1646
+ var CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
1647
+ var NON_EMPTY_ID = { type: "string", minLength: 1, maxLength: 160 };
1648
+ var ACTION_FIELDS = {
1649
+ list_conversations: { required: [], optional: ["limit"] },
1650
+ discover_agents: { required: [], optional: ["query", "limit", "cursor"] },
1651
+ message_existing: { required: ["conversationId", "text"], optional: ["messageId"] },
1652
+ start_conversation: { required: ["principalId", "text"], optional: ["selection", "messageId"] },
1653
+ create_conversation: { required: ["memberIds"], optional: ["name"] },
1654
+ manage_participants: { required: ["conversationId", "userId", "operation"], optional: [] },
1655
+ leave_conversation: { required: ["conversationId"], optional: [] },
1656
+ forward_message: { required: ["sourceConversationId", "targetConversationId", "messageId"], optional: ["text"] },
1657
+ share_contact: { required: ["conversationId", "contactUserId"], optional: ["text", "messageId"] }
1658
+ };
1659
+ var ALIASES = {
1660
+ start_direct: "start_conversation",
1661
+ create_group: "create_conversation",
1662
+ manage_group_members: "manage_participants"
1663
+ };
1664
+ var CANON_COMMUNICATE_SHAPE = Object.fromEntries(Object.entries(ACTION_FIELDS).map(([action, { required, optional }]) => [action, [...required, ...optional]]));
1665
+ var FIELD_SCHEMAS = {
1666
+ limit: { type: "integer", minimum: 1, maximum: 100 },
1667
+ query: { type: "string", maxLength: MAX_DISCOVER_AGENTS_QUERY_LENGTH },
1668
+ cursor: { type: "string", minLength: 1 },
1669
+ conversationId: NON_EMPTY_ID,
1670
+ text: { type: "string" },
1671
+ messageId: NON_EMPTY_ID,
1672
+ principalId: NON_EMPTY_ID,
1673
+ selection: {
1674
+ type: "object",
1675
+ required: ["mode"],
1676
+ additionalProperties: false,
1677
+ properties: { mode: { type: "string", enum: ["new", "latest_or_new", "specific"] }, conversationId: NON_EMPTY_ID }
1678
+ },
1679
+ memberIds: { type: "array", minItems: 1, maxItems: VERB_LIMITS.groupMembers - 1, uniqueItems: true, items: NON_EMPTY_ID },
1680
+ name: { type: "string", minLength: 1, maxLength: VERB_LIMITS.groupNameChars },
1681
+ userId: NON_EMPTY_ID,
1682
+ operation: { type: "string", enum: ["add", "remove"] },
1683
+ sourceConversationId: NON_EMPTY_ID,
1684
+ targetConversationId: NON_EMPTY_ID,
1685
+ contactUserId: NON_EMPTY_ID
1686
+ };
1687
+ var SEND_ID = "a stable id for this message; reuse it when retrying";
1688
+ var FIELD_NOTES = {
1689
+ limit: { list_conversations: "how many conversations, 1 to 100", discover_agents: `how many agents, 1 to ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS} (default ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS})` },
1690
+ query: { discover_agents: `a name or description prefix of at most ${MAX_DISCOVER_AGENTS_QUERY_LENGTH} characters; leave it out to list the directory` },
1691
+ cursor: { discover_agents: "the cursor a previous discover_agents call with the same query returned" },
1692
+ conversationId: {
1693
+ message_existing: "the conversation to post in",
1694
+ share_contact: "the conversation to share the contact in",
1695
+ manage_participants: "the conversation",
1696
+ leave_conversation: "the conversation to leave"
1697
+ },
1698
+ text: {
1699
+ message_existing: "the message",
1700
+ start_conversation: `the opening message, never split; at most ${MLS_DEV_TEXT_MAX_BYTES} UTF-8 bytes when encrypted. Use message_existing for longer follow-ups`,
1701
+ forward_message: "a caption",
1702
+ share_contact: "a caption"
1703
+ },
1704
+ messageId: { message_existing: SEND_ID, start_conversation: SEND_ID, share_contact: SEND_ID, forward_message: "the message to forward" },
1705
+ principalId: { start_conversation: "the person or agent to talk to" },
1706
+ selection: { start_conversation: "which direct conversation: mode new (the default), latest_or_new, or specific with its conversationId" },
1707
+ memberIds: { create_conversation: "the people and agents to add besides you" },
1708
+ name: { create_conversation: "a title" },
1709
+ userId: { manage_participants: "the person or agent to add or remove" },
1710
+ operation: { manage_participants: "add or remove" },
1711
+ sourceConversationId: { forward_message: "the conversation the message is in" },
1712
+ targetConversationId: { forward_message: "the conversation to forward it to" },
1713
+ contactUserId: { share_contact: "whose contact card to share" }
1714
+ };
1715
+ function communicateInputSchema(shape, textMaxLength = VERB_LIMITS.messageTextBytes) {
1716
+ const actions = Object.keys(shape);
1717
+ const properties = {
1718
+ action: { type: "string", enum: actions, description: "What to do. Each other field says which actions take it." }
1719
+ };
1720
+ for (const [field, schema] of Object.entries(FIELD_SCHEMAS)) {
1721
+ const uses = actions.filter((action) => shape[action].includes(field) || ACTION_FIELDS[action].required.includes(field));
1722
+ if (!uses.length) continue;
1723
+ const notes = uses.map((action) => `${action} (${ACTION_FIELDS[action].required.includes(field) ? "required" : "optional"}): ${FIELD_NOTES[field][action]}`);
1724
+ properties[field] = { ...schema, ...field === "text" && textMaxLength !== null ? { maxLength: textMaxLength } : {}, description: `${notes.join("; ")}.` };
1725
+ }
1726
+ return { type: "object", required: ["action"], additionalProperties: false, properties };
1727
+ }
1728
+ __name(communicateInputSchema, "communicateInputSchema");
1729
+ function isRecord3(value) {
1730
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1731
+ }
1732
+ __name(isRecord3, "isRecord");
1733
+ function readRequiredString(value) {
1734
+ return typeof value === "string" && value.trim() ? value : null;
1735
+ }
1736
+ __name(readRequiredString, "readRequiredString");
1737
+ var listed = /* @__PURE__ */ __name((names) => names.length < 2 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`, "listed");
1738
+ function parseCommunicateToolInput(value, shape = CANON_COMMUNICATE_SHAPE) {
1739
+ if (!isRecord3(value)) throw new Error("communicate arguments must be an object");
1740
+ const action = typeof value.action === "string" ? ALIASES[value.action] ?? value.action : "";
1741
+ if (!Object.hasOwn(shape, action)) {
1742
+ throw new Error(`communicate.action is invalid: use ${listed(Object.keys(shape))}`);
1743
+ }
1744
+ const fields = ACTION_FIELDS[action];
1745
+ const missing = fields.required.filter((field) => value[field] === void 0 || value[field] === null || typeof value[field] === "string" && !value[field].trim());
1746
+ if (missing.length) throw new Error(`${String(value.action)} needs ${listed(missing)}`);
1747
+ const taken = ["action", ...fields.required, ...fields.optional];
1748
+ const extra = Object.keys(value).filter((field) => !taken.includes(field));
1749
+ if (extra.length) throw new Error(`${String(value.action)} does not take ${listed(extra)}`);
1750
+ if (value.action === "list_conversations") {
1751
+ if (value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > 100)) throw new Error("list_conversations requires an optional limit from 1 to 100");
1752
+ return { action: "list_conversations", ...typeof value.limit === "number" ? { limit: value.limit } : {} };
1753
+ }
1754
+ if (value.action === "leave_conversation") {
1755
+ const conversationId = readRequiredString(value.conversationId);
1756
+ if (!conversationId) throw new Error("leave_conversation requires conversationId");
1757
+ return { action: "leave_conversation", conversationId };
1758
+ }
1759
+ if (value.action === "discover_agents") {
1760
+ if (value.query !== void 0 && typeof value.query !== "string") {
1761
+ throw new Error("communicate.query must be a string");
1762
+ }
1763
+ if (typeof value.query === "string" && value.query.length > MAX_DISCOVER_AGENTS_QUERY_LENGTH) {
1764
+ throw new Error(
1765
+ `communicate.query must be at most ${MAX_DISCOVER_AGENTS_QUERY_LENGTH} characters`
1766
+ );
1767
+ }
1768
+ if (value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS)) {
1769
+ throw new Error(
1770
+ `communicate.limit must be an integer from 1 to ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS}`
1771
+ );
1772
+ }
1773
+ if (value.cursor !== void 0 && (typeof value.cursor !== "string" || value.cursor.length === 0)) {
1774
+ throw new Error("communicate.cursor must be a non-empty string");
1775
+ }
1776
+ return {
1777
+ action: "discover_agents",
1778
+ ...typeof value.query === "string" ? { query: value.query } : {},
1779
+ limit: typeof value.limit === "number" ? value.limit : CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
1780
+ ...typeof value.cursor === "string" ? { cursor: value.cursor } : {}
1781
+ };
1782
+ }
1783
+ if (value.action === "create_group" || value.action === "create_conversation") {
1784
+ const name = value.name === void 0 ? void 0 : readRequiredString(value.name);
1785
+ if (name === null) throw new Error("communicate.name must be a non-empty string when supplied");
1786
+ if (!Array.isArray(value.memberIds) || value.memberIds.length === 0 || !value.memberIds.every((entry) => readRequiredString(entry) !== null)) {
1787
+ throw new Error("communicate.memberIds must contain at least one principal id");
1788
+ }
1789
+ return {
1790
+ action: value.action,
1791
+ ...name ? { name } : {},
1792
+ memberIds: [...new Set(value.memberIds)]
1793
+ };
1794
+ }
1795
+ if (value.action === "forward_message") {
1796
+ const sourceConversationId = readRequiredString(value.sourceConversationId);
1797
+ const targetConversationId = readRequiredString(value.targetConversationId);
1798
+ const messageId2 = readRequiredString(value.messageId);
1799
+ if (!sourceConversationId) throw new Error("communicate.sourceConversationId is required");
1800
+ if (!targetConversationId) throw new Error("communicate.targetConversationId is required");
1801
+ if (!messageId2) throw new Error("communicate.messageId is required");
1802
+ if (value.text !== void 0 && typeof value.text !== "string") {
1803
+ throw new Error("communicate.text must be a string");
1804
+ }
1805
+ return {
1806
+ action: "forward_message",
1807
+ sourceConversationId,
1808
+ targetConversationId,
1809
+ messageId: messageId2,
1810
+ ...value.text !== void 0 ? { text: value.text } : {}
1811
+ };
1812
+ }
1813
+ if (value.action === "share_contact") {
1814
+ const conversationId = readRequiredString(value.conversationId);
1815
+ const contactUserId = readRequiredString(value.contactUserId);
1816
+ const messageId2 = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
1817
+ if (!conversationId) throw new Error("communicate.conversationId is required");
1818
+ if (!contactUserId) throw new Error("communicate.contactUserId is required");
1819
+ if (value.text !== void 0 && typeof value.text !== "string") {
1820
+ throw new Error("communicate.text must be a string");
1821
+ }
1822
+ if (value.messageId !== void 0 && !messageId2) {
1823
+ throw new Error("communicate.messageId must be a non-empty string");
1824
+ }
1825
+ return {
1826
+ action: "share_contact",
1827
+ conversationId,
1828
+ contactUserId,
1829
+ ...value.text !== void 0 ? { text: value.text } : {},
1830
+ ...messageId2 ? { messageId: messageId2 } : {}
1831
+ };
1832
+ }
1833
+ if (value.action === "manage_group_members" || value.action === "manage_participants") {
1834
+ const conversationId = readRequiredString(value.conversationId);
1835
+ const userId = readRequiredString(value.userId);
1836
+ if (!conversationId) throw new Error("communicate.conversationId is required");
1837
+ if (!userId) throw new Error("communicate.userId is required");
1838
+ if (value.operation !== "add" && value.operation !== "remove") {
1839
+ throw new Error("communicate.operation must be add or remove");
1840
+ }
1841
+ return {
1842
+ action: value.action,
1843
+ conversationId,
1844
+ userId,
1845
+ operation: value.operation
1846
+ };
1847
+ }
1848
+ const text3 = readRequiredString(value.text);
1849
+ const messageId = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
1850
+ if (!text3) throw new Error("communicate.text is required");
1851
+ if (value.messageId !== void 0 && !messageId) {
1852
+ throw new Error("communicate.messageId must be a non-empty string");
1853
+ }
1854
+ if (value.action === "message_existing") {
1855
+ const conversationId = readRequiredString(value.conversationId);
1856
+ if (!conversationId) throw new Error("communicate.conversationId is required");
1857
+ return {
1858
+ action: "message_existing",
1859
+ conversationId,
1860
+ text: text3,
1861
+ ...messageId ? { messageId } : {}
1862
+ };
1863
+ }
1864
+ if (value.action === "start_direct" || value.action === "start_conversation") {
1865
+ const principalId = readRequiredString(value.principalId);
1866
+ if (!principalId) throw new Error("communicate.principalId is required");
1867
+ if (utf8ByteLength(text3) > MLS_DEV_TEXT_MAX_BYTES) {
1868
+ throw new Error(`${value.action} opening text must be at most ${MLS_DEV_TEXT_MAX_BYTES} UTF-8 bytes; use message_existing for longer follow-ups`);
1869
+ }
1870
+ const selection = value.selection === void 0 ? value.action === "start_conversation" ? { mode: "new" } : void 0 : parseDirectConversationSelection(value.selection);
1871
+ return {
1872
+ action: value.action,
1873
+ principalId,
1874
+ text: text3,
1875
+ ...selection ? { selection } : {},
1876
+ ...messageId ? { messageId } : {}
1877
+ };
1878
+ }
1879
+ throw new Error("communicate.action is invalid");
1880
+ }
1881
+ __name(parseCommunicateToolInput, "parseCommunicateToolInput");
1882
+ function renderResult(result) {
1883
+ let id;
1884
+ if (result.status === "messaged" || result.status === "forwarded" || result.status === "shared") {
1885
+ id = result.messageId;
1886
+ } else if (result.status === "added" || result.status === "removed") {
1887
+ id = result.userId;
1888
+ } else if (result.status === "created") {
1889
+ id = result.conversationId;
1890
+ } else if (result.status === "requested" || result.status === "pending") {
1891
+ id = result.requestId;
1892
+ }
1893
+ return {
1894
+ content: [{
1895
+ type: "text",
1896
+ text: `communicate: ${result.status}${id ? ` (${id})` : ""}
1897
+ ${JSON.stringify(result, null, 2)}`
1898
+ }]
1899
+ };
1900
+ }
1901
+ __name(renderResult, "renderResult");
1902
+ async function executeCanonCommunicateTool(client, args, shape = CANON_COMMUNICATE_SHAPE) {
1903
+ let input;
1904
+ try {
1905
+ input = parseCommunicateToolInput(args, shape);
1906
+ } catch (error) {
1907
+ return {
1908
+ content: [{
1909
+ type: "text",
1910
+ text: `Invalid communicate arguments: ${error.message}`
1911
+ }],
1912
+ isError: true
1913
+ };
1914
+ }
1915
+ return renderResult(await client.communicate(input));
1916
+ }
1917
+ __name(executeCanonCommunicateTool, "executeCanonCommunicateTool");
1918
+
1919
+ // src/runner/mls.ts
1639
1920
  var fail = /* @__PURE__ */ __name(() => {
1640
1921
  throw new Error("MLS_AUTHORITY_REQUIRED");
1641
1922
  }, "fail");
@@ -1712,30 +1993,22 @@ function mentionsAgent(text3, names) {
1712
1993
  });
1713
1994
  }
1714
1995
  __name(mentionsAgent, "mentionsAgent");
1996
+ var ENCRYPTED_COMMUNICATE_SHAPE = {
1997
+ list_conversations: ["limit"],
1998
+ discover_agents: ["limit"],
1999
+ message_existing: ["conversationId", "text"]
2000
+ };
1715
2001
  var ENCRYPTED_COMMUNICATE = {
1716
2002
  name: "communicate",
1717
2003
  description: "List Canon conversations or agents, or send text to the current encrypted conversation. Other Canon outputs are not yet supported.",
1718
- inputSchema: { type: "object", oneOf: [
1719
- {
1720
- type: "object",
1721
- required: ["action"],
1722
- additionalProperties: false,
1723
- properties: { action: { const: "list_conversations" }, limit: { type: "integer", minimum: 1, maximum: 100 } }
1724
- },
1725
- {
1726
- type: "object",
1727
- required: ["action"],
1728
- additionalProperties: false,
1729
- properties: { action: { const: "discover_agents" }, limit: { type: "integer", minimum: 1, maximum: 10 } }
1730
- },
1731
- {
1732
- type: "object",
1733
- required: ["action", "conversationId", "text"],
1734
- additionalProperties: false,
1735
- properties: { action: { const: "message_existing" }, conversationId: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 } }
1736
- }
1737
- ] }
2004
+ inputSchema: communicateInputSchema(ENCRYPTED_COMMUNICATE_SHAPE, null)
1738
2005
  };
2006
+ var errandShape = /* @__PURE__ */ __name((contact) => ({ ...ENCRYPTED_COMMUNICATE_SHAPE, ...contact ? { start_conversation: ["principalId", "text"] } : {} }), "errandShape");
2007
+ var errandCommunicate = /* @__PURE__ */ __name((contact) => ({
2008
+ name: "communicate",
2009
+ description: "List Canon conversations or agents, or send text to your errand's recipient or to the conversation it came from" + (contact ? ". Start the conversation with the recipient first, with start_conversation." : "."),
2010
+ inputSchema: communicateInputSchema(errandShape(contact), null)
2011
+ }), "errandCommunicate");
1739
2012
  var MAX_CONFIGURATION_BYTES = 64 * 1024;
1740
2013
  function invalid() {
1741
2014
  throw new Error("MLS_DEV_CONFIGURATION_INVALID");
@@ -2412,232 +2685,16 @@ function relayRunnerHome(home, installationId) {
2412
2685
  }
2413
2686
  __name(relayRunnerHome, "relayRunnerHome");
2414
2687
 
2688
+ // src/version.ts
2689
+ var AGENT_SDK_VERSION = true ? "13.3.0" : void 0;
2690
+ var RUNNER_CLIENT = isCanonClientVersion(AGENT_SDK_VERSION) ? Object.freeze({ kind: "runner", version: AGENT_SDK_VERSION }) : void 0;
2691
+
2415
2692
  // src/runner/runner.ts
2416
2693
  import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
2417
2694
  import { readFileSync as readFileSync7, rmSync as rmSync2 } from "node:fs";
2418
2695
  import { realpath as realpath4, stat as stat2 } from "node:fs/promises";
2419
2696
  import { basename as basename4, isAbsolute as isAbsolute3, join as join8, sep } from "node:path";
2420
2697
 
2421
- // ../agent-tools/src/communication-tool.ts
2422
- var CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
2423
- function isRecord3(value) {
2424
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
2425
- }
2426
- __name(isRecord3, "isRecord");
2427
- function readRequiredString(value) {
2428
- return typeof value === "string" && value.trim() ? value : null;
2429
- }
2430
- __name(readRequiredString, "readRequiredString");
2431
- function hasOnlyKeys(record, allowed) {
2432
- return Object.keys(record).every((key) => allowed.has(key));
2433
- }
2434
- __name(hasOnlyKeys, "hasOnlyKeys");
2435
- function parseCommunicateToolInput(value) {
2436
- if (!isRecord3(value)) throw new Error("communicate arguments must be an object");
2437
- if (value.action === "list_conversations") {
2438
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "limit"])) || value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > 100)) throw new Error("list_conversations requires an optional limit from 1 to 100");
2439
- return { action: "list_conversations", ...typeof value.limit === "number" ? { limit: value.limit } : {} };
2440
- }
2441
- if (value.action === "leave_conversation") {
2442
- const conversationId = readRequiredString(value.conversationId);
2443
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "conversationId"])) || !conversationId) throw new Error("leave_conversation requires conversationId");
2444
- return { action: "leave_conversation", conversationId };
2445
- }
2446
- if (value.action === "discover_agents") {
2447
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "query", "limit", "cursor"]))) {
2448
- throw new Error("discover_agents contains unsupported fields");
2449
- }
2450
- if (value.query !== void 0 && typeof value.query !== "string") {
2451
- throw new Error("communicate.query must be a string");
2452
- }
2453
- if (typeof value.query === "string" && value.query.length > MAX_DISCOVER_AGENTS_QUERY_LENGTH) {
2454
- throw new Error(
2455
- `communicate.query must be at most ${MAX_DISCOVER_AGENTS_QUERY_LENGTH} characters`
2456
- );
2457
- }
2458
- if (value.limit !== void 0 && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS)) {
2459
- throw new Error(
2460
- `communicate.limit must be an integer from 1 to ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS}`
2461
- );
2462
- }
2463
- if (value.cursor !== void 0 && (typeof value.cursor !== "string" || value.cursor.length === 0)) {
2464
- throw new Error("communicate.cursor must be a non-empty string");
2465
- }
2466
- return {
2467
- action: "discover_agents",
2468
- ...typeof value.query === "string" ? { query: value.query } : {},
2469
- limit: typeof value.limit === "number" ? value.limit : CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
2470
- ...typeof value.cursor === "string" ? { cursor: value.cursor } : {}
2471
- };
2472
- }
2473
- if (value.action === "create_group" || value.action === "create_conversation") {
2474
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "name", "memberIds"]))) {
2475
- throw new Error("create_group contains unsupported fields");
2476
- }
2477
- const name = value.name === void 0 ? void 0 : readRequiredString(value.name);
2478
- if (name === null) throw new Error("communicate.name must be a non-empty string when supplied");
2479
- if (!Array.isArray(value.memberIds) || value.memberIds.length === 0 || !value.memberIds.every((entry) => readRequiredString(entry) !== null)) {
2480
- throw new Error("communicate.memberIds must contain at least one principal id");
2481
- }
2482
- return {
2483
- action: value.action,
2484
- ...name ? { name } : {},
2485
- memberIds: [...new Set(value.memberIds)]
2486
- };
2487
- }
2488
- if (value.action === "forward_message") {
2489
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set([
2490
- "action",
2491
- "sourceConversationId",
2492
- "targetConversationId",
2493
- "messageId",
2494
- "text"
2495
- ]))) {
2496
- throw new Error("forward_message contains unsupported fields");
2497
- }
2498
- const sourceConversationId = readRequiredString(value.sourceConversationId);
2499
- const targetConversationId = readRequiredString(value.targetConversationId);
2500
- const messageId2 = readRequiredString(value.messageId);
2501
- if (!sourceConversationId) throw new Error("communicate.sourceConversationId is required");
2502
- if (!targetConversationId) throw new Error("communicate.targetConversationId is required");
2503
- if (!messageId2) throw new Error("communicate.messageId is required");
2504
- if (value.text !== void 0 && typeof value.text !== "string") {
2505
- throw new Error("communicate.text must be a string");
2506
- }
2507
- return {
2508
- action: "forward_message",
2509
- sourceConversationId,
2510
- targetConversationId,
2511
- messageId: messageId2,
2512
- ...value.text !== void 0 ? { text: value.text } : {}
2513
- };
2514
- }
2515
- if (value.action === "share_contact") {
2516
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set([
2517
- "action",
2518
- "conversationId",
2519
- "contactUserId",
2520
- "text",
2521
- "messageId"
2522
- ]))) {
2523
- throw new Error("share_contact contains unsupported fields");
2524
- }
2525
- const conversationId = readRequiredString(value.conversationId);
2526
- const contactUserId = readRequiredString(value.contactUserId);
2527
- const messageId2 = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
2528
- if (!conversationId) throw new Error("communicate.conversationId is required");
2529
- if (!contactUserId) throw new Error("communicate.contactUserId is required");
2530
- if (value.text !== void 0 && typeof value.text !== "string") {
2531
- throw new Error("communicate.text must be a string");
2532
- }
2533
- if (value.messageId !== void 0 && !messageId2) {
2534
- throw new Error("communicate.messageId must be a non-empty string");
2535
- }
2536
- return {
2537
- action: "share_contact",
2538
- conversationId,
2539
- contactUserId,
2540
- ...value.text !== void 0 ? { text: value.text } : {},
2541
- ...messageId2 ? { messageId: messageId2 } : {}
2542
- };
2543
- }
2544
- if (value.action === "manage_group_members" || value.action === "manage_participants") {
2545
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set([
2546
- "action",
2547
- "conversationId",
2548
- "userId",
2549
- "operation"
2550
- ]))) {
2551
- throw new Error("manage_group_members contains unsupported fields");
2552
- }
2553
- const conversationId = readRequiredString(value.conversationId);
2554
- const userId = readRequiredString(value.userId);
2555
- if (!conversationId) throw new Error("communicate.conversationId is required");
2556
- if (!userId) throw new Error("communicate.userId is required");
2557
- if (value.operation !== "add" && value.operation !== "remove") {
2558
- throw new Error("communicate.operation must be add or remove");
2559
- }
2560
- return {
2561
- action: value.action,
2562
- conversationId,
2563
- userId,
2564
- operation: value.operation
2565
- };
2566
- }
2567
- const text3 = readRequiredString(value.text);
2568
- const messageId = value.messageId === void 0 ? void 0 : readRequiredString(value.messageId);
2569
- if (!text3) throw new Error("communicate.text is required");
2570
- if (value.messageId !== void 0 && !messageId) {
2571
- throw new Error("communicate.messageId must be a non-empty string");
2572
- }
2573
- if (value.action === "message_existing") {
2574
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "conversationId", "text", "messageId"]))) {
2575
- throw new Error("message_existing contains unsupported fields");
2576
- }
2577
- const conversationId = readRequiredString(value.conversationId);
2578
- if (!conversationId) throw new Error("communicate.conversationId is required");
2579
- return {
2580
- action: "message_existing",
2581
- conversationId,
2582
- text: text3,
2583
- ...messageId ? { messageId } : {}
2584
- };
2585
- }
2586
- if (value.action === "start_direct" || value.action === "start_conversation") {
2587
- if (!hasOnlyKeys(value, /* @__PURE__ */ new Set(["action", "principalId", "text", "selection", "messageId"]))) {
2588
- throw new Error("start_direct contains unsupported fields");
2589
- }
2590
- const principalId = readRequiredString(value.principalId);
2591
- if (!principalId) throw new Error("communicate.principalId is required");
2592
- const selection = value.selection === void 0 ? value.action === "start_conversation" ? { mode: "new" } : void 0 : parseDirectConversationSelection(value.selection);
2593
- return {
2594
- action: value.action,
2595
- principalId,
2596
- text: text3,
2597
- ...selection ? { selection } : {},
2598
- ...messageId ? { messageId } : {}
2599
- };
2600
- }
2601
- throw new Error("communicate.action is invalid");
2602
- }
2603
- __name(parseCommunicateToolInput, "parseCommunicateToolInput");
2604
- function renderResult(result) {
2605
- let id;
2606
- if (result.status === "messaged" || result.status === "forwarded" || result.status === "shared") {
2607
- id = result.messageId;
2608
- } else if (result.status === "added" || result.status === "removed") {
2609
- id = result.userId;
2610
- } else if (result.status === "created") {
2611
- id = result.conversationId;
2612
- } else if (result.status === "requested" || result.status === "pending") {
2613
- id = result.requestId;
2614
- }
2615
- return {
2616
- content: [{
2617
- type: "text",
2618
- text: `communicate: ${result.status}${id ? ` (${id})` : ""}
2619
- ${JSON.stringify(result, null, 2)}`
2620
- }]
2621
- };
2622
- }
2623
- __name(renderResult, "renderResult");
2624
- async function executeCanonCommunicateTool(client, args) {
2625
- let input;
2626
- try {
2627
- input = parseCommunicateToolInput(args);
2628
- } catch (error) {
2629
- return {
2630
- content: [{
2631
- type: "text",
2632
- text: `Invalid communicate arguments: ${error.message}`
2633
- }],
2634
- isError: true
2635
- };
2636
- }
2637
- return renderResult(await client.communicate(input));
2638
- }
2639
- __name(executeCanonCommunicateTool, "executeCanonCommunicateTool");
2640
-
2641
2698
  // src/runner/finals.ts
2642
2699
  import { createHash as createHash7 } from "node:crypto";
2643
2700
  import { readdirSync as readdirSync3, readFileSync as readFileSync5, rmdirSync, unlinkSync as unlinkSync2 } from "node:fs";
@@ -3371,7 +3428,21 @@ var STYLE_RULES = {
3371
3428
  observer: "Stay quiet unless a reply is necessary, and end your turn with no_reply."
3372
3429
  };
3373
3430
  var factText = /* @__PURE__ */ __name((value, limit = MAX_FACT_NAME) => speaker(value).slice(0, limit).trimEnd(), "factText");
3374
- function factsPrompt({ name, topic, owner, members, rules }, kind) {
3431
+ var mentionKey = /* @__PURE__ */ __name((value) => value.normalize("NFC").trim().replace(/^@+/, "").replace(/\s+/g, " ").toLowerCase(), "mentionKey");
3432
+ var mentionsToo = /* @__PURE__ */ __name((name, other) => other.length > 0 && name.startsWith(other) && !/^[\p{L}\p{N}\p{M}_]/u.test(name.slice(other.length)), "mentionsToo");
3433
+ function mentionOf(peer, agents) {
3434
+ const name = peer.name?.trim();
3435
+ const key = name ? mentionKey(name) : "";
3436
+ if (name && key && !name.startsWith("@") && factText(name) === name && !agents.some((other) => other !== peer && !other.isYou && [other.name, other.id].some((word) => word !== void 0 && mentionsToo(key, mentionKey(word))))) return name;
3437
+ return ENDPOINT_ID_PATTERN.test(peer.id) ? peer.id : void 0;
3438
+ }
3439
+ __name(mentionOf, "mentionOf");
3440
+ function factsPrompt({ name, topic, owner, members, rules }, kind, self) {
3441
+ const agents = members.filter((member) => member.type === "ai_agent");
3442
+ const reach = rules && !rules.allowAgentToAgent ? [] : members.slice(0, MAX_FACT_MEMBERS).filter((member) => member.type === "ai_agent" && !member.isYou).flatMap((peer) => {
3443
+ const mention = mentionOf(peer, agents), seen = factText(peer.name ?? peer.id);
3444
+ return !mention ? [] : [mention === seen ? `@${mention}` : `@${mention} (the id of ${seen})`];
3445
+ });
3375
3446
  const shown = members.slice(0, MAX_FACT_MEMBERS).map((member) => member.isYou ? "you" : factText(member.name ?? member.id) + (member.isOwner ? OWNER_MARK : member.type === "ai_agent" ? " [agent]" : ""));
3376
3447
  const lines = !rules ? [] : [
3377
3448
  STYLE_RULES[rules.style],
@@ -3382,10 +3453,12 @@ function factsPrompt({ name, topic, owner, members, rules }, kind) {
3382
3453
  ].filter((line) => line !== void 0);
3383
3454
  return [
3384
3455
  FACTS_OPEN,
3456
+ ...self ? [`Your name: ${factText(self)}`] : [],
3385
3457
  `Your owner: ${factText(owner.name ?? owner.id)}`,
3386
3458
  ...name ? [`Conversation: ${factText(name)}`] : [],
3387
3459
  ...topic ? [`Topic: ${factText(topic, MAX_FACT_TOPIC)}`] : [],
3388
3460
  `Members: ${shown.join(", ")}${members.length > shown.length ? `, and ${members.length - shown.length} more` : ""}`,
3461
+ ...reach.length ? [`${reach.length > 1 ? "Other agents here answer only a message that mentions them" : "Another agent here answers only a message that mentions it"}: ${reach.join(", ")}.`] : [],
3389
3462
  ...lines.length ? [
3390
3463
  "This conversation's rules, which your owner or any of its admins can set. They say how you take part here, not what you may do:",
3391
3464
  ...lines.map((line) => `- ${line}`)
@@ -3394,16 +3467,6 @@ function factsPrompt({ name, topic, owner, members, rules }, kind) {
3394
3467
  ];
3395
3468
  }
3396
3469
  __name(factsPrompt, "factsPrompt");
3397
- var errandCommunicate = /* @__PURE__ */ __name((contact) => ({
3398
- name: "communicate",
3399
- description: "List Canon conversations or agents, or send text to your errand's recipient or to the conversation it came from" + (contact ? ". Start the conversation with the recipient first, with start_conversation." : "."),
3400
- inputSchema: { type: "object", oneOf: [...ENCRYPTED_COMMUNICATE.inputSchema.oneOf, ...contact ? [{
3401
- type: "object",
3402
- required: ["action", "principalId", "text"],
3403
- additionalProperties: false,
3404
- properties: { action: { const: "start_conversation" }, principalId: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 } }
3405
- }] : []] }
3406
- }), "errandCommunicate");
3407
3470
  var promptLine = /* @__PURE__ */ __name((message2, name, media, mark = "") => [
3408
3471
  `${speaker(name)}${mark}: ${defused(renderCanonHostInboundContent(message2, media?.files))}`,
3409
3472
  ...(media?.failed ?? []).map((label) => `[Attachment "${defused(inert(label))}" could not be downloaded]`)
@@ -3553,6 +3616,7 @@ async function createRunner(options) {
3553
3616
  }, "keyed");
3554
3617
  let outboundClosed = false;
3555
3618
  let ownershipVersion;
3619
+ let installationId;
3556
3620
  let displayName;
3557
3621
  let ownerOf;
3558
3622
  const refused = /* @__PURE__ */ new Set();
@@ -3686,11 +3750,18 @@ async function createRunner(options) {
3686
3750
  log(why === "plaintext" ? `Refusing room ${roomId}: it is not encrypted, and this runner takes part only in encrypted rooms. Its messages are not delivered to this agent.` : `Refusing encrypted room ${roomId}: ${why === "no-device" ? "this runner has no MLS device (run it with --mls-dev-config)" : why === "remote" ? `the ${driver.name} driver runs off this host, which an encrypted room allows only in relay mode` : "its encryption protocol is not supported"}. Its messages are not delivered to this agent.`);
3687
3751
  }
3688
3752
  __name(refuse, "refuse");
3753
+ let homeSince = 0;
3689
3754
  async function authorize() {
3690
3755
  await endpoint.mlsDevDevice.ensureReady();
3691
3756
  const me = await client.getAgentMe();
3692
3757
  requireMlsSupport({ environmentId: options.environmentId });
3693
3758
  if (me.agentId !== agentId || me.ownershipVersion !== ownershipVersion) throw new Error("MLS_AUTHORITY_REQUIRED");
3759
+ if (!options.relay && me.home && (me.home.kind === "connector" || me.home.installationId !== void 0 && me.home.installationId !== installationId)) {
3760
+ const error = Object.assign(new Error(agentHomeElsewhereMessage(me.displayName, me.home)), { code: AGENT_HOME_ELSEWHERE });
3761
+ options.onFatal?.(error);
3762
+ throw error;
3763
+ }
3764
+ homeSince = !options.relay && me.home?.kind === "runner" ? me.home.since ?? 0 : 0;
3694
3765
  outboundClosed = me.outboundPolicy === "closed";
3695
3766
  if (outboundClosed && options.relay) throw new Error("MLS_AUTHORITY_REQUIRED");
3696
3767
  displayName = me.displayName;
@@ -3733,6 +3804,50 @@ ${error instanceof Error ? error.message : String(error)}`;
3733
3804
  turn.timer.unref?.();
3734
3805
  }
3735
3806
  __name(progress, "progress");
3807
+ const waitingRooms = /* @__PURE__ */ new Map(), noting = /* @__PURE__ */ new Map();
3808
+ let ownerAsk;
3809
+ function noteAdmission(roomId, known) {
3810
+ const next = (noting.get(roomId) ?? Promise.resolve()).then(() => lookAtAdmission(roomId, known));
3811
+ noting.set(roomId, next.catch(() => void 0));
3812
+ return next;
3813
+ }
3814
+ __name(noteAdmission, "noteAdmission");
3815
+ async function lookAtAdmission(roomId, known) {
3816
+ if (!mlsOn || stopping) return;
3817
+ const inGroup = /* @__PURE__ */ __name(async () => {
3818
+ const view = await endpoint.mlsDevGroups.read(roomId);
3819
+ return !!view && view.status !== "removed";
3820
+ }, "inGroup");
3821
+ const read = /* @__PURE__ */ __name(async () => await endpoint.getConversation(roomId).catch(() => null) ?? void 0, "read");
3822
+ let joined = await inGroup(), room2 = known;
3823
+ if (!joined && known && !waitingRooms.has(roomId)) {
3824
+ room2 = await read() ?? room2;
3825
+ joined = await inGroup();
3826
+ }
3827
+ if (joined) {
3828
+ if (waitingRooms.has(roomId)) log(`Joined ${waitingRooms.get(roomId)}.`);
3829
+ waitingRooms.delete(roomId);
3830
+ return;
3831
+ }
3832
+ if (waitingRooms.has(roomId) || (await endpoint.mlsDevGroups.readAdmissionHold(roomId)).outside !== true) return;
3833
+ room2 ??= await read();
3834
+ const members = Array.isArray(room2?.memberIds) ? room2.memberIds : [];
3835
+ if (!members.includes(agentId)) return;
3836
+ const label = typeof room2?.name === "string" && room2.name.trim() ? `"${room2.name.trim()}"` : `room ${roomId}`;
3837
+ waitingRooms.set(roomId, label);
3838
+ ownerAsk ??= client.getAgentMe().then((me) => ({ id: me.ownerId, ...me.ownerName ? { name: me.ownerName } : {} }));
3839
+ const owner2 = await ownerAsk.catch(() => void 0);
3840
+ const ask2 = owner2 && members.includes(owner2.id) ? `Ask ${owner2.name ?? "its owner"} to open Canon.` : "Ask one of its members to open Canon.";
3841
+ log(`Waiting to join ${label}: only a device already in it can add this agent's device. ${ask2}`);
3842
+ }
3843
+ __name(lookAtAdmission, "lookAtAdmission");
3844
+ async function noteAdmissions() {
3845
+ const { conversations = [] } = await endpoint.query("list_conversations", { limit: 100 });
3846
+ for (const room2 of conversations) {
3847
+ if (typeof room2.id === "string" && ["mls", "mlsRequired", "mlsProtocol"].some((key) => Object.hasOwn(room2, key))) await noteAdmission(room2.id, room2);
3848
+ }
3849
+ }
3850
+ __name(noteAdmissions, "noteAdmissions");
3736
3851
  async function offer(event) {
3737
3852
  offering += 1;
3738
3853
  touch();
@@ -3783,6 +3898,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3783
3898
  await authorize();
3784
3899
  const binding = await groups.readVerifiedMessageBinding({ conversationId, messageId: payload.message.id });
3785
3900
  if (!binding) throw new Error("MLS_INPUT_NOT_VERIFIED");
3901
+ if (binding.acceptedAtMs < homeSince) return void await handle.settle("mls-before-home");
3786
3902
  const { messages } = await endpoint.query("get_messages", { conversationId, messageIds: [binding.messageId] });
3787
3903
  const [message2] = messages;
3788
3904
  if (messages.length !== 1 || message2.id !== binding.messageId || message2.senderId !== binding.senderId || message2.senderType !== binding.senderPrincipal.type || !Number.isSafeInteger(message2.mlsFeedOrdinal) || typeof message2.text !== "string" && !(message2.text === null && (message2.attachments?.length ?? 0) === 1)) throw new Error("MLS_INPUT_NOT_VERIFIED");
@@ -4166,6 +4282,8 @@ ${decision.reasonCode}`;
4166
4282
  function bindTools(current, turn) {
4167
4283
  const questions = /* @__PURE__ */ new Map();
4168
4284
  let asked = 0;
4285
+ const errandContact = Boolean(turn.errand && "contact" in turn.errand.target);
4286
+ const shape = turn.errand ? errandShape(errandContact) : ENCRYPTED_COMMUNICATE_SHAPE;
4169
4287
  const handlers = {
4170
4288
  no_reply: /* @__PURE__ */ __name(async ({ reason }) => {
4171
4289
  turn.silenced = typeof reason === "string" && reason.trim() ? { reason } : {};
@@ -4186,7 +4304,7 @@ ${decision.reasonCode}`;
4186
4304
  turn.outbox.push(file.path);
4187
4305
  return { result: toolText(`${basename4(file.path)} will be sent with your reply.`) };
4188
4306
  }, "send_file"),
4189
- communicate: /* @__PURE__ */ __name(async (args) => ({ result: await communicate(current, turn, args) }), "communicate"),
4307
+ communicate: /* @__PURE__ */ __name(async (args) => ({ result: await communicate(current, turn, args, shape) }), "communicate"),
4190
4308
  // RUN-25 through a tool: the same `ask`, so the same responder, card, expiry, waiting_input and paused deadline;
4191
4309
  // the turn's end or an abort withdraws it, and the call then fails.
4192
4310
  ask_question: /* @__PURE__ */ __name(async (args) => {
@@ -4213,7 +4331,7 @@ ${decision.reasonCode}`;
4213
4331
  // §12.6: an errand turn reaches its recipient and its origin, and asks only in an origin its report goes to.
4214
4332
  definitions: errand ? [
4215
4333
  TOOLS.no_reply,
4216
- errandCommunicate("contact" in errand.target),
4334
+ errandCommunicate(errandContact),
4217
4335
  ...errand.reports && cards && offersQuestions ? [TOOLS.ask_question] : []
4218
4336
  ] : [
4219
4337
  TOOLS.no_reply,
@@ -4250,12 +4368,12 @@ ${decision.reasonCode}`;
4250
4368
  }, "call") } };
4251
4369
  }
4252
4370
  __name(bindTools, "bindTools");
4253
- async function communicate(current, turn, args) {
4371
+ async function communicate(current, turn, args, shape) {
4254
4372
  const errand = turn.errand;
4255
4373
  const origins = errand ? errand.origin ? [errand.origin.room] : [] : turn.contexts.flatMap((record) => record.origin ? [record.origin] : []);
4256
4374
  let command;
4257
4375
  try {
4258
- command = parseCommunicateToolInput(args);
4376
+ command = parseCommunicateToolInput(args, shape);
4259
4377
  } catch (error) {
4260
4378
  return toolText(`Invalid communicate arguments: ${error instanceof Error ? error.message : String(error)}`, true);
4261
4379
  }
@@ -4462,7 +4580,9 @@ ${JSON.stringify(result, null, 2)}`);
4462
4580
  async function stopOutput(record) {
4463
4581
  let queued2 = 0;
4464
4582
  for (const [index, { messageId }] of encryptedMessages(record.mls, record.text, record.options, record).entries()) {
4465
- const status = (await endpoint.getOperation(encryptedOperation(record.mls, messageId)))?.status;
4583
+ let operation = await endpoint.getOperation(encryptedOperation(record.mls, messageId));
4584
+ while (operation?.resentAs) operation = await endpoint.getOperation(operation.resentAs);
4585
+ const status = operation?.status;
4466
4586
  if (status && status !== "succeeded" && status !== "rejected") queued2 = index + 1;
4467
4587
  }
4468
4588
  const { items: _items, errand: _errand, ...rest } = record;
@@ -4470,13 +4590,14 @@ ${JSON.stringify(result, null, 2)}`);
4470
4590
  else await endpoint.finishCompletion(record.turnId);
4471
4591
  }
4472
4592
  __name(stopOutput, "stopOutput");
4593
+ const publicationGuard = /* @__PURE__ */ __name((audience) => ({ automaticResend }) => (automaticResend ? authorize() : assertAudience(audience)).then(() => true, (error) => {
4594
+ if (audienceChanged(error)) throw error;
4595
+ return false;
4596
+ }), "publicationGuard");
4473
4597
  async function sendEncrypted(audience, messageId, text3, signal, replyAuthority) {
4474
4598
  const operationId = encryptedOperation(audience, messageId);
4475
4599
  if (!guards.has(operationId)) {
4476
- guards.set(operationId, endpoint.registerDeliveryGuard(operationId, () => assertAudience(audience).then(() => true, (error) => {
4477
- if (audienceChanged(error)) throw error;
4478
- return false;
4479
- })));
4600
+ guards.set(operationId, endpoint.registerDeliveryGuard(operationId, publicationGuard(audience)));
4480
4601
  }
4481
4602
  if (!await endpoint.getOperation(operationId)) await assertAudience(audience);
4482
4603
  signal?.throwIfAborted();
@@ -4516,7 +4637,7 @@ ${JSON.stringify(result, null, 2)}`);
4516
4637
  async function sendEncryptedItems(audience, items, signal) {
4517
4638
  if (!items.length) return;
4518
4639
  const media = await client.getFileMediaPipeline(options.fetch);
4519
- const sendOptions = { signal, beforePublish: /* @__PURE__ */ __name(() => assertAudience(audience).then(() => true), "beforePublish") };
4640
+ const sendOptions = { signal, beforePublish: publicationGuard(audience) };
4520
4641
  for (const item of items) {
4521
4642
  signal?.throwIfAborted();
4522
4643
  try {
@@ -4599,7 +4720,7 @@ ${JSON.stringify(result, null, 2)}`);
4599
4720
  session: turn.session,
4600
4721
  // §12.6: an errand turn's prompt is its errand; the owner's message is only its input's record.
4601
4722
  prompt: turn.errand ? errandPrompt(turn.errand) : [
4602
- ...turn.facts ? [...factsPrompt(turn.facts, kind), ""] : [],
4723
+ ...turn.facts ? [...factsPrompt(turn.facts, kind, displayName), ""] : [],
4603
4724
  ...turn.notes.flatMap((note) => [NOTE_OPEN, defused(note.text), NOTE_CLOSE, ""]),
4604
4725
  ...turn.contexts.flatMap((record) => [...contextPrompt(record), ""]),
4605
4726
  ...history.length ? ["Earlier in this conversation (context, not new requests):", ...history.map((message2) => line(message2)), "", "New messages:"] : turn.facts || turn.notes.length || turn.contexts.length ? ["New messages:"] : [],
@@ -5073,7 +5194,7 @@ ${JSON.stringify(result, null, 2)}`);
5073
5194
  ...record.origin ? { origin: record.origin } : {}
5074
5195
  }), "errandFacts");
5075
5196
  function errandPrompt({ record, target, origin, reports }) {
5076
- const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation.` : `the encrypted conversation ${target.room}. Send to it with communicate message_existing.`;
5197
+ const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation. If they are an agent, address them with @${target.contact} (their exact agent id): an agent's message reaches another only if it mentions it.` : `the encrypted conversation ${target.room}. Send to it with communicate message_existing. An agent's message reaches another agent there only if it mentions it, as @ and its exact agent id.`;
5077
5198
  const report = reports ? `your final answer is your report to your owner, posted in the conversation ${origin.room}.` : "your final answer is your report to your owner, and it stays on their machine.";
5078
5199
  return [
5079
5200
  `${ERRAND_OPEN} Your owner sends you on an errand, from the machine you run on. It is not a message in any conversation, and no one else sees it.`,
@@ -5283,7 +5404,10 @@ ${JSON.stringify(result, null, 2)}`);
5283
5404
  const known = rooms.get(data.conversationId);
5284
5405
  if (known && Array.isArray(merged.memberIds) && !merged.memberIds.includes(agentId)) loseMembership(known);
5285
5406
  }
5286
- if (mlsOn && (encrypted || Array.isArray(changes?.memberIds))) await checkAudience(rooms.get(data.conversationId));
5407
+ if (mlsOn && (encrypted || Array.isArray(changes?.memberIds))) {
5408
+ await checkAudience(rooms.get(data.conversationId));
5409
+ await noteAdmission(data.conversationId).catch(warn("room admission"));
5410
+ }
5287
5411
  if (encrypted) mlsAsks?.wake(data.conversationId);
5288
5412
  }
5289
5413
  __name(onRoomChanged, "onRoomChanged");
@@ -5393,9 +5517,12 @@ ${JSON.stringify(result, null, 2)}`);
5393
5517
  environmentId: connection.environmentId,
5394
5518
  streamUrl: connection.streamUrl,
5395
5519
  stateDirectory: join8(home, "endpoint"),
5396
- ...options.client ? { client: options.client } : {},
5520
+ ...options.client ?? RUNNER_CLIENT ? { client: options.client ?? RUNNER_CLIENT } : {},
5397
5521
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
5398
5522
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
5523
+ // The runner is the agent's home; Canon's relay is the connector home under its own installation.
5524
+ claimHome: true,
5525
+ ...options.takeOver ? { takeOver: true } : {},
5399
5526
  ...options.installationId ? { installationId: options.installationId } : {},
5400
5527
  ...mls ? { mlsDev: { configuration: mls.configuration, custody: mls.custody } } : {},
5401
5528
  ...options.endpoint ? { endpoint: options.endpoint } : {},
@@ -5410,6 +5537,7 @@ ${JSON.stringify(result, null, 2)}`);
5410
5537
  }
5411
5538
  agentId = identity.principalId;
5412
5539
  ownershipVersion = identity.ownershipVersion;
5540
+ installationId = identity.installationId;
5413
5541
  if (options.wake !== void 0) await client.configureWake(options.wake);
5414
5542
  mlsOn = !!(endpoint.mlsDevDevice && endpoint.mlsDevGroups);
5415
5543
  if (!mlsOn && (await endpoint.capabilities().catch(() => null))?.mls?.roomMode === "default") {
@@ -5488,6 +5616,7 @@ ${JSON.stringify(result, null, 2)}`);
5488
5616
  families: ["messages", "contacts", "runtime_turn"],
5489
5617
  onConnected: /* @__PURE__ */ __name(() => {
5490
5618
  heartbeat?.connect();
5619
+ if (mlsOn) void noteAdmissions().catch(warn("room admission"));
5491
5620
  if (reconnect) {
5492
5621
  for (const target of rooms.values()) if (target.turn) scheduleControl(target);
5493
5622
  }
@@ -5761,6 +5890,7 @@ export {
5761
5890
  readRelayCredential,
5762
5891
  writeRelayCredential,
5763
5892
  relayRunnerHome,
5893
+ RUNNER_CLIENT,
5764
5894
  runtimeDescriptor,
5765
5895
  createRunner,
5766
5896
  parseWakeConfiguration,