@canonmsg/agent-sdk 13.2.1 → 13.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.
@@ -2,8 +2,8 @@ import {
2
2
  createMlsDevCredentialVerifier,
3
3
  mlsDevDeviceHttpConfiguration,
4
4
  parseMlsDevClientConfiguration
5
- } from "./chunk-2NVLVPSG.js";
6
- import "./chunk-VIS5WXNH.js";
5
+ } from "./chunk-NIORQKUI.js";
6
+ import "./chunk-CUPSYLBV.js";
7
7
  export {
8
8
  createMlsDevCredentialVerifier,
9
9
  mlsDevDeviceHttpConfiguration,
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  MLS_SETUP_ACTIONS,
4
+ RUNNER_CLIENT,
4
5
  acpDriver,
5
6
  awaitErrand,
6
7
  checkAcp,
@@ -25,28 +26,30 @@ import {
25
26
  runMlsDevSetup,
26
27
  startRelayChannel,
27
28
  writeRelayCredential
28
- } from "./chunks/chunk-TJBG6NKF.js";
29
+ } from "./chunks/chunk-B6AOBWNZ.js";
29
30
  import {
30
31
  assertPrivateHome,
31
32
  runnerLockName
32
- } from "./chunks/chunk-POBMFS2K.js";
33
+ } from "./chunks/chunk-BQGXOAIW.js";
33
34
  import {
34
35
  CanonApiError,
35
36
  CanonClient,
36
37
  acquireLock,
38
+ clearPendingRegistration,
39
+ ensureAgentProfile,
37
40
  isProfileLocked,
41
+ loadPendingRegistrations,
38
42
  loadProfiles,
39
- readCliPackageVersion,
40
43
  resolveCanonRuntimeConnection,
41
44
  runCli,
42
45
  verifyCanonRuntimeConnection
43
- } from "./chunks/chunk-HTLKCUQG.js";
44
- import "./chunks/chunk-BPSFHEAA.js";
45
- import "./chunks/chunk-2NVLVPSG.js";
46
+ } from "./chunks/chunk-SS22I6HM.js";
47
+ import "./chunks/chunk-HFT75BNB.js";
48
+ import "./chunks/chunk-NIORQKUI.js";
46
49
  import {
47
- __name,
48
- isCanonClientVersion
49
- } from "./chunks/chunk-VIS5WXNH.js";
50
+ AGENT_HOME_ELSEWHERE,
51
+ __name
52
+ } from "./chunks/chunk-CUPSYLBV.js";
50
53
 
51
54
  // src/cli.ts
52
55
  import { realpathSync } from "node:fs";
@@ -54,8 +57,25 @@ import { homedir, tmpdir } from "node:os";
54
57
  import { dirname, join, resolve } from "node:path";
55
58
  import { pathToFileURL } from "node:url";
56
59
  import { parseArgs } from "node:util";
57
- var RUNNER_VERSION = readCliPackageVersion(import.meta.url);
58
- var RUNNER_CLIENT = isCanonClientVersion(RUNNER_VERSION) ? { kind: "runner", version: RUNNER_VERSION } : void 0;
60
+ var OWNER_DECLINED = "OWNER_DECLINED";
61
+ function exitCodeFor(error) {
62
+ const code = error?.code;
63
+ return code === AGENT_HOME_ELSEWHERE || code === OWNER_DECLINED ? 0 : 1;
64
+ }
65
+ __name(exitCodeFor, "exitCodeFor");
66
+ function stopThenExit(stop, code, exit = process.exit, boundMs = 15e3) {
67
+ let done = false;
68
+ const finish = /* @__PURE__ */ __name(() => {
69
+ if (!done) {
70
+ done = true;
71
+ clearTimeout(bound);
72
+ exit(code);
73
+ }
74
+ }, "finish");
75
+ const bound = setTimeout(finish, boundMs);
76
+ void Promise.resolve().then(stop).catch(() => void 0).finally(finish);
77
+ }
78
+ __name(stopThenExit, "stopThenExit");
59
79
  function once(handler) {
60
80
  let called = false;
61
81
  return (...args) => {
@@ -69,6 +89,8 @@ __name(once, "once");
69
89
  var HELP = `canon-agent: run a Canon agent on the unified runner
70
90
 
71
91
  Usage:
92
+ canon-agent run --driver <driver> --name <name> --owner-phone <+E.164> [--description <text>]
93
+ [--environment <id>] ... (the first run: registers the agent, then runs)
72
94
  canon-agent run --driver code --module <path> [--profile <name>] [--environment <id>]
73
95
  canon-agent run --driver claude [--cwd <dir>] [--model <id>] [--permission-mode <mode>]
74
96
  [--claude-path <path>] [--profile <name>] [--environment <id>]
@@ -99,6 +121,19 @@ command also takes --mls-dev-config <path>, an operator's own custody instead. W
99
121
  of these, the agent refuses every room, and where Canon encrypts every new conversation
100
122
  it does not start.
101
123
 
124
+ First run: with --name and --owner-phone, a run whose profile (--profile, default the
125
+ name) has no key asks the owner to approve the agent in Canon, waits up to 30 minutes,
126
+ saves the profile and runs. A run whose saved key Canon refuses asks the owner for a
127
+ new key for the same agent the same way; the agent keeps its conversations, devices and
128
+ connections. The request stays open while nobody answers: run the command again to keep
129
+ waiting. A declined request ends the run.
130
+
131
+ One home: an agent answers from one place at a time. A run on a new machine, or with an
132
+ empty $CANON_HOME, takes it over at start, and the run it replaced stops and exits 0, so
133
+ a supervisor that restarts only on failure leaves it stopped. That run does not take the
134
+ home back by itself; start it with --take-over to move the agent back there. While the
135
+ agent's owner has Canon's relay on for its MCP connection, no run starts (exit 0).
136
+
102
137
  Waking and sleeping: code, claude, codex and acp also take [--wake-config <path> |
103
138
  --wake-off] [--idle-timeout <duration>]. With --wake-config the agent registers, at
104
139
  each start, a public HTTPS callback for up to 32 rooms; Canon POSTs it a signed,
@@ -191,6 +226,8 @@ Flags:
191
226
  with a signed challenge first; the agent does not start if
192
227
  that fails. Without the flag a registration is left as it is.
193
228
  --wake-off Remove the agent's wake callback at start.
229
+ --take-over Move the agent's home back to this machine after a run
230
+ elsewhere took it over.
194
231
  --idle-timeout <d> Stop and exit 0 after this long with nothing to do: whole
195
232
  seconds, or 90s, 10m, 1h. Typing in a room does not reach the
196
233
  agent: a host that also receives typing hints decides itself
@@ -221,6 +258,9 @@ Flags:
221
258
  1 unless it was accepted (a note kept, a post sent, an errand's
222
259
  turn ended and any report it sent to --origin accepted).
223
260
  --profile <name> A profile in $CANON_HOME/agents.json (key and endpoints).
261
+ --name <name> run: the agent's name, to register it (with --owner-phone).
262
+ --owner-phone <phone> run: the owner's phone number in Canon, international format.
263
+ --description <text> run: what the agent does, shown to its owner (default: the name).
224
264
  --environment <id> canon-prod-v1 or canon-dev-v1 (default: CANON_ENVIRONMENT_ID).
225
265
  --help, -h Show this help.
226
266
  --version, -V Show the package version.
@@ -310,10 +350,11 @@ async function main() {
310
350
  if (trustedAgents.some((id) => !/^[\w.:-]{1,200}$/.test(id))) throw new UsageError("--trust-agent takes an agent id.");
311
351
  const instructions = values.instructions !== void 0 ? await loadInstructions(resolve(values.instructions)) : void 0;
312
352
  const wake = values["wake-off"] ? null : values["wake-config"] !== void 0 ? await loadWakeConfiguration(resolve(values["wake-config"])) : void 0;
353
+ const registered = await ensureRegistered(values, values.driver);
354
+ if (registered) values.profile = registered;
313
355
  const { environmentId, home, profile, apiKey } = await settings(values);
314
356
  const mls = await mlsDevice(values, environmentId, home);
315
- const baseUrl = process.env.CANON_BASE_URL || profile?.baseUrl;
316
- const streamUrl = process.env.CANON_STREAM_URL || profile?.streamUrl;
357
+ const { apiBaseUrl: baseUrl, streamUrl } = connectionFor(environmentId, profile);
317
358
  let driver;
318
359
  if (values.driver === "claude") {
319
360
  driver = claudeDriver({
@@ -366,8 +407,8 @@ async function main() {
366
407
  ...team,
367
408
  ...trustedAgents.length ? { trustedAgents } : {},
368
409
  ...values["allow-session-for-destructive"] ? { sessionApprovalForDestructive: true } : {},
369
- ...RUNNER_CLIENT ? { client: RUNNER_CLIENT } : {},
370
410
  ...wake !== void 0 ? { wake } : {},
411
+ ...values["take-over"] ? { takeOver: true } : {},
371
412
  // Idle: a clean stop and exit 0, so a supervisor that restarts only on failure leaves the agent stopped until it is woken.
372
413
  ...idleTimeoutMs !== void 0 ? { idleTimeoutMs, onIdle: once(() => {
373
414
  console.error(`canon-agent: nothing to do for ${values["idle-timeout"]}; stopping.`);
@@ -378,7 +419,7 @@ async function main() {
378
419
  }) } : {},
379
420
  onFatal: once((error) => {
380
421
  console.error(`canon-agent: ${error.message}`);
381
- void runner.stop().catch(() => void 0).finally(() => process.exit(1));
422
+ stopThenExit(() => runner.stop(), exitCodeFor(error));
382
423
  })
383
424
  });
384
425
  stopOnSignals(() => runner.stop());
@@ -394,8 +435,7 @@ __name(main, "main");
394
435
  async function runRelay(values, team) {
395
436
  const { environmentId, home, profile } = await settings(values, { key: false });
396
437
  const relay = await loadRelayConfiguration(values["relay-config"], { environmentId });
397
- const baseUrl = process.env.CANON_BASE_URL || profile?.baseUrl;
398
- const streamUrl = process.env.CANON_STREAM_URL || profile?.streamUrl;
438
+ const { apiBaseUrl: baseUrl, streamUrl } = connectionFor(environmentId, profile);
399
439
  let current;
400
440
  let latest;
401
441
  let switching = Promise.resolve();
@@ -446,10 +486,12 @@ async function runRelay(values, team) {
446
486
  relay: true,
447
487
  installationId: stored2.installationId,
448
488
  ...team,
449
- ...RUNNER_CLIENT ? { client: RUNNER_CLIENT } : {},
450
489
  onFatal: once((error) => {
451
490
  console.error(`canon-agent: ${error.message}`);
452
- void Promise.resolve().then(() => channel.close()).then(() => runner.stop()).catch(() => void 0).finally(() => process.exit(1));
491
+ stopThenExit(async () => {
492
+ await channel.close();
493
+ await runner.stop();
494
+ }, 1);
453
495
  })
454
496
  });
455
497
  try {
@@ -522,11 +564,85 @@ function parse() {
522
564
  cancel: { type: "string" },
523
565
  "wake-config": { type: "string" },
524
566
  "wake-off": { type: "boolean" },
525
- "idle-timeout": { type: "string" }
567
+ "idle-timeout": { type: "string" },
568
+ "take-over": { type: "boolean" },
569
+ name: { type: "string" },
570
+ "owner-phone": { type: "string" },
571
+ description: { type: "string" }
526
572
  }
527
573
  });
528
574
  }
529
575
  __name(parse, "parse");
576
+ var REGISTRATION_WAIT_MS = 30 * 6e4;
577
+ var REGISTRATION_CLIENT_TYPES = { claude: "claude-code", codex: "codex" };
578
+ async function ensureRegistered(values, driver) {
579
+ const name = values.name?.trim(), ownerPhone = values["owner-phone"]?.trim();
580
+ if (!name && !ownerPhone && values.description === void 0) return void 0;
581
+ if (!name || !ownerPhone) throw new UsageError("--name and --owner-phone go together (and --description with them).");
582
+ if (process.env.CANON_API_KEY) throw new UsageError("--name and --owner-phone keep the agent's key in a profile: unset CANON_API_KEY.");
583
+ const environmentId = values.environment || process.env.CANON_ENVIRONMENT_ID;
584
+ if (!environmentId) throw new Error("CANON_ENVIRONMENT_ID (or --environment) is required.");
585
+ const home = resolve(process.env.CANON_HOME || join(homedir(), ".canon"));
586
+ await assertPrivateHome(home);
587
+ const profile = values.profile || name;
588
+ const saved = loadProfiles(home)[profile];
589
+ if (saved && saved.environmentId !== environmentId) throw new Error(`Profile "${profile}" belongs to ${saved.environmentId}, not ${environmentId}.`);
590
+ const connection = connectionFor(environmentId, saved);
591
+ const agentName = saved?.agentName ?? name;
592
+ const register = /* @__PURE__ */ __name(async (reconnect) => {
593
+ const pending = loadPendingRegistrations()[profile];
594
+ const ask = reconnect || pending?.requestedAgentId ? `a new key for ${agentName}` : agentName;
595
+ const waiting = /* @__PURE__ */ __name(() => console.error(`canon-agent: ask the owner of ${ownerPhone} to approve ${ask} in Canon. Waiting up to 30 minutes; the request stays open until they answer, and running this command again keeps waiting.`), "waiting");
596
+ if (pending && !pending.credentialAgentId) waiting();
597
+ const asked = reconnect || !!pending || !saved;
598
+ const progress = await ensureAgentProfile({
599
+ profile,
600
+ reconnect,
601
+ waitMs: REGISTRATION_WAIT_MS,
602
+ onSubmitted: waiting,
603
+ connection,
604
+ input: {
605
+ name,
606
+ description: values.description?.trim() || name,
607
+ ownerPhone,
608
+ clientType: REGISTRATION_CLIENT_TYPES[driver] ?? "generic",
609
+ developerInfo: `canon-agent run --driver ${driver}`
610
+ }
611
+ });
612
+ switch (progress.status) {
613
+ case "approved":
614
+ if (asked) console.error(`canon-agent: ${progress.agentName} is approved; its key is saved in profile "${profile}".`);
615
+ return progress.apiKey;
616
+ case "pending":
617
+ throw new Error(`Nobody has answered the request for ${ask} yet. It stays open: run this command again to keep waiting.`);
618
+ case "rejected":
619
+ clearPendingRegistration(profile);
620
+ throw Object.assign(new Error(`The owner declined ${ask}. Nothing was saved; run this command again to ask again.`), { code: OWNER_DECLINED });
621
+ case "credential-expired":
622
+ clearPendingRegistration(profile);
623
+ throw new Error(`The owner approved ${ask}, but its key was not collected within 48 hours and Canon removed it. Run this command again to ask again.`);
624
+ case "credential-delivered":
625
+ clearPendingRegistration(profile);
626
+ throw new Error(`The key for ${ask} was already collected elsewhere, so this run cannot have it. Run this command again to ask again.`);
627
+ }
628
+ }, "register");
629
+ const apiKey = await register(false);
630
+ try {
631
+ await new CanonClient(apiKey, resolveCanonRuntimeConnection(connection).apiBaseUrl).getAgentMe();
632
+ return profile;
633
+ } catch (error) {
634
+ if (!(error instanceof CanonApiError) || error.status !== 401) throw error;
635
+ }
636
+ console.error(`canon-agent: Canon no longer accepts ${agentName}'s saved key.`);
637
+ await register(true);
638
+ return profile;
639
+ }
640
+ __name(ensureRegistered, "ensureRegistered");
641
+ function connectionFor(environmentId, profile) {
642
+ const apiBaseUrl = process.env.CANON_BASE_URL || profile?.baseUrl, streamUrl = process.env.CANON_STREAM_URL || profile?.streamUrl;
643
+ return { environmentId, ...apiBaseUrl ? { apiBaseUrl } : {}, ...streamUrl ? { streamUrl } : {} };
644
+ }
645
+ __name(connectionFor, "connectionFor");
530
646
  async function settings(values, { key = true } = {}) {
531
647
  const environmentId = values.environment || process.env.CANON_ENVIRONMENT_ID;
532
648
  if (!environmentId) throw new Error("CANON_ENVIRONMENT_ID (or --environment) is required.");
@@ -564,11 +680,7 @@ async function mlsSetup(values) {
564
680
  let client;
565
681
  let lock;
566
682
  try {
567
- const connection = resolveCanonRuntimeConnection({
568
- environmentId,
569
- apiBaseUrl: process.env.CANON_BASE_URL || profile?.baseUrl,
570
- streamUrl: process.env.CANON_STREAM_URL || profile?.streamUrl
571
- });
683
+ const connection = resolveCanonRuntimeConnection(connectionFor(environmentId, profile));
572
684
  await verifyCanonRuntimeConnection(connection);
573
685
  client = new CanonClient(apiKey, connection.apiBaseUrl, {
574
686
  environmentId,
@@ -660,5 +772,10 @@ __name(stopOnSignals, "stopOnSignals");
660
772
  runCli(import.meta.url, main, (error) => {
661
773
  console.error(`canon-agent: ${error instanceof Error ? error.message : String(error)}`);
662
774
  if (error instanceof UsageError) console.error(HELP.split("\n\nFlags:")[0]);
663
- process.exit(1);
775
+ process.exit(error instanceof UsageError ? 1 : exitCodeFor(error));
664
776
  }, { name: "canon-agent", help: HELP });
777
+ export {
778
+ ensureRegistered,
779
+ exitCodeFor,
780
+ stopThenExit
781
+ };
package/dist/index.js CHANGED
@@ -26,8 +26,8 @@ import {
26
26
  runMlsDevSetup,
27
27
  runtimeDescriptor,
28
28
  startRelayChannel
29
- } from "./chunks/chunk-TJBG6NKF.js";
30
- import "./chunks/chunk-POBMFS2K.js";
29
+ } from "./chunks/chunk-B6AOBWNZ.js";
30
+ import "./chunks/chunk-BQGXOAIW.js";
31
31
  import {
32
32
  CanonApiError,
33
33
  DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
@@ -52,10 +52,10 @@ import {
52
52
  toAnthropicImageBlock,
53
53
  toAnthropicImageBlocksWithinBudget,
54
54
  uploadMediaFile
55
- } from "./chunks/chunk-HTLKCUQG.js";
56
- import "./chunks/chunk-BPSFHEAA.js";
57
- import "./chunks/chunk-2NVLVPSG.js";
58
- import "./chunks/chunk-VIS5WXNH.js";
55
+ } from "./chunks/chunk-SS22I6HM.js";
56
+ import "./chunks/chunk-HFT75BNB.js";
57
+ import "./chunks/chunk-NIORQKUI.js";
58
+ import "./chunks/chunk-CUPSYLBV.js";
59
59
  export {
60
60
  CanonApiError,
61
61
  ConnectorRelayError,
@@ -0,0 +1,80 @@
1
+ import { type CanonClient, type CommunicateInput } from '../core/index.js';
2
+ export interface VerbToolDefinition {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: Record<string, unknown>;
6
+ }
7
+ export interface VerbToolResult {
8
+ content: Array<{
9
+ type: 'text';
10
+ text: string;
11
+ }>;
12
+ isError?: boolean;
13
+ }
14
+ export declare const CANON_COMMUNICATE_TOOL_NAME = "communicate";
15
+ export declare const CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
16
+ /**
17
+ * What each action takes: the fields it needs, then those it may have. The legacy aliases (start_direct,
18
+ * create_group, manage_group_members) take their action's fields; the parser still accepts them, and no schema offers them.
19
+ */
20
+ declare const ACTION_FIELDS: {
21
+ readonly list_conversations: {
22
+ readonly required: readonly [];
23
+ readonly optional: readonly ["limit"];
24
+ };
25
+ readonly discover_agents: {
26
+ readonly required: readonly [];
27
+ readonly optional: readonly ["query", "limit", "cursor"];
28
+ };
29
+ readonly message_existing: {
30
+ readonly required: readonly ["conversationId", "text"];
31
+ readonly optional: readonly ["messageId"];
32
+ };
33
+ readonly start_conversation: {
34
+ readonly required: readonly ["principalId", "text"];
35
+ readonly optional: readonly ["selection", "messageId"];
36
+ };
37
+ readonly create_conversation: {
38
+ readonly required: readonly ["memberIds"];
39
+ readonly optional: readonly ["name"];
40
+ };
41
+ readonly manage_participants: {
42
+ readonly required: readonly ["conversationId", "userId", "operation"];
43
+ readonly optional: readonly [];
44
+ };
45
+ readonly leave_conversation: {
46
+ readonly required: readonly ["conversationId"];
47
+ readonly optional: readonly [];
48
+ };
49
+ readonly forward_message: {
50
+ readonly required: readonly ["sourceConversationId", "targetConversationId", "messageId"];
51
+ readonly optional: readonly ["text"];
52
+ };
53
+ readonly share_contact: {
54
+ readonly required: readonly ["conversationId", "contactUserId"];
55
+ readonly optional: readonly ["text", "messageId"];
56
+ };
57
+ };
58
+ export type CanonCommunicateAction = keyof typeof ACTION_FIELDS;
59
+ /** The actions a binding offers, each with the fields it shows for it (an action's required fields always). */
60
+ export type CommunicateShape = Readonly<Partial<Record<CanonCommunicateAction, readonly string[]>>>;
61
+ /** Every action with every field: the complete communication surface. */
62
+ export declare const CANON_COMMUNICATE_SHAPE: CommunicateShape;
63
+ /**
64
+ * One flat object schema for `communicate`: `action` names the action, and every other field is optional and says which
65
+ * actions take it. Claude reads a tool's schema as one object, so a top-level `oneOf` showed it only its first
66
+ * alternative (the driver trial on Dev, 2026-11-09); the parser checks each combination instead.
67
+ */
68
+ export declare function communicateInputSchema(shape: CommunicateShape, textMaxLength?: number | null): Record<string, unknown>;
69
+ /**
70
+ * The complete model-facing communication surface. It is exported separately
71
+ * from the in-conversation verb list so each runtime can opt into it.
72
+ */
73
+ export declare function canonCommunicateToolDefinition(name?: string): VerbToolDefinition;
74
+ /**
75
+ * Runtime validation for bindings that dispatch directly. The schema is flat, so this is where a combination is checked:
76
+ * an action the binding does not offer, a field the action needs, or one it does not take, is refused by name.
77
+ */
78
+ export declare function parseCommunicateToolInput(value: unknown, shape?: CommunicateShape): CommunicateInput;
79
+ export declare function executeCanonCommunicateTool(client: CanonClient, args: unknown, shape?: CommunicateShape): Promise<VerbToolResult>;
80
+ export {};
@@ -0,0 +1 @@
1
+ export { CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS, CANON_COMMUNICATE_SHAPE, CANON_COMMUNICATE_TOOL_NAME, canonCommunicateToolDefinition, communicateInputSchema, executeCanonCommunicateTool, parseCommunicateToolInput, type CanonCommunicateAction, type CommunicateShape, type VerbToolDefinition, type VerbToolResult, } from './communication-tool.js';
@@ -118,3 +118,28 @@ export declare const ENDPOINT_ID_PATTERN: RegExp;
118
118
  export declare const ENDPOINT_ACTION_PATTERN: RegExp;
119
119
  export declare function isEndpointOperationRequest(value: unknown): value is EndpointOperationRequest;
120
120
  export declare function isEndpointOperationReceipt(value: unknown): value is EndpointOperationReceipt;
121
+ /**
122
+ * An agent answers from one home at a time: Canon's relay for its MCP connection while the owner has the relay on,
123
+ * otherwise its runner, the installation that started most recently (`initialize_endpoint` moves it there). Everything
124
+ * the other home sends is refused with `AGENT_HOME_ELSEWHERE`, so no message gets two answers. It is read from the
125
+ * agent's own document alone (`users/{agentId}`), so every hot path can check it without another read.
126
+ *
127
+ * `since` (ms, Canon's clock): this home answers for work from here, for its work floor and in the refusal's text. Nothing
128
+ * Canon accepted before it is that home's work, since the home before may have answered it. For the relay, its approval.
129
+ * For a runner, the later of `installedAt` and the relay's turn-off, after which the runner it replaced is the home again
130
+ * by itself. Despite its name, `installedAt` holds where its installation answers for work from (its baseline's cutover):
131
+ * a first installation's approval, a later one's take-over. Older markers hold a first installation's first start, and
132
+ * are read as they are.
133
+ */
134
+ export declare const AGENT_HOME_ELSEWHERE = "AGENT_HOME_ELSEWHERE";
135
+ export type AgentHome = {
136
+ kind: 'connector';
137
+ since?: number;
138
+ } | {
139
+ kind: 'runner';
140
+ installationId?: string;
141
+ since?: number;
142
+ };
143
+ export declare function readAgentHome(agentConfig: unknown): AgentHome;
144
+ /** The one refusal both homes give the other: what is true now, and what the owner or operator can do. */
145
+ export declare function agentHomeElsewhereMessage(agentName: string | undefined, home: AgentHome): string;
@@ -1,5 +1,9 @@
1
1
  export declare const MLS_DEV_KEY_PACKAGES_MAX_BYTES = 32768;
2
2
  export declare const MLS_DEV_KEY_PACKAGE_TLS_MAX_BYTES = 16384;
3
+ /** How long a claim holds its recipient's slot before another member's device may take it over (mls-rooms-by-default.md
4
+ * §2.4). On Dev from 2026-10-20 to 11-08, 113 Adds were accepted a median 2.4 s after their claim, 99% within 9.4 s, and
5
+ * the slowest in 61.5 s: two minutes is about twice the slowest, and a dead claimant holds a device off no longer. */
6
+ export declare const MLS_DEV_KEY_PACKAGE_CLAIM_LEASE_MS: number;
3
7
  export interface MlsDevKeyPackagePublishRequest {
4
8
  readonly version: 1;
5
9
  readonly requestId: string;
@@ -8,10 +8,6 @@ export interface ResumableMediaUploadResult {
8
8
  status?: 'processing' | 'ready' | 'failed';
9
9
  attachment: MediaAttachment;
10
10
  }
11
- /**
12
- * Agent-facing ergonomics over the shared endpoint engine. Authentication and
13
- * operator registration remain outside the conversation protocol.
14
- */
15
11
  export declare class CanonClient {
16
12
  private baseUrl;
17
13
  private apiKey;
@@ -30,6 +30,14 @@ export interface CanonClientEndpointOptions {
30
30
  mediaDirectory?: string;
31
31
  /** Operator inspection/import does not automatically publish queued operations. */
32
32
  autoDrain?: boolean;
33
+ /**
34
+ * This client is the agent's runner, and its installation is the agent's home: a new installation takes the home at
35
+ * start; one that lost it to another refuses to start (`AGENT_HOME_ELSEWHERE`) unless `takeOver` moves it back, so two
36
+ * supervised machines never take it from each other in turn. Other clients (a script, a CLI step) never move the home.
37
+ */
38
+ claimHome?: boolean;
39
+ /** With `claimHome`: move the home back to this installation after another one took it. */
40
+ takeOver?: boolean;
33
41
  /** Replicated cloud hosts supply operator-owned durable byte storage. */
34
42
  mediaBytes?: EndpointMediaBytes<string>;
35
43
  /** Test seam for the client's out-of-protocol HTTP (identity, profile); production leaves it unset. */
@@ -55,5 +55,5 @@ export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, run
55
55
  export type { CliMetadata, RunCliMetadataOptions, } from './cli-metadata.js';
56
56
  export { CANON_VERB_NAMES, CANON_VERB_NAMESPACE, CANON_VERBS_SCHEMA_ID, CANON_VERBS_SCHEMA_VERSION, CANON_VERB_WIRE_SCHEMA_ID, CANON_VERB_WIRE_SCHEMA_VERSION, CANON_CARD_SCHEMA_ID, VERB_LIMITS, VERB_RATE_LIMITS, VERB_WIRE_ENVELOPE_FIELDS, canonVerbToolName, findVerbByteLimitViolations, findVerbWireEnvelopeViolations, getVerbResultSchema, mergeVerbWireToIntent, normalizeVerbNativeMetadata, projectVerbIntentToWire, type CanonVerbName, type CanonVerbWireEnvelope, type CanonVerbWireRequest, type CanonVerbWireResponse, type CanonVerbJsonSchema, type SendToInput, type SendToResult, type VerbMessageOptions, type VerbNativeMetadata, type VerbWireAccepted, type VerbWireBody, } from '../backend-contracts/index.js';
57
57
  export { DEFAULT_DISCOVER_AGENTS_LIMIT, MAX_DISCOVER_AGENTS_LIMIT, MAX_DISCOVER_AGENTS_QUERY_LENGTH, type CanonAgentDirectoryEntry, type DiscoverAgentsInput, type DiscoverAgentsResult, } from '../backend-contracts/index.js';
58
- export { parseDirectConversationSelection, readCommunicationRule, type CommunicateInput, type CommunicateResult, type CommunicationRule, type DirectConversationSelection, type StartDirectInput, type StartDirectResult, } from '../backend-contracts/index.js';
58
+ export { MLS_DEV_TEXT_MAX_BYTES, parseDirectConversationSelection, readCommunicationRule, type CommunicateInput, type CommunicateResult, type CommunicationRule, type DirectConversationSelection, type StartDirectInput, type StartDirectResult, } from '../backend-contracts/index.js';
59
59
  export { canonMediaDownloadUrl, sanitizeDownloadFileName, buildMediaContentDisposition, getStoredFileExtension } from '../backend-contracts/index.js';
@@ -3,7 +3,10 @@ export interface EnsureAgentProfileOptions extends Omit<ResumeRegistrationOption
3
3
  /** The local profile store owns its stable localRegistrationId. */
4
4
  input: Omit<ResumeRegistrationOptions['input'], 'localRegistrationId'>;
5
5
  profile: string;
6
- /** Explicitly request a new registration even if a usable local credential exists. */
6
+ /**
7
+ * Ask the owner for a new key for this profile's agent (its saved agent ID, or `input.requestedAgentId`), even if a
8
+ * usable local credential exists. Approval only replaces the key: the agent keeps its rooms, devices and connections.
9
+ */
7
10
  reconnect?: boolean;
8
11
  }
9
12
  /** Reuse an environment-bound local profile, or resume its owner-approved registration. */
@@ -1,4 +1,4 @@
1
- import type { AgentOrigin, AgentOriginDeclaration, ArtifactContext } from '../backend-contracts/index.js';
1
+ import type { AgentHome, AgentOrigin, AgentOriginDeclaration, ArtifactContext } from '../backend-contracts/index.js';
2
2
  import type { CanonControlValue, CanonRuntimeStreamingMode, CanonRuntimeDetailTier, CanonRuntimeVisibility, CanonRuntimeCommandDescriptor } from '../backend-contracts/index.js';
3
3
  export type { AgentOrigin, AgentOriginDeclaration, CanonControlValue, CanonRuntimeStreamingMode, CanonRuntimeDetailTier, CanonRuntimeVisibility, CanonRuntimeActionAvailability, CanonRuntimeActionPlacement, CanonRuntimeActionCategory, CanonRuntimePrimitiveId, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionDescriptor, CanonRuntimeCommandDescriptor, } from '../backend-contracts/index.js';
4
4
  import type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
@@ -479,6 +479,8 @@ export interface AgentContext {
479
479
  ownershipVersion: number;
480
480
  /** Server-created new-principal marker; initialization still binds the installation atomically. */
481
481
  endpointBootstrapEligible?: boolean;
482
+ /** Where the agent answers from: Canon's relay, or the runner installation that started last (absent from older Canon). */
483
+ home?: AgentHome;
482
484
  canonContactId?: string;
483
485
  displayName?: string;
484
486
  avatarUrl?: string | null;
@@ -72,6 +72,11 @@ export interface EndpointOperation {
72
72
  /** Local-only binding of a withheld start's text; retained after the opener is sent or its payload is erased. */
73
73
  openerFingerprint?: string;
74
74
  receipt?: EndpointOperationReceipt;
75
+ /** A refused encrypted send this device encrypted again: the operation that replaced it, which retries and waits
76
+ * follow (mls-dev-groups.ts, resendText). */
77
+ resentAs?: string;
78
+ /** Local successor made after Canon refused an encrypted send, with readers and sender admission rechecked. */
79
+ automaticResend?: boolean;
75
80
  attempts: number;
76
81
  attempted: boolean;
77
82
  nextAttemptAt?: number;
@@ -148,10 +153,15 @@ export interface Endpoint {
148
153
  submit(intent: EndpointIntent, options?: EndpointExecuteOptions): Promise<EndpointOperation>;
149
154
  execute<T = unknown>(intent: EndpointIntent, options?: EndpointExecuteOptions): Promise<EndpointOperationReceipt<T>>;
150
155
  executeResult<T = unknown>(intent: EndpointIntent, options?: EndpointExecuteOptions): Promise<T>;
151
- /** reconcileOnly reads the server receipt; it never dispatches an operation. */
156
+ /** reconcileOnly reads and records the server receipt; it never dispatches or encrypts again. An operation encrypted
157
+ * again (`resentAs`) answers with the one that replaced it. An ordinary retry of a send Canon refused one change behind
158
+ * encrypts it again, by the rule its first refusal had, when a reconcile only recorded the refusal. */
152
159
  retry<T = unknown>(operationId: string, options?: {
153
160
  reconcileOnly?: boolean;
154
161
  }): Promise<EndpointOperationReceipt<T>>;
162
+ /** A person's Retry of an encrypted message Canon refused: encrypts it again, from this device's copy, for its room as
163
+ * it is now, under a new operation that replaces the refused one. Returns the operation to retry. */
164
+ resend(operationId: string): Promise<EndpointOperation>;
155
165
  drain(options?: {
156
166
  limit?: number;
157
167
  }): Promise<void>;
@@ -192,8 +202,9 @@ export interface Endpoint {
192
202
  cancelMessageGroup(id: string): Promise<void>;
193
203
  /** Runtime-specific publication policy; closures are never serialized.
194
204
  * Return false to defer, or throw a nonretryable EndpointError to retire an
195
- * unsent operation. An attempted operation still needs its server receipt. */
196
- registerDeliveryGuard(id: string, allow: () => Promise<boolean>): () => void;
205
+ * unsent operation. An attempted operation still needs its server receipt.
206
+ * Automatic resends retain the policy; only an audience-specific check may use the revalidated resend context. */
207
+ registerDeliveryGuard(id: string, allow: EndpointDeliveryGuard): () => void;
197
208
  /** Keep a start's opening line that Canon did not store, and send it once the room and the request allow (§2a.13). */
198
209
  deferOpener(input: {
199
210
  conversationId: string;
@@ -216,6 +227,21 @@ export interface Endpoint {
216
227
  /** Immutable operator-reviewed or server-proven cutover for new rooms/admissions. */
217
228
  configureRecoveryBaseline(config: EndpointRecoveryBaseline): Promise<void>;
218
229
  getRecoveryBaseline(): Promise<EndpointRecoveryBaseline | undefined>;
230
+ /**
231
+ * Work here starts at `cutoverAt` (Canon's clock), where this installation's turn as its agent's home began: a
232
+ * take-over, or Canon's relay turned off. Every input Canon accepted before it is history, never work, whether it is
233
+ * already in the inbox or still to come off a room's feed, since the home before may have answered it. Only an input a
234
+ * journaled output answers keeps it. The floor only ever moves later.
235
+ */
236
+ startWorkAt(cutoverAt: string): Promise<void>;
237
+ /**
238
+ * A runner moving its agent's home here, under a new `initialize_endpoint` operation each time, starts work from
239
+ * Canon's answer (`startWorkAt`). Only this call sends it. One that fails, or that a process left unanswered when it
240
+ * died, is never sent again (the latter is settled when the store next opens, before anything drains), so no later
241
+ * start makes a take-over without its operator. If Canon took the home all the same, the next start reads that from the
242
+ * agent's home and starts work from there.
243
+ */
244
+ takeOverHome(): Promise<void>;
219
245
  refreshRecovery(): Promise<void>;
220
246
  planRecoveryMigration(baseline: EndpointRecoveryBaseline, conversationIds?: string[]): Promise<EndpointRecoveryMigration>;
221
247
  importRecoveryMigration(manifest: EndpointRecoveryMigration, reviewedSha256: string): Promise<EndpointMigrationReceipt>;
@@ -266,5 +292,8 @@ export interface Endpoint {
266
292
  logout(): Promise<void>;
267
293
  close(): Promise<void>;
268
294
  }
295
+ export type EndpointDeliveryGuard = (context: {
296
+ automaticResend: boolean;
297
+ }) => Promise<boolean>;
269
298
  export declare function mergeConversationSnapshot<T extends EndpointConversation>(current: T | undefined, incoming: T): T;
270
299
  export declare function createEndpoint(options: EndpointOptions): Endpoint;
@@ -1,4 +1,4 @@
1
- import type { Endpoint } from './engine.js';
1
+ import type { Endpoint, EndpointDeliveryGuard } from './engine.js';
2
2
  import { type EndpointStore } from './store.js';
3
3
  export interface EndpointMediaDescriptor {
4
4
  mimeType: string;
@@ -64,7 +64,7 @@ export interface EndpointMediaProgress {
64
64
  export interface EndpointMediaSendOptions {
65
65
  signal?: AbortSignal;
66
66
  onProgress?: (progress: EndpointMediaProgress) => void;
67
- beforePublish?: () => Promise<boolean>;
67
+ beforePublish?: EndpointDeliveryGuard;
68
68
  }
69
69
  export interface EndpointMediaInput<Source> {
70
70
  conversationId: string;
@@ -48,6 +48,8 @@ export interface MlsDevAdmissionHold {
48
48
  readonly waitingPrincipalIds: readonly string[];
49
49
  /** This device joined after the room's first message, so its history starts later (mls-rooms-by-default.md §2.7). */
50
50
  readonly missedEarlier?: true;
51
+ /** The room's group exists and this device is not in it: only a device already there can add it (§2.4). */
52
+ readonly outside?: true;
51
53
  }
52
54
  export interface MlsDevCurrentAudience {
53
55
  readonly version: 1;
@@ -166,6 +168,8 @@ export interface MlsDevGroupBridge {
166
168
  /** Reads and applies the room's conversation snapshot from Canon, for openings of members admitted since. */
167
169
  readRoom?(conversationId: string): Promise<unknown>;
168
170
  }
171
+ /** How many times a refused send is encrypted again by itself before it shows as not sent. */
172
+ export declare const MLS_DEV_RESEND_MAX = 3;
169
173
  /** Internal attachment: the engine supplies its existing queue, receipt reducer,
170
174
  * event/recovery intake, store and lifetime. Native work never runs inside tx. */
171
175
  export declare function createMlsDevGroups(options: MlsDevDeviceClientOptions, context: MlsDevDeviceClientContext, keyPackages: MlsDevKeyPackageClient | undefined, bridge: MlsDevGroupBridge): {
@@ -175,6 +179,8 @@ export declare function createMlsDevGroups(options: MlsDevDeviceClientOptions, c
175
179
  * encrypted room (mls-payload-v3.md §4). Items and cards are not forwarded yet. */
176
180
  forwardText(value: unknown, operationId: string, guardId?: string): Promise<EndpointOperation>;
177
181
  sendText(value: unknown, operationId: string, guardId?: string): Promise<EndpointOperation>;
182
+ /** A person's Retry of a text or item Canon refused: encrypted again for the room as it is now (`resendText`). */
183
+ resendText(operation: EndpointOperation): Promise<EndpointOperation | undefined>;
178
184
  /** `sendText` for a person's composer: in a held room the text is kept, not refused (undefined), until `sendHeldTexts`. */
179
185
  holdText(value: unknown, operationId: string): Promise<EndpointOperation | undefined>;
180
186
  sendHeldTexts(conversationId: string): Promise<{
@@ -196,6 +202,8 @@ export declare function createMlsDevGroups(options: MlsDevDeviceClientOptions, c
196
202
  syncText(conversationId: string, options?: EndpointSyncOptions): Promise<EndpointSyncPage>;
197
203
  isTextReady: (conversationId: string) => Promise<boolean>;
198
204
  readAcceptedReceipt(operation: EndpointOperation): Promise<EndpointOperationReceipt | undefined>;
199
- settleReceipt(operation: EndpointOperation, receipt: EndpointOperationReceipt): Promise<boolean>;
205
+ settleReceipt(operation: EndpointOperation, receipt: EndpointOperationReceipt, options?: {
206
+ reconcileOnly?: boolean;
207
+ }): Promise<boolean>;
200
208
  };
201
209
  export {};