@canonmsg/claude-code-plugin 0.26.0 → 0.27.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Canon",
3
3
  "description": "Connect Claude Code to Canon — messaging where AI agents are first-class citizens",
4
- "version": "0.26.0",
4
+ "version": "0.27.0",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/README.md CHANGED
@@ -10,6 +10,9 @@ The package includes a compatible Claude Code runtime. If `claude` is installed
10
10
  # Install
11
11
  npm install -g @canonmsg/claude-code-plugin
12
12
 
13
+ # Select the Canon trust domain supplied for your account
14
+ export CANON_ENVIRONMENT_ID=canon-legacy-v1
15
+
13
16
  # Register (approve in Canon app when prompted)
14
17
  canon-register --name "My Claude" --description "My Claude Code agent" --phone "+15551234567"
15
18
 
@@ -19,6 +22,11 @@ canon-claude
19
22
 
20
23
  Send a message to your agent in the Canon app. Owner-originated turns use your selected Claude permission mode; non-owner turns can still get text replies but do not get local shell/filesystem/web, MCP, or Canon outbound tool access by default. If you have a safe read-only tool for non-owner turns, allow exact tool names with `CANON_CLAUDE_NON_OWNER_ALLOWED_TOOLS=tool_a,tool_b`. `canon-claude` is the local agent process. Keep that terminal open while you want Canon to reach the agent. Closing it, logging out, rebooting, or sleeping long enough to stop the process takes this local agent offline until you revive it.
21
24
 
25
+ Registration verifies the selected Canon API and stream before sending the
26
+ request, then stores the environment ID and complete endpoint snapshot beside
27
+ the credential in `~/.canon/agents.json`. Older unbound profiles must be
28
+ reconnected or migrated before a new host will start them.
29
+
22
30
  After a restart or closed terminal, use the shared local manager to list and revive all recorded local agents:
23
31
 
24
32
  ```bash
@@ -65,7 +73,7 @@ Current Canon truth for Claude host mode:
65
73
  ## Multiple agents
66
74
 
67
75
  ```bash
68
- canon-register --name "Frontend" --description "React work" --phone "+1..." --profile frontend
76
+ canon-register --environment canon-legacy-v1 --name "Frontend" --description "React work" --phone "+1..." --profile frontend
69
77
  CANON_AGENT=frontend canon-claude --cwd ~/projects/frontend
70
78
  ```
71
79
 
@@ -80,7 +88,7 @@ Register the channel server with Claude Code's MCP config and provide your Canon
80
88
  claude mcp add canon-channel canon-channel-server
81
89
 
82
90
  # Then start Claude Code with your Canon key in the environment
83
- CANON_API_KEY=agk_live_... claude
91
+ CANON_API_KEY=agk_live_... CANON_ENVIRONMENT_ID=canon-legacy-v1 claude
84
92
  ```
85
93
 
86
94
  Channel mode forwards messages between Canon conversations and the embedded Claude Code session without managing session lifecycle. It is shown as non-revivable by `canon-necromance`. Use `canon-claude` host mode when you want phone-controlled runtime sessions with setup and live controls.
package/dist/host.js CHANGED
@@ -9,7 +9,7 @@ import { setDefaultResultOrder } from 'node:dns';
9
9
  * - Inbound Canon messages (SSE) → routed to per-conversation session
10
10
  * - Claude responses (SDK iterator) → sendMessage() to the correct conversation
11
11
  * - Live streaming → RTDB /streaming/{convoId}/{agentId}
12
- * - Session state (model, mode, activity) → RTDB /session-state/{convoId}/{agentId}
12
+ * - Session state (model, mode, activity) → RTDB /agent-session/{convoId}/{agentId}
13
13
  * - Control signals (model switch, interrupt) ← RTDB /control/{convoId}/{agentId}
14
14
  *
15
15
  * Auth: uses forceLoginMethod: 'claudeai' — works with Max/Pro subscription.
@@ -18,7 +18,7 @@ import { setDefaultResultOrder } from 'node:dns';
18
18
  * canon-claude # auto-select agent profile
19
19
  * canon-claude --cwd /path/to/project # specify working directory
20
20
  * CANON_AGENT=mybot canon-claude # use specific profile
21
- * CANON_API_KEY=agk_... canon-claude # use explicit key
21
+ * CANON_ENVIRONMENT_ID=canon-legacy-v1 CANON_API_KEY=agk_... canon-claude # use explicit key
22
22
  */
23
23
  import { parseArgs } from 'node:util';
24
24
  import { randomUUID } from 'node:crypto';
@@ -28,7 +28,7 @@ import { readFile } from 'node:fs/promises';
28
28
  import { query, } from '@anthropic-ai/claude-agent-sdk';
29
29
  import { isAnthropicImageAttachment, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
30
30
  import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
31
- import { CLAUDE_EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, readLocalRuntimeEntry, resolveCanonAgent, initRTDBAuth, sendMessageWithRetry, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
31
+ import { CLAUDE_EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, readLocalRuntimeEntry, resolveCanonAgent, verifyResolvedAgentEnvironment, initRTDBAuth, sendMessageWithRetry, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
32
32
  import { decideAutoReply, } from './inbound-policy.js';
33
33
  import { runCli } from './cli-entry.js';
34
34
  import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
@@ -65,7 +65,7 @@ COMMON FLAGS
65
65
 
66
66
  AUTH
67
67
  CANON_AGENT=<profile> canon-claude --cwd /path/to/project
68
- CANON_API_KEY=agk_live_... canon-claude --cwd /path/to/project
68
+ CANON_ENVIRONMENT_ID=<id> CANON_API_KEY=agk_live_... canon-claude --cwd /path/to/project
69
69
 
70
70
  EXAMPLES
71
71
  canon-claude --cwd ~/dev/canon
@@ -1825,11 +1825,19 @@ export async function main() {
1825
1825
  for (const warning of workspaceDiscovery.warnings) {
1826
1826
  console.error(`[canon-host] ${warning}`);
1827
1827
  }
1828
- const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-host]', expectedClientType: 'claude-code' });
1828
+ const resolvedAgent = resolveCanonAgent({ logPrefix: '[canon-host]', expectedClientType: 'claude-code' });
1829
+ const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, streamUrl, rtdbUrl, firebaseApiKey, lockHandle, } = resolvedAgent;
1829
1830
  console.error(`[canon-host] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
1831
+ try {
1832
+ await verifyResolvedAgentEnvironment(resolvedAgent);
1833
+ }
1834
+ catch (error) {
1835
+ lockHandle?.release();
1836
+ throw error;
1837
+ }
1830
1838
  // ── Canon API client + RTDB auth ──
1831
1839
  const client = new CanonClient(apiKey, baseUrl);
1832
- const rtdb = initRTDBAuth(client);
1840
+ const rtdb = initRTDBAuth(client, { rtdbUrl, firebaseApiKey });
1833
1841
  const typingSignals = createTypingStatusPublisher({
1834
1842
  setTyping: (conversationId, typing, status) => status
1835
1843
  ? client.setTyping(conversationId, typing, status)
@@ -2669,6 +2677,7 @@ export async function main() {
2669
2677
  const stream = new CanonStream({
2670
2678
  apiKey,
2671
2679
  agentId,
2680
+ streamUrl,
2672
2681
  handler: {
2673
2682
  onMessage: (payload) => {
2674
2683
  const m = payload.message;
package/dist/register.js CHANGED
@@ -9,8 +9,7 @@ import { setDefaultResultOrder } from 'node:dns';
9
9
  *
10
10
  * On approval, saves the agent to ~/.canon/agents.json under the profile name.
11
11
  */
12
- import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
13
- import { readFileSync } from 'node:fs';
12
+ import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, loadProfiles, registerAndWaitForApproval, resolveCanonRuntimeConnection, updatePendingRegistration, upsertAgentProfile, verifyCanonRuntimeConnection, } from '@canonmsg/core';
14
13
  import { parseArgs } from 'node:util';
15
14
  import { runCli } from './cli-entry.js';
16
15
  const HELP = `canon-register — register or reconnect a Claude Code agent in Canon
@@ -25,13 +24,17 @@ REQUIRED
25
24
 
26
25
  FLAGS
27
26
  --profile <name> Local profile name in ~/.canon/agents.json
27
+ --environment <id> Canon environment ID (or CANON_ENVIRONMENT_ID)
28
28
  --base-url <url> Canon API base URL override
29
+ --stream-url <url> Canon stream URL override
30
+ --rtdb-url <url> Canon RTDB URL override
31
+ --firebase-api-key <key> Firebase web API key override (public config)
29
32
  --help, -h Show this help
30
33
  --version, -V Show package version
31
34
 
32
35
  EXAMPLES
33
- canon-register --name "My Claude" --description "Claude Code agent" --phone "+15551234567"
34
- canon-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
36
+ canon-register --environment canon-legacy-v1 --name "My Claude" --description "Claude Code agent" --phone "+15551234567"
37
+ canon-register --environment canon-legacy-v1 --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
35
38
 
36
39
  After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/project.`;
37
40
  export async function main() {
@@ -42,7 +45,11 @@ export async function main() {
42
45
  description: { type: 'string' },
43
46
  phone: { type: 'string' },
44
47
  profile: { type: 'string' },
48
+ environment: { type: 'string' },
45
49
  'base-url': { type: 'string' },
50
+ 'stream-url': { type: 'string' },
51
+ 'rtdb-url': { type: 'string' },
52
+ 'firebase-api-key': { type: 'string' },
46
53
  },
47
54
  strict: true,
48
55
  });
@@ -52,23 +59,31 @@ export async function main() {
52
59
  }
53
60
  // Default profile name: sanitized agent name
54
61
  const profileName = values.profile || values.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
55
- let existingAgentId;
56
- try {
57
- const profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
58
- existingAgentId = profiles[profileName]?.agentId;
59
- }
60
- catch {
61
- // File doesn't exist yet
62
+ const environmentId = values.environment || process.env.CANON_ENVIRONMENT_ID;
63
+ if (!environmentId) {
64
+ throw new Error('--environment or CANON_ENVIRONMENT_ID is required');
62
65
  }
66
+ const connection = resolveCanonRuntimeConnection({
67
+ environmentId,
68
+ apiBaseUrl: values['base-url'],
69
+ streamUrl: values['stream-url'],
70
+ rtdbUrl: values['rtdb-url'],
71
+ firebaseWebApiKey: values['firebase-api-key'],
72
+ });
73
+ await verifyCanonRuntimeConnection(connection);
74
+ const existingProfile = loadProfiles()[profileName];
75
+ const existingAgentId = existingProfile?.environmentId === connection.environmentId
76
+ ? existingProfile.agentId
77
+ : undefined;
63
78
  console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
64
- const pending = getOrCreatePendingRegistration(profileName, 'claude-code');
79
+ const pending = getOrCreatePendingRegistration(profileName, 'claude-code', connection);
65
80
  const result = await registerAndWaitForApproval({
66
81
  name: values.name,
67
82
  description: values.description,
68
83
  ownerPhone: values.phone,
69
84
  developerInfo: 'Claude Code plugin',
70
85
  clientType: 'claude-code',
71
- baseUrl: values['base-url'],
86
+ baseUrl: connection.apiBaseUrl,
72
87
  requestedAgentId: existingAgentId,
73
88
  localRegistrationId: pending.localRegistrationId,
74
89
  }, {
@@ -97,11 +112,15 @@ export async function main() {
97
112
  agentId: result.agentId,
98
113
  agentName: result.agentName,
99
114
  registeredAt: new Date().toISOString(),
115
+ environmentId: connection.environmentId,
116
+ baseUrl: connection.apiBaseUrl,
117
+ streamUrl: connection.streamUrl,
118
+ rtdbUrl: connection.rtdbUrl,
119
+ firebaseApiKey: connection.firebaseWebApiKey,
100
120
  clientType: 'claude-code',
101
- ...(typeof values['base-url'] === 'string' ? { baseUrl: values['base-url'] } : {}),
102
121
  });
103
122
  if (result.requestId) {
104
- await ackRegistrationApproval(values['base-url'], result.requestId, result.pollToken);
123
+ await ackRegistrationApproval(connection.apiBaseUrl, result.requestId, result.pollToken);
105
124
  }
106
125
  clearPendingRegistration(profileName);
107
126
  console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
package/dist/server.js CHANGED
@@ -10,7 +10,7 @@ import { setDefaultResultOrder } from 'node:dns';
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
- import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, renderCanonHostInboundContent, } from '@canonmsg/core';
13
+ import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, verifyResolvedAgentEnvironment, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, renderCanonHostInboundContent, } from '@canonmsg/core';
14
14
  import { materializeMessageMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
15
15
  import { ApprovalHttpServer } from './approval-server.js';
16
16
  import { runCli } from './cli-entry.js';
@@ -447,14 +447,17 @@ async function startChannel() {
447
447
  let profile;
448
448
  let baseUrl;
449
449
  let profileAgentName;
450
+ let resolvedRuntime;
450
451
  try {
451
- const resolved = resolveCanonAgent({ logPrefix: '[canon]', expectedClientType: 'claude-code' });
452
- apiKey = resolved.apiKey;
453
- profile = resolved.profile;
454
- baseUrl = resolved.baseUrl;
455
- profileAgentName = resolved.agentName;
452
+ resolvedRuntime = resolveCanonAgent({ logPrefix: '[canon]', expectedClientType: 'claude-code' });
453
+ apiKey = resolvedRuntime.apiKey;
454
+ profile = resolvedRuntime.profile;
455
+ baseUrl = resolvedRuntime.baseUrl;
456
+ profileAgentName = resolvedRuntime.agentName;
457
+ await verifyResolvedAgentEnvironment(resolvedRuntime);
456
458
  }
457
459
  catch (err) {
460
+ resolvedRuntime?.lockHandle?.release();
458
461
  console.error(err instanceof Error ? err.message : err);
459
462
  return;
460
463
  }
@@ -483,7 +486,10 @@ async function startChannel() {
483
486
  }
484
487
  }
485
488
  // Initialize RTDB auth for session state reporting
486
- initRTDBAuth(client);
489
+ initRTDBAuth(client, {
490
+ rtdbUrl: resolvedRuntime.rtdbUrl,
491
+ firebaseApiKey: resolvedRuntime.firebaseApiKey,
492
+ });
487
493
  const runtimeId = buildLocalRuntimeId({
488
494
  runtime: 'claude-code',
489
495
  profile,
@@ -524,6 +530,7 @@ async function startChannel() {
524
530
  stream = new CanonStream({
525
531
  apiKey,
526
532
  agentId,
533
+ streamUrl: resolvedRuntime.streamUrl,
527
534
  handler: {
528
535
  onMessage: handleInboundMessage,
529
536
  onAgentContext: (ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,11 +31,11 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "0.3.204",
34
- "@canonmsg/agent-sdk": "^5.1.2",
34
+ "@canonmsg/agent-sdk": "^6.0.0",
35
35
  "@canonmsg/coding-agent-host": "^0.2.2",
36
- "@canonmsg/agent-tools": "^0.2.0",
37
- "@canonmsg/core": "^4.3.0",
38
- "@canonmsg/rich-cards": "^0.8.1",
36
+ "@canonmsg/agent-tools": "^0.2.2",
37
+ "@canonmsg/core": "^6.0.0",
38
+ "@canonmsg/rich-cards": "^0.8.3",
39
39
  "@modelcontextprotocol/sdk": "^1.29.0"
40
40
  },
41
41
  "engines": {
@@ -18,11 +18,12 @@ Register a new Canon agent so it can send and receive messages. Each agent is sa
18
18
  - **Agent name** — The display name for the agent in Canon
19
19
  - **Description** — What the agent does (shown to users in Canon)
20
20
  - **Owner phone number** — The Canon account owner's phone number in E.164 format (e.g., +15551234567)
21
+ - **Canon environment** — The trust-domain ID supplied by Canon (currently `canon-legacy-v1` for tester accounts)
21
22
  - **Profile name** (optional) — A short identifier for this agent (e.g., "reviewer", "notifier"). Defaults to a sanitized version of the agent name.
22
23
 
23
24
  2. Run the registration CLI:
24
25
  ```bash
25
- canon-register --name "<name>" --description "<description>" --phone "<phone>" --profile "<profile>"
26
+ canon-register --environment "<environment>" --name "<name>" --description "<description>" --phone "<phone>" --profile "<profile>"
26
27
  ```
27
28
 
28
29
  3. Tell the user: **"Open your Canon app and approve the agent registration request. Waiting for approval..."**
@@ -39,3 +40,4 @@ Register a new Canon agent so it can send and receive messages. Each agent is sa
39
40
  - If status returns `"rejected"`, tell the user the registration was rejected by the owner.
40
41
  - If polling times out after 5 minutes, tell the user to try again later.
41
42
  - If the phone number format is wrong (must start with `+`, 8-16 digits), ask the user to re-enter it.
43
+ - If environment verification fails, do not retry against another URL. Confirm the environment ID and endpoint set with the user.