@canonmsg/claude-code-plugin 0.32.0 → 0.33.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.32.0",
4
+ "version": "0.33.0",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
@@ -1,6 +1,6 @@
1
1
  import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { type AnthropicImageBudgetOptions, type MaterializedCanonAttachment } from '@canonmsg/agent-sdk';
3
- import { renderCanonHostInboundContent, type CanonReplyContext, type HostInboundContentRenderOptions } from '@canonmsg/core';
3
+ import { renderCanonHostInboundContent, type CanonReplyContext } from '@canonmsg/core';
4
4
  type ClaudeRenderableInboundMessage = Parameters<typeof renderCanonHostInboundContent>[0];
5
5
  /**
6
6
  * Claude-only rendering policy for Canon media. Core deliberately omits all
@@ -8,8 +8,8 @@ type ClaudeRenderableInboundMessage = Parameters<typeof renderCanonHostInboundCo
8
8
  * that the bounded runtime did not materialize, while materialized images keep
9
9
  * their local-path placeholder without a duplicate link.
10
10
  */
11
- export declare function renderClaudeInboundContent(message: ClaudeRenderableInboundMessage, materialized?: ReadonlyArray<MaterializedCanonAttachment>, renderOptions?: HostInboundContentRenderOptions): string;
12
- export declare function withClaudeReplyContextMediaReferences(replyContext: CanonReplyContext | null, materialized: ReadonlyArray<MaterializedCanonAttachment>, renderOptions?: HostInboundContentRenderOptions): CanonReplyContext | null;
11
+ export declare function renderClaudeInboundContent(message: ClaudeRenderableInboundMessage, materialized?: ReadonlyArray<MaterializedCanonAttachment>): string;
12
+ export declare function withClaudeReplyContextMediaReferences(replyContext: CanonReplyContext | null, materialized: ReadonlyArray<MaterializedCanonAttachment>): CanonReplyContext | null;
13
13
  /**
14
14
  * Build the Claude Code SDK's multimodal user content without allowing native
15
15
  * image blocks to consume the whole request. The prompt text is preserved
@@ -28,8 +28,8 @@ function validatedPromptAttachmentUrl(value) {
28
28
  * that the bounded runtime did not materialize, while materialized images keep
29
29
  * their local-path placeholder without a duplicate link.
30
30
  */
31
- export function renderClaudeInboundContent(message, materialized = [], renderOptions = {}) {
32
- const rendered = renderCanonHostInboundContent(message, materialized, renderOptions);
31
+ export function renderClaudeInboundContent(message, materialized = []) {
32
+ const rendered = renderCanonHostInboundContent(message, materialized);
33
33
  const materializedIndexes = new Set(materialized.map((attachment) => attachment.index));
34
34
  const imageLinks = (message.attachments ?? []).flatMap((attachment, index) => {
35
35
  if (attachment.kind !== 'image' || materializedIndexes.has(index))
@@ -39,7 +39,7 @@ export function renderClaudeInboundContent(message, materialized = [], renderOpt
39
39
  });
40
40
  return imageLinks.length > 0 ? `${rendered}\n${imageLinks.join('\n')}` : rendered;
41
41
  }
42
- export function withClaudeReplyContextMediaReferences(replyContext, materialized, renderOptions = {}) {
42
+ export function withClaudeReplyContextMediaReferences(replyContext, materialized) {
43
43
  if (!replyContext?.found)
44
44
  return replyContext;
45
45
  return {
@@ -50,7 +50,7 @@ export function withClaudeReplyContextMediaReferences(replyContext, materialized
50
50
  attachments: replyContext.attachments,
51
51
  contactCard: replyContext.contactCard,
52
52
  senderType: replyContext.senderType ?? undefined,
53
- }, materialized, renderOptions),
53
+ }, materialized),
54
54
  };
55
55
  }
56
56
  /**
package/dist/host.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { type PermissionResult, type SDKControlReloadPluginsResponse } from '@anthropic-ai/claude-agent-sdk';
3
3
  import { type RecoveryCheckpointTracker } from '@canonmsg/coding-agent-host';
4
- import { type CanonRuntimeCommandDescriptor, type HostInboundParticipantContext as InboundParticipantContext, type CanonReplyContext, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type TurnVerbosityConfig } from '@canonmsg/core';
4
+ import { type CanonRuntimeCommandDescriptor, type ExecutionEnvironmentMode, type HostInboundParticipantContext as InboundParticipantContext, type CanonReplyContext, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type TurnVerbosityConfig } from '@canonmsg/core';
5
5
  /**
6
6
  * `--turn-verbosity` beats `CANON_TURN_VERBOSITY`; `null` means "unset, use the
7
7
  * per-conversation-type default".
@@ -25,9 +25,11 @@ export declare function buildClaudePlanAllowResult(input: Record<string, unknown
25
25
  * through. Canon-injected aliases keep precedence over native names.
26
26
  */
27
27
  export declare function buildNativeSlashCommandDescriptors(native: SDKControlReloadPluginsResponse['commands'], reservedAliases: ReadonlySet<string>): CanonRuntimeCommandDescriptor[];
28
+ export declare function resolveSessionExecutionMode(config: {
29
+ executionMode?: ExecutionEnvironmentMode;
30
+ } | null | undefined, defaultExecutionMode?: ExecutionEnvironmentMode): ExecutionEnvironmentMode;
28
31
  export declare function createClaudeRecoveryCheckpointTracker(persist: (messageId: string) => boolean): RecoveryCheckpointTracker;
29
32
  export declare const NO_REPLY_TOOL_NAME: string;
30
- export declare const REACH_OUT_TOOL_NAME = "mcp__canon__send_to";
31
33
  export declare function buildCanonPrompt(input: {
32
34
  content: string;
33
35
  conversationId: string;
package/dist/host.js CHANGED
@@ -29,9 +29,9 @@ import { query, } from '@anthropic-ai/claude-agent-sdk';
29
29
  import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
30
30
  import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
31
31
  import { buildCanonUserContent, renderClaudeInboundContent, withClaudeReplyContextMediaReferences, } from './canon-user-content.js';
32
- import { 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, formatPendingContactLifecycleContext, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, readLocalRuntimeEntry, reconcileContactLifecycleEvents, recordLocalRuntimeContactLifecycleEvent, saveLocalRuntimeContactLifecycleCursor, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, takeLocalRuntimeContactLifecycleEvents, upsertLocalRuntimeEntry, } from '@canonmsg/core';
32
+ import { 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, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
33
33
  import { runCli } from '@canonmsg/core';
34
- import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, } from '@canonmsg/agent-tools';
34
+ import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
35
35
  import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
36
36
  import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
37
37
  import { applyClaudeSessionControl, boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, beginClaudeAssistantResponse, claimClaudeTextSegmentId, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, endClaudeAssistantResponse, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeStreamingWrite, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, claudeFinalTurnTrail, publishedClaudeTurnState, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, shouldStopTypingDotsOnStreamedText, isOpenClaudeTurnState, takeClaudeResultOwner, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender, composeClaudeTurnFinal, describeUndeliveredClaudeFinal, confirmClaudeInterrupt, createClaudeInputEnvelope, deriveClaudeSupplementalModelProbes, formatClaudeCliVersion, formatClaudeControlError, isClaudeCustomModelOption, isSupportedClaudeCliVersion, mergeClaudeDiscoveredModelOptions, parseClaudeCliVersion, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, shouldRouteClaudeTurnArtifacts, MINIMUM_CLAUDE_CLI_VERSION, } from './session-state.js';
@@ -53,6 +53,8 @@ COMMON FLAGS
53
53
  --cwd <path> Project directory to run Claude Code from
54
54
  --workspace <path> Additional project to expose in Canon
55
55
  --workspace-root <path> Discover projects under an approved root
56
+ --default-execution-mode <worktree|locked>
57
+ Default for new Canon conversations
56
58
  --runtime-visibility <normal|minimal|full>
57
59
  Detail visibility preset
58
60
  --show-runtime-detail <field>
@@ -271,6 +273,7 @@ function buildClaudeRuntimeDescriptor(input) {
271
273
  workspaces: input.workspaces,
272
274
  workspaceRoots: input.workspaceRoots,
273
275
  executionModes: input.executionModes,
276
+ defaultExecutionMode: input.defaultExecutionMode,
274
277
  permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
275
278
  defaultPermissionMode: 'default',
276
279
  effortOptions: [...EFFORT_OPTIONS],
@@ -303,11 +306,7 @@ function buildClaudeRuntimeDescriptor(input) {
303
306
  activation: { kind: 'control', controlId: 'permissionMode', value: 'plan' },
304
307
  },
305
308
  ],
306
- admissionActions: {
307
- ...HOST_ADMISSION_ACTIONS_DISABLED,
308
- requestContact: true,
309
- reachOut: true,
310
- },
309
+ admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
311
310
  presentation: input.presentation,
312
311
  commands,
313
312
  });
@@ -425,7 +424,6 @@ async function loadSessionConfig(conversationId, agentId, rtdb) {
425
424
  agentId,
426
425
  rtdb,
427
426
  extraStringFields: ['permissionMode', 'effort'],
428
- retryMissingMs: 3_000,
429
427
  });
430
428
  return {
431
429
  ...config,
@@ -436,10 +434,15 @@ async function loadSessionConfig(conversationId, agentId, rtdb) {
436
434
  ultracode: parseClaudeUltracodeMode(config?.runtimeControlValues?.ultracode),
437
435
  };
438
436
  }
439
- function resolveSessionExecutionMode(config) {
440
- if (config?.executionMode)
441
- return config.executionMode;
442
- throw new ExecutionEnvironmentError('Session config is missing an execution mode.', 'Choose Isolated worktree or Use shared project before starting this coding session.');
437
+ export function resolveSessionExecutionMode(config, defaultExecutionMode = 'worktree') {
438
+ return config?.executionMode ?? defaultExecutionMode;
439
+ }
440
+ function resolveConfiguredDefaultExecutionMode(value) {
441
+ if (value == null || value === '')
442
+ return 'worktree';
443
+ if (value === 'worktree' || value === 'locked')
444
+ return value;
445
+ throw new Error('--default-execution-mode must be worktree or locked');
443
446
  }
444
447
  function resolveWorkspaceCwd(config) {
445
448
  return resolveHostWorkspaceCwd({
@@ -757,16 +760,6 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
757
760
  */
758
761
  const NO_REPLY_VERB = 'no_reply';
759
762
  export const NO_REPLY_TOOL_NAME = `mcp__${CANON_VERB_MCP_SERVER_NAME}__${NO_REPLY_VERB}`;
760
- export const REACH_OUT_TOOL_NAME = `mcp__${CANON_VERB_MCP_SERVER_NAME}__send_to`;
761
- function ownerBoundRenderOptions(isOwner) {
762
- const admissionActions = isOwner
763
- ? { ...HOST_ADMISSION_ACTIONS_DISABLED, requestContact: true, reachOut: true }
764
- : HOST_ADMISSION_ACTIONS_DISABLED;
765
- return {
766
- admissionActions,
767
- ...(admissionActions.reachOut ? { reachOutToolName: REACH_OUT_TOOL_NAME } : {}),
768
- };
769
- }
770
763
  export function buildCanonPrompt(input) {
771
764
  return renderCodingHostInboundPrompt(buildCanonInboundFrameV1(buildCanonTurnContextV2({
772
765
  content: input.content,
@@ -799,8 +792,8 @@ function resolveClaudeTurnModes(participantContext) {
799
792
  }),
800
793
  };
801
794
  }
802
- function renderInboundContent(message, materialized, renderOptions = {}) {
803
- return renderClaudeInboundContent(message, materialized, renderOptions);
795
+ function renderInboundContent(message, materialized) {
796
+ return renderClaudeInboundContent(message, materialized);
804
797
  }
805
798
  async function materializePromptReplyContext(input) {
806
799
  if (!input.replyContext?.found || !input.replyContext.attachments?.length) {
@@ -813,27 +806,17 @@ async function materializePromptReplyContext(input) {
813
806
  });
814
807
  return {
815
808
  ...result,
816
- replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized, input.renderOptions),
809
+ replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized),
817
810
  };
818
811
  }
819
812
  catch (error) {
820
813
  console.error(`${input.logPrefix} Failed to materialize replied-to media:`, error instanceof Error ? error.message : error);
821
814
  return {
822
- replyContext: withClaudeReplyContextMediaReferences(input.replyContext, [], input.renderOptions),
815
+ replyContext: withClaudeReplyContextMediaReferences(input.replyContext, []),
823
816
  materialized: [],
824
817
  };
825
818
  }
826
819
  }
827
- function ownerBoundReplyContactTarget(replyContext) {
828
- const card = replyContext?.found ? replyContext.contactCard : undefined;
829
- if (!card?.userId)
830
- return undefined;
831
- return {
832
- targetUserId: card.userId,
833
- ...(card.canonContactId ? { canonContactId: card.canonContactId } : {}),
834
- sourceCardMessageId: replyContext.messageId,
835
- };
836
- }
837
820
  // ── Session factory ─────────────────────────────────────────────────
838
821
  function createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
839
822
  const { cwd } = environment;
@@ -852,7 +835,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
852
835
  messageQueue.push(input);
853
836
  }
854
837
  };
855
- let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}, replyContactTarget) => {
838
+ let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => {
856
839
  sendInput(createClaudeInputEnvelope({
857
840
  kind: 'canon',
858
841
  msg,
@@ -861,7 +844,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
861
844
  markAccepted,
862
845
  isOwnerTurn,
863
846
  requestingUserId,
864
- replyContactTarget,
865
847
  ...turnModes,
866
848
  }));
867
849
  };
@@ -1008,24 +990,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1008
990
  const turnKey = session.activeInput?.turnKey;
1009
991
  if (turnKey)
1010
992
  session.silencedTurnKeys.add(turnKey);
1011
- }, {
1012
- ownerBoundCommunication: {
1013
- getContext: () => {
1014
- const input = session.activeInput;
1015
- if (!input?.isOwnerTurn || !input.sourceMessageId)
1016
- return null;
1017
- return {
1018
- isOwnerTurn: true,
1019
- conversationId,
1020
- sourceMessageId: input.sourceMessageId,
1021
- ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
1022
- ...(input.replyContactTarget
1023
- ? { replyContactTarget: input.replyContactTarget }
1024
- : {}),
1025
- };
1026
- },
1027
- },
1028
- }),
993
+ }, { communication: config.communicationEnabled !== false }),
1029
994
  },
1030
995
  },
1031
996
  ...(claudeCliPath ? { pathToClaudeCodeExecutable: claudeCliPath } : {}),
@@ -1214,7 +1179,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1214
1179
  environment,
1215
1180
  query: q,
1216
1181
  sendInput,
1217
- enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}, replyContactTarget) => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, requestingUserId, turnModes, replyContactTarget),
1182
+ enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, requestingUserId, turnModes),
1218
1183
  state: {
1219
1184
  model: undefined,
1220
1185
  permissionMode: config.permissionMode,
@@ -1968,7 +1933,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1968
1933
  function hasInterruptibleSdkInput() {
1969
1934
  return session.activeInput !== null && session.dispatchingInput === null;
1970
1935
  }
1971
- enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}, replyContactTarget) => {
1936
+ enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => {
1972
1937
  session.lastActivity = Date.now();
1973
1938
  const input = createClaudeInputEnvelope({
1974
1939
  kind: 'canon',
@@ -1978,7 +1943,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1978
1943
  markAccepted,
1979
1944
  isOwnerTurn,
1980
1945
  requestingUserId,
1981
- replyContactTarget,
1982
1946
  ...turnModes,
1983
1947
  });
1984
1948
  const decision = decideClaudeInboundDispatch({
@@ -2430,11 +2394,13 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2430
2394
  availableModels: modelList,
2431
2395
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
2432
2396
  availableExecutionModes: config.availableExecutionModes,
2397
+ defaultExecutionMode: config.defaultExecutionMode,
2433
2398
  runtimeDescriptor: buildClaudeRuntimeDescriptor({
2434
2399
  models: modelList,
2435
2400
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
2436
2401
  workspaceRoots: workspaceRootMetadata,
2437
2402
  executionModes: config.availableExecutionModes,
2403
+ defaultExecutionMode: config.defaultExecutionMode,
2438
2404
  presentation: config.runtimePresentation,
2439
2405
  }),
2440
2406
  });
@@ -2464,6 +2430,7 @@ export async function main() {
2464
2430
  cwd: { type: 'string' },
2465
2431
  workspace: { type: 'string', multiple: true },
2466
2432
  'workspace-root': { type: 'string', multiple: true },
2433
+ 'default-execution-mode': { type: 'string' },
2467
2434
  'runtime-visibility': { type: 'string' },
2468
2435
  'show-runtime-detail': { type: 'string', multiple: true },
2469
2436
  'hide-runtime-detail': { type: 'string', multiple: true },
@@ -2476,6 +2443,7 @@ export async function main() {
2476
2443
  env: process.env.CANON_TURN_VERBOSITY,
2477
2444
  onWarning: (message) => console.error(`[canon-host] ${message}`),
2478
2445
  });
2446
+ const defaultExecutionMode = resolveConfiguredDefaultExecutionMode(args['default-execution-mode']);
2479
2447
  workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
2480
2448
  const configuredWorkspaces = (args.workspace ?? []).filter((value) => typeof value === 'string');
2481
2449
  const configuredWorkspaceRoots = (args['workspace-root'] ?? []).filter((value) => typeof value === 'string');
@@ -2516,11 +2484,13 @@ export async function main() {
2516
2484
  let agentId;
2517
2485
  let ownerId = null;
2518
2486
  let ownerName = null;
2487
+ let communicationEnabled = true;
2519
2488
  try {
2520
2489
  const ctx = await client.getAgentMe();
2521
2490
  agentId = ctx.agentId;
2522
2491
  ownerId = ctx.ownerId;
2523
2492
  ownerName = ctx.ownerName;
2493
+ communicationEnabled = ctx.outboundPolicy !== 'closed';
2524
2494
  console.error(`[canon-host] Connected as ${ctx.displayName || agentId}`);
2525
2495
  }
2526
2496
  catch {
@@ -2567,15 +2537,6 @@ export async function main() {
2567
2537
  hostMode: true,
2568
2538
  rtdb,
2569
2539
  });
2570
- const recoveryCheckpointTrackers = new Map();
2571
- function recoveryCheckpointsFor(conversationId) {
2572
- let tracker = recoveryCheckpointTrackers.get(conversationId);
2573
- if (!tracker) {
2574
- tracker = createClaudeRecoveryCheckpointTracker((messageId) => persistConversationRecoveryCursor(conversationId, messageId));
2575
- recoveryCheckpointTrackers.set(conversationId, tracker);
2576
- }
2577
- return tracker;
2578
- }
2579
2540
  let streamConnected = false;
2580
2541
  const hostAvailableExecutionModes = [
2581
2542
  ...EXECUTION_ENVIRONMENT_MODES,
@@ -2587,6 +2548,7 @@ export async function main() {
2587
2548
  hide: stringArgs(args['hide-runtime-detail']),
2588
2549
  });
2589
2550
  let runtimeDescriptor = {
2551
+ defaultExecutionMode,
2590
2552
  defaultWorkspaceId: workspaceOptions[0]?.id,
2591
2553
  defaultPermissionMode: 'default',
2592
2554
  availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
@@ -2597,6 +2559,7 @@ export async function main() {
2597
2559
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
2598
2560
  workspaceRoots: workspaceRootMetadata,
2599
2561
  executionModes: hostAvailableExecutionModes,
2562
+ defaultExecutionMode,
2600
2563
  presentation: runtimePresentation,
2601
2564
  }),
2602
2565
  };
@@ -2675,6 +2638,7 @@ export async function main() {
2675
2638
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
2676
2639
  workspaceRoots: workspaceRootMetadata,
2677
2640
  executionModes: hostAvailableExecutionModes,
2641
+ defaultExecutionMode,
2678
2642
  presentation: runtimePresentation,
2679
2643
  }),
2680
2644
  };
@@ -2709,6 +2673,7 @@ export async function main() {
2709
2673
  try {
2710
2674
  runtimeModels = await detectRuntimeModels(workingDir);
2711
2675
  runtimeDescriptor = {
2676
+ defaultExecutionMode,
2712
2677
  defaultWorkspaceId: workspaceOptions[0]?.id,
2713
2678
  defaultModel: runtimeModels[0]?.value,
2714
2679
  defaultPermissionMode: 'default',
@@ -2721,12 +2686,14 @@ export async function main() {
2721
2686
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
2722
2687
  workspaceRoots: workspaceRootMetadata,
2723
2688
  executionModes: hostAvailableExecutionModes,
2689
+ defaultExecutionMode,
2724
2690
  presentation: runtimePresentation,
2725
2691
  }),
2726
2692
  };
2727
2693
  }
2728
2694
  catch {
2729
2695
  runtimeDescriptor = {
2696
+ defaultExecutionMode,
2730
2697
  defaultWorkspaceId: workspaceOptions[0]?.id,
2731
2698
  defaultPermissionMode: 'default',
2732
2699
  availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
@@ -2737,6 +2704,7 @@ export async function main() {
2737
2704
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
2738
2705
  workspaceRoots: workspaceRootMetadata,
2739
2706
  executionModes: hostAvailableExecutionModes,
2707
+ defaultExecutionMode,
2740
2708
  presentation: runtimePresentation,
2741
2709
  }),
2742
2710
  };
@@ -2759,6 +2727,15 @@ export async function main() {
2759
2727
  }
2760
2728
  // ── Session manager ──
2761
2729
  const sessions = new Map();
2730
+ const recoveryCheckpointTrackers = new Map();
2731
+ function recoveryCheckpointsFor(conversationId) {
2732
+ let tracker = recoveryCheckpointTrackers.get(conversationId);
2733
+ if (!tracker) {
2734
+ tracker = createClaudeRecoveryCheckpointTracker((messageId) => persistConversationRecoveryCursor(conversationId, messageId));
2735
+ recoveryCheckpointTrackers.set(conversationId, tracker);
2736
+ }
2737
+ return tracker;
2738
+ }
2762
2739
  const approvalManager = ownerId ? new ApprovalManager(client, agentId, ownerId) : null;
2763
2740
  // Interactive runtime input (AskUserQuestion) works with or without an owner,
2764
2741
  // so its poll engine is always present. Create + poll fold onto the built-in
@@ -2887,7 +2864,6 @@ export async function main() {
2887
2864
  ownerName,
2888
2865
  membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
2889
2866
  groupContextMode: getGroupContextMode(input.conversationId, conversation),
2890
- renderOptions: input.renderOptions,
2891
2867
  });
2892
2868
  }
2893
2869
  function closeSession(conversationId, options = {}) {
@@ -3025,7 +3001,7 @@ export async function main() {
3025
3001
  }
3026
3002
  const creation = (async () => {
3027
3003
  const config = await loadSessionConfig(conversationId, agentId, rtdb);
3028
- const sessionExecutionMode = resolveSessionExecutionMode(config);
3004
+ const sessionExecutionMode = resolveSessionExecutionMode(config, defaultExecutionMode);
3029
3005
  const workspaceCwd = resolveWorkspaceCwd(config);
3030
3006
  const environment = prepareConversationEnvironment({
3031
3007
  agentId,
@@ -3086,12 +3062,14 @@ export async function main() {
3086
3062
  ultracode: config?.ultracode,
3087
3063
  availableModels: runtimeModels,
3088
3064
  availableExecutionModes: hostAvailableExecutionModes,
3065
+ defaultExecutionMode,
3089
3066
  runtimePresentation,
3090
3067
  approvalManager,
3091
3068
  runtimeInputManager,
3092
3069
  canRequestApproval: () => canRequestCanonApproval(conversationId),
3093
3070
  runtimeId,
3094
3071
  ownerId,
3072
+ communicationEnabled,
3095
3073
  onInputCompleted: (sourceMessageId) => {
3096
3074
  recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
3097
3075
  settleInboundMessageId(sourceMessageId, true);
@@ -3151,15 +3129,13 @@ export async function main() {
3151
3129
  }
3152
3130
  const sender = m.senderName || m.senderId;
3153
3131
  const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
3154
- const renderOptions = ownerBoundRenderOptions(isOwner);
3155
- const content = renderInboundContent(m, materialized, renderOptions);
3132
+ const content = renderInboundContent(m, materialized);
3156
3133
  const hydrated = await loadHydratedInboundContext({
3157
3134
  conversationId: input.conversationId,
3158
3135
  message: m,
3159
3136
  senderName: sender,
3160
3137
  isOwner,
3161
3138
  hydratedPage: input.hydratedPage,
3162
- renderOptions,
3163
3139
  });
3164
3140
  const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
3165
3141
  const activeSelfContextId = hydrated.activeSelfContextId;
@@ -3169,7 +3145,6 @@ export async function main() {
3169
3145
  agentId,
3170
3146
  conversationId: input.conversationId,
3171
3147
  logPrefix: `[canon-host] [${input.conversationId.slice(0, 8)}]`,
3172
- renderOptions,
3173
3148
  });
3174
3149
  const replyContext = replyMedia.replyContext;
3175
3150
  const participantContext = hydrated.participantContext;
@@ -3202,7 +3177,7 @@ export async function main() {
3202
3177
  return 'handled';
3203
3178
  }
3204
3179
  session.activeSelfContextId = activeSelfContextId;
3205
- const basePromptText = buildCanonPrompt({
3180
+ const promptText = buildCanonPrompt({
3206
3181
  content,
3207
3182
  conversationId: input.conversationId,
3208
3183
  participantContext,
@@ -3213,12 +3188,6 @@ export async function main() {
3213
3188
  replyContext,
3214
3189
  message: m,
3215
3190
  });
3216
- const lifecycleContext = isOwner
3217
- ? formatPendingContactLifecycleContext(takeLocalRuntimeContactLifecycleEvents(runtimeId, input.conversationId))
3218
- : null;
3219
- const promptText = lifecycleContext
3220
- ? `${basePromptText}\n\n${lifecycleContext}`
3221
- : basePromptText;
3222
3191
  const messageContent = await buildCanonUserContent({
3223
3192
  promptText,
3224
3193
  materialized: [...replyMedia.materialized, ...materialized],
@@ -3236,7 +3205,7 @@ export async function main() {
3236
3205
  senderId: m.senderId,
3237
3206
  senderName: m.senderName,
3238
3207
  }),
3239
- }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes, ownerBoundReplyContactTarget(replyContext));
3208
+ }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
3240
3209
  return 'queued';
3241
3210
  }
3242
3211
  const acceptedInboundMessageIds = new Set();
@@ -3316,27 +3285,6 @@ export async function main() {
3316
3285
  const conversationsDiscoveredWhileOffline = startupRecoveryComplete
3317
3286
  ? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
3318
3287
  : new Set();
3319
- try {
3320
- let cursor = readLocalRuntimeEntry(runtimeId)?.contactLifecycleCursor ?? null;
3321
- for (let pageNumber = 0; pageNumber < 10; pageNumber += 1) {
3322
- const page = await client.listContactRequestLifecyclePage({
3323
- cursor,
3324
- limit: 100,
3325
- });
3326
- await reconcileContactLifecycleEvents({
3327
- requests: page.requests,
3328
- requesterId: agentId,
3329
- record: (request) => recordLocalRuntimeContactLifecycleEvent(runtimeId, request),
3330
- });
3331
- cursor = page.nextCursor;
3332
- saveLocalRuntimeContactLifecycleCursor(runtimeId, cursor);
3333
- if (!page.hasMore)
3334
- break;
3335
- }
3336
- }
3337
- catch (error) {
3338
- console.error('[canon-host] Contact lifecycle recovery failed:', error instanceof Error ? error.message : error);
3339
- }
3340
3288
  for (const conversationId of knownConversationIds) {
3341
3289
  const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
3342
3290
  const recoveryBatch = recoveryCheckpoints.reserveBatch();
@@ -3436,8 +3384,7 @@ export async function main() {
3436
3384
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
3437
3385
  }
3438
3386
  }
3439
- const renderOptions = ownerBoundRenderOptions(isOwner);
3440
- const content = renderInboundContent(m, materialized, renderOptions);
3387
+ const content = renderInboundContent(m, materialized);
3441
3388
  const hydrated = await loadHydratedInboundContext({
3442
3389
  conversationId: convoId,
3443
3390
  message: m,
@@ -3446,7 +3393,6 @@ export async function main() {
3446
3393
  activeSelfContextId: payload.activeSelfContextId,
3447
3394
  selfContexts: payload.selfContexts,
3448
3395
  provenance: payload.provenance,
3449
- renderOptions,
3450
3396
  });
3451
3397
  const behavior = payload.behavior ?? hydrated.behavior;
3452
3398
  const activeSelfContextId = hydrated.activeSelfContextId;
@@ -3456,7 +3402,6 @@ export async function main() {
3456
3402
  agentId,
3457
3403
  conversationId: convoId,
3458
3404
  logPrefix: `[canon-host] [${convoId.slice(0, 8)}]`,
3459
- renderOptions,
3460
3405
  });
3461
3406
  const replyContext = replyMedia.replyContext;
3462
3407
  const participantContext = hydrated.participantContext;
@@ -3495,7 +3440,7 @@ export async function main() {
3495
3440
  return false;
3496
3441
  }
3497
3442
  session.activeSelfContextId = activeSelfContextId;
3498
- const basePromptText = buildCanonPrompt({
3443
+ const promptText = buildCanonPrompt({
3499
3444
  content,
3500
3445
  conversationId: convoId,
3501
3446
  participantContext,
@@ -3506,12 +3451,6 @@ export async function main() {
3506
3451
  replyContext,
3507
3452
  message: m,
3508
3453
  });
3509
- const lifecycleContext = isOwner
3510
- ? formatPendingContactLifecycleContext(takeLocalRuntimeContactLifecycleEvents(runtimeId, convoId))
3511
- : null;
3512
- const promptText = lifecycleContext
3513
- ? `${basePromptText}\n\n${lifecycleContext}`
3514
- : basePromptText;
3515
3454
  const messageContent = await buildCanonUserContent({
3516
3455
  promptText,
3517
3456
  materialized: [...replyMedia.materialized, ...materialized],
@@ -3529,7 +3468,7 @@ export async function main() {
3529
3468
  senderId: m.senderId,
3530
3469
  senderName: m.senderName,
3531
3470
  }),
3532
- }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes, ownerBoundReplyContactTarget(replyContext));
3471
+ }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
3533
3472
  return true;
3534
3473
  })().then((queued) => {
3535
3474
  if (!queued) {
@@ -3547,24 +3486,20 @@ export async function main() {
3547
3486
  onConversationUpdated: (payload) => {
3548
3487
  handleConversationUpdated(payload);
3549
3488
  },
3550
- onContactRequestUpdated: (payload) => {
3551
- if (payload.requesterId !== agentId || !payload.sourceConversationId)
3552
- return;
3553
- recordLocalRuntimeContactLifecycleEvent(runtimeId, payload);
3554
- },
3555
3489
  onReplayExpired: () => {
3556
3490
  void reconnectRecovery.onReplayExpired().catch((error) => {
3557
- console.error('[canon-host] Replay-expired recovery failed:', error);
3491
+ console.error('[canon-host] Replay-expiry recovery failed:', error instanceof Error ? error.message : error);
3558
3492
  });
3559
3493
  },
3560
3494
  onConnected: () => {
3561
3495
  streamConnected = true;
3562
3496
  void publishRuntimeHeartbeat();
3563
3497
  const recovery = reconnectRecovery.onConnected();
3564
- if (recovery)
3498
+ if (recovery) {
3565
3499
  void recovery.catch((error) => {
3566
- console.error('[canon-host] Reconnect recovery failed:', error);
3500
+ console.error('[canon-host] Reconnect recovery failed:', error instanceof Error ? error.message : error);
3567
3501
  });
3502
+ }
3568
3503
  console.error('[canon-host] SSE connected');
3569
3504
  },
3570
3505
  onDisconnected: () => {
package/dist/register.js CHANGED
@@ -37,7 +37,6 @@ After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/
37
37
  const OPTIONS = {
38
38
  moduleUrl: import.meta.url,
39
39
  clientType: 'claude-code',
40
- sessionSetupPolicy: 'runtime_descriptor_required',
41
40
  cliName: 'canon-register',
42
41
  hostBinName: 'canon-claude',
43
42
  developerInfo: 'Claude Code plugin',
package/dist/server.js CHANGED
@@ -16,8 +16,7 @@ import { ApprovalHttpServer } from './approval-server.js';
16
16
  import { renderClaudeInboundContent } from './canon-user-content.js';
17
17
  import { runCli } from '@canonmsg/core';
18
18
  import { parseReplyArgs, parseSendMessageArgs, parseSetTypingArgs, } from './mcp-args.js';
19
- import { OWNER_BOUND_CANON_COMMUNICATION_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from '@canonmsg/agent-tools';
20
- const OWNER_BOUND_COMMUNICATION_VERBS = new Set(OWNER_BOUND_CANON_COMMUNICATION_VERBS);
19
+ import { canonVerbToolDefinitions, createCanonCommunicationBinding, executeCanonVerbTool, isCanonToolVerb, } from '@canonmsg/agent-tools';
21
20
  const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
22
21
 
23
22
  USAGE
@@ -46,6 +45,7 @@ const conversationCache = new Map();
46
45
  const TURN_COMPLETE_METADATA = {
47
46
  turnSemantics: 'turn_complete',
48
47
  };
48
+ const communicationBinding = createCanonCommunicationBinding();
49
49
  /** Last owner-visible conversation to route approval cards into. */
50
50
  let approvalConversationId = null;
51
51
  /** Whether the message that most recently triggered work came from the owner. */
@@ -175,7 +175,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
175
175
  // Canonical verbs — projections of canon.verbs.v1 (@canonmsg/agent-tools).
176
176
  // Channel mode owns no turn, so `no_reply` is an ack-only no-op here: the
177
177
  // standalone session has no final delivery for it to suppress.
178
- ...canonVerbToolDefinitions().filter((definition) => !OWNER_BOUND_COMMUNICATION_VERBS.has(definition.name)),
178
+ ...canonVerbToolDefinitions(),
179
+ ...(agentContext?.outboundPolicy === 'closed' ? [] : communicationBinding.tools),
179
180
  ],
180
181
  }));
181
182
  // ── Tool handlers ──────────────────────────────────────────────────────
@@ -315,13 +316,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
315
316
  }
316
317
  default: {
317
318
  const name = request.params.name;
318
- if (OWNER_BOUND_COMMUNICATION_VERBS.has(name)) {
319
- return toolArgumentError(`${name} requires an owner-authored Canon turn and is unavailable in standalone channel mode.`);
319
+ if (communicationBinding.isToolName(name)) {
320
+ const result = await communicationBinding.execute(client, name, args);
321
+ return {
322
+ content: result.content.map((item) => ({ type: 'text', text: item.text })),
323
+ ...(result.isError ? { isError: true } : {}),
324
+ };
320
325
  }
321
326
  if (isCanonToolVerb(name)) {
322
327
  const rawArgs = args && typeof args === 'object' ? { ...args } : {};
323
- const verbArgs = name === 'send_to' ? stampSendToTurnComplete(rawArgs) : rawArgs;
324
- const verbResult = await executeCanonVerbTool(client, name, verbArgs);
328
+ const verbResult = await executeCanonVerbTool(client, name, rawArgs);
325
329
  return {
326
330
  content: verbResult.content.map((item) => ({ type: 'text', text: item.text })),
327
331
  ...(verbResult.isError ? { isError: true } : {}),
@@ -1,6 +1,5 @@
1
1
  import type { PermissionMode, SDKMessageOrigin, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import type { DeliveryIntent, ModelOption, RuntimeStreamingPayload, TurnLifecycleState, TurnOutputBlock, TurnOutputSnapshot, TurnVerbosity } from '@canonmsg/core';
3
- import type { OwnerBoundCanonContactTarget } from '@canonmsg/agent-tools';
4
3
  import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
5
4
  export type ClaudeInputKind = 'seed' | 'canon';
6
5
  export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
@@ -24,8 +23,6 @@ export interface ClaudeInputEnvelope {
24
23
  turnKey: string;
25
24
  isOwnerTurn: boolean;
26
25
  requestingUserId: string | null;
27
- /** Trusted contact-card target resolved from the owner message's reply. */
28
- replyContactTarget?: OwnerBoundCanonContactTarget;
29
26
  }
30
27
  /**
31
28
  * The per-turn modes an inbound message resolves from its participant context,
@@ -105,7 +102,6 @@ export declare function createClaudeInputEnvelope(input: {
105
102
  artifactBaseline?: TurnArtifactSnapshot | null;
106
103
  isOwnerTurn?: boolean;
107
104
  requestingUserId?: string | null;
108
- replyContactTarget?: OwnerBoundCanonContactTarget;
109
105
  }): ClaudeInputEnvelope;
110
106
  export declare function resolveClaudeTurnResponseRouting(turn: Pick<ClaudeInputEnvelope, 'kind' | 'isOwnerTurn' | 'requestingUserId'> | null | undefined, ownerId: string | null | undefined, ownerOnly?: boolean): {
111
107
  isOwnerTurn: boolean;
@@ -28,9 +28,6 @@ export function createClaudeInputEnvelope(input) {
28
28
  requestingUserId: input.kind === 'canon' && input.requestingUserId
29
29
  ? input.requestingUserId
30
30
  : null,
31
- ...(input.kind === 'canon' && input.replyContactTarget
32
- ? { replyContactTarget: input.replyContactTarget }
33
- : {}),
34
31
  turnKey: input.kind === 'canon' && sourceMessageId
35
32
  ? `canon:${sourceMessageId}`
36
33
  : `${input.kind}:${fallbackId}`,
@@ -37,13 +37,9 @@ function normalizeToolName(toolName) {
37
37
  function isCanonOutboundTool(normalizedToolName) {
38
38
  return normalizedToolName.endsWith('__reply')
39
39
  || normalizedToolName.endsWith('__send_message')
40
- || normalizedToolName.endsWith('__send_contextual_message')
41
- || normalizedToolName.endsWith('__send_to')
42
40
  || normalizedToolName.endsWith('__share_contact')
43
41
  || normalizedToolName === 'reply'
44
42
  || normalizedToolName === 'send_message'
45
- || normalizedToolName === 'send_contextual_message'
46
- || normalizedToolName === 'send_to'
47
43
  || normalizedToolName === 'share_contact';
48
44
  }
49
45
  /**
@@ -79,7 +75,7 @@ const CANON_INTERACTION_LIFECYCLE_VERBS = new Set([
79
75
  * Verbs a NON-owner conversation member may trigger: same-conversation HITL
80
76
  * and display surfaces, whose responder routing and owner-only escalation
81
77
  * (secret/sudo, session rules) are enforced server-side. Cross-conversation
82
- * verbs (send_to, share_contact) and the reads (contacts, conversation
78
+ * cross-conversation verbs (share_contact) and the reads (contacts, conversation
83
79
  * lists — they expose the agent's other relationships to this conversation)
84
80
  * stay owner-only until the provenance trigger-token primitive lands.
85
81
  */
@@ -257,7 +253,7 @@ export async function decideClaudeToolPermissionForMode(input) {
257
253
  }
258
254
  // HITL + read verbs are allowed without an approval card: the HITL verbs
259
255
  // themselves render the human-facing card (recursion otherwise), and the
260
- // reads are side-effect free. Outbound verbs (send_to, share_contact)
256
+ // reads are side-effect free. Outbound verbs (share_contact)
261
257
  // fall through to the mode policy — in native-approval mode they gate
262
258
  // like any other side effect; in default mode owner turns allow.
263
259
  if (CANON_HITL_OR_READ_VERBS.has(canonVerb) && !input.ruleForcedAsk) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.32.0",
3
+ "version": "0.33.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.228",
34
- "@canonmsg/agent-sdk": "^8.9.0",
35
- "@canonmsg/agent-tools": "^0.6.0",
36
- "@canonmsg/coding-agent-host": "^0.6.0",
37
- "@canonmsg/core": "^10.7.0",
38
- "@canonmsg/rich-cards": "^0.10.2",
34
+ "@canonmsg/agent-sdk": "^9.0.0",
35
+ "@canonmsg/agent-tools": "^0.7.0",
36
+ "@canonmsg/coding-agent-host": "^0.7.0",
37
+ "@canonmsg/core": "^11.0.0",
38
+ "@canonmsg/rich-cards": "^0.10.3",
39
39
  "@modelcontextprotocol/sdk": "^1.30.0"
40
40
  },
41
41
  "engines": {