@canonmsg/claude-code-plugin 0.25.5 → 0.26.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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/host.d.ts +2 -1
- package/dist/host.js +27 -1
- package/dist/mcp-args.d.ts +0 -11
- package/dist/mcp-args.js +0 -68
- package/dist/server.js +17 -111
- package/dist/tool-policy.d.ts +3 -0
- package/dist/tool-policy.js +81 -4
- package/package.json +6 -4
package/dist/host.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type SDKControlReloadPluginsResponse } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { type PermissionResult, type SDKControlReloadPluginsResponse } from '@anthropic-ai/claude-agent-sdk';
|
|
3
3
|
import { type CanonRuntimeCommandDescriptor } from '@canonmsg/core';
|
|
4
|
+
export declare function buildClaudePlanAllowResult(input: Record<string, unknown>): PermissionResult;
|
|
4
5
|
/**
|
|
5
6
|
* Claude Code's own slash commands (built-ins, plugins, project .claude/
|
|
6
7
|
* commands) are executable when sent as prompt text, so the runtime's command
|
package/dist/host.js
CHANGED
|
@@ -31,6 +31,7 @@ import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/co
|
|
|
31
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';
|
|
32
32
|
import { decideAutoReply, } from './inbound-policy.js';
|
|
33
33
|
import { runCli } from './cli-entry.js';
|
|
34
|
+
import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
|
|
34
35
|
import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
|
|
35
36
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
36
37
|
import { applyClaudeSessionControl, buildClaudeFinalMessageId, claudeModelInfoToOption, confirmClaudeInterrupt, createClaudeInputEnvelope, formatClaudeControlError, isClaudeCustomModelOption, mergeClaudeDiscoveredModelOptions, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
@@ -92,6 +93,9 @@ let runtimeMetadata = {
|
|
|
92
93
|
let runtimeMetadataRefresh = null;
|
|
93
94
|
/** Bound on native slash commands exposed to the composer palette. */
|
|
94
95
|
const MAX_NATIVE_SLASH_COMMANDS = 50;
|
|
96
|
+
export function buildClaudePlanAllowResult(input) {
|
|
97
|
+
return { behavior: 'allow', updatedInput: input };
|
|
98
|
+
}
|
|
95
99
|
/**
|
|
96
100
|
* Claude Code's own slash commands (built-ins, plugins, project .claude/
|
|
97
101
|
* commands) are executable when sent as prompt text, so the runtime's command
|
|
@@ -864,6 +868,28 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
864
868
|
...(config.effort ? { effort: config.effort } : {}),
|
|
865
869
|
settings: { forceLoginMethod: 'claudeai' },
|
|
866
870
|
settingSources: ['project', 'local'],
|
|
871
|
+
// Canonical Canon verbs, in-process (projection 2 of canon.verbs.v1).
|
|
872
|
+
// Fresh instance per session: the SDK connects the instance to its own
|
|
873
|
+
// in-process transport, so instances are not shared across queries.
|
|
874
|
+
// The context getter reads the session lazily (tool calls only happen
|
|
875
|
+
// once the query is running, after `session` is assigned — same pattern
|
|
876
|
+
// as canUseTool): host sessions are conversation-scoped, so the binding
|
|
877
|
+
// supplies the active conversation, turn, and turn responder that the
|
|
878
|
+
// semantic schemas leave to it.
|
|
879
|
+
mcpServers: {
|
|
880
|
+
[CANON_VERB_MCP_SERVER_NAME]: {
|
|
881
|
+
type: 'sdk',
|
|
882
|
+
name: CANON_VERB_MCP_SERVER_NAME,
|
|
883
|
+
instance: createCanonVerbMcpServer(() => client, () => {
|
|
884
|
+
const routing = resolveClaudeTurnResponseRouting(session.activeInput, config.ownerId);
|
|
885
|
+
return {
|
|
886
|
+
conversationId,
|
|
887
|
+
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
888
|
+
...(routing.responseUserId ? { responseUserId: routing.responseUserId } : {}),
|
|
889
|
+
};
|
|
890
|
+
}),
|
|
891
|
+
},
|
|
892
|
+
},
|
|
867
893
|
...(claudeCliPath ? { pathToClaudeCodeExecutable: claudeCliPath } : {}),
|
|
868
894
|
stderr: (data) => {
|
|
869
895
|
const trimmed = data.toString().trim();
|
|
@@ -979,7 +1005,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
979
1005
|
// instead of waiting for the next 30s heartbeat (lag fix).
|
|
980
1006
|
config.publishSessionSnapshots?.([conversationId]);
|
|
981
1007
|
}
|
|
982
|
-
return
|
|
1008
|
+
return buildClaudePlanAllowResult(input);
|
|
983
1009
|
}
|
|
984
1010
|
if (reply.status === 'reject') {
|
|
985
1011
|
return {
|
package/dist/mcp-args.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { DirectSessionSelection, SendContextualMessageOptions } from '@canonmsg/core';
|
|
2
1
|
export type McpParseResult<T> = {
|
|
3
2
|
ok: true;
|
|
4
3
|
value: T;
|
|
@@ -26,16 +25,6 @@ export interface SendMessageArgs {
|
|
|
26
25
|
durationMs?: number;
|
|
27
26
|
}
|
|
28
27
|
export declare function parseSendMessageArgs(value: unknown): McpParseResult<SendMessageArgs>;
|
|
29
|
-
export interface SendContextualMessageArgs {
|
|
30
|
-
sourceConversationId: string;
|
|
31
|
-
targetConversationId?: string;
|
|
32
|
-
targetUserId?: string;
|
|
33
|
-
text: string;
|
|
34
|
-
selfContext: SendContextualMessageOptions['selfContext'];
|
|
35
|
-
sessionSelection?: DirectSessionSelection;
|
|
36
|
-
messageOptions?: SendContextualMessageOptions['messageOptions'];
|
|
37
|
-
}
|
|
38
|
-
export declare function parseSendContextualMessageArgs(value: unknown): McpParseResult<SendContextualMessageArgs>;
|
|
39
28
|
export interface SetTypingArgs {
|
|
40
29
|
conversationId: string;
|
|
41
30
|
typing: boolean;
|
package/dist/mcp-args.js
CHANGED
|
@@ -87,74 +87,6 @@ export function parseSendMessageArgs(value) {
|
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
-
function parseSessionSelection(value) {
|
|
91
|
-
if (value == null)
|
|
92
|
-
return { ok: true, value: undefined };
|
|
93
|
-
const raw = typeof value === 'string'
|
|
94
|
-
? value
|
|
95
|
-
: isRecord(value) && typeof value.mode === 'string'
|
|
96
|
-
? value.mode
|
|
97
|
-
: null;
|
|
98
|
-
const mode = raw?.trim().toLowerCase().replace(/-/g, '_');
|
|
99
|
-
if (!mode)
|
|
100
|
-
return { ok: false, error: 'Invalid session_selection' };
|
|
101
|
-
if (mode === 'new' || mode === 'fresh' || mode === 'start_new') {
|
|
102
|
-
return { ok: true, value: { mode: 'new' } };
|
|
103
|
-
}
|
|
104
|
-
if (mode === 'continue_latest' || mode === 'latest' || mode === 'continue') {
|
|
105
|
-
return { ok: true, value: { mode: 'continue_latest' } };
|
|
106
|
-
}
|
|
107
|
-
if (mode === 'continue_or_create' || mode === 'auto' || mode === 'reuse_or_create') {
|
|
108
|
-
return { ok: true, value: { mode: 'continue_or_create' } };
|
|
109
|
-
}
|
|
110
|
-
return { ok: false, error: 'session_selection must be new, continue_latest, or continue_or_create' };
|
|
111
|
-
}
|
|
112
|
-
export function parseSendContextualMessageArgs(value) {
|
|
113
|
-
if (!isRecord(value))
|
|
114
|
-
return { ok: false, error: 'Arguments must be an object' };
|
|
115
|
-
const sourceConversationId = requiredString(value, 'source_conversation_id', 256);
|
|
116
|
-
if (!sourceConversationId.ok)
|
|
117
|
-
return sourceConversationId;
|
|
118
|
-
const text = requiredString(value, 'text');
|
|
119
|
-
if (!text.ok)
|
|
120
|
-
return text;
|
|
121
|
-
const targetConversationId = optionalString(value, 'target_conversation_id', 256);
|
|
122
|
-
const targetUserId = optionalString(value, 'target_user_id', 256);
|
|
123
|
-
if (Boolean(targetConversationId) === Boolean(targetUserId)) {
|
|
124
|
-
return { ok: false, error: 'Provide exactly one target_conversation_id or target_user_id' };
|
|
125
|
-
}
|
|
126
|
-
if (!isRecord(value.self_context)) {
|
|
127
|
-
return { ok: false, error: 'Missing self_context' };
|
|
128
|
-
}
|
|
129
|
-
const type = value.self_context.type;
|
|
130
|
-
const context = requiredString(value.self_context, 'context');
|
|
131
|
-
if (!context.ok)
|
|
132
|
-
return context;
|
|
133
|
-
if (type !== 'cross_session') {
|
|
134
|
-
return { ok: false, error: 'self_context.type must be cross_session' };
|
|
135
|
-
}
|
|
136
|
-
const sessionSelection = parseSessionSelection(value.session_selection);
|
|
137
|
-
if (!sessionSelection.ok)
|
|
138
|
-
return sessionSelection;
|
|
139
|
-
const messageOptions = isRecord(value.message_options)
|
|
140
|
-
? value.message_options
|
|
141
|
-
: undefined;
|
|
142
|
-
return {
|
|
143
|
-
ok: true,
|
|
144
|
-
value: {
|
|
145
|
-
sourceConversationId: sourceConversationId.value,
|
|
146
|
-
...(targetConversationId ? { targetConversationId } : {}),
|
|
147
|
-
...(targetUserId ? { targetUserId } : {}),
|
|
148
|
-
text: text.value,
|
|
149
|
-
selfContext: {
|
|
150
|
-
type: 'cross_session',
|
|
151
|
-
context: context.value,
|
|
152
|
-
},
|
|
153
|
-
...(sessionSelection.value ? { sessionSelection: sessionSelection.value } : {}),
|
|
154
|
-
...(messageOptions ? { messageOptions } : {}),
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
90
|
export function parseSetTypingArgs(value) {
|
|
159
91
|
if (!isRecord(value))
|
|
160
92
|
return { ok: false, error: 'Arguments must be an object' };
|
package/dist/server.js
CHANGED
|
@@ -14,7 +14,8 @@ import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActi
|
|
|
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';
|
|
17
|
-
import { parseReplyArgs,
|
|
17
|
+
import { parseReplyArgs, parseSendMessageArgs, parseSetTypingArgs, } from './mcp-args.js';
|
|
18
|
+
import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from '@canonmsg/agent-tools';
|
|
18
19
|
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
19
20
|
|
|
20
21
|
USAGE
|
|
@@ -137,58 +138,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
137
138
|
required: ['conversation_id'],
|
|
138
139
|
},
|
|
139
140
|
},
|
|
140
|
-
{
|
|
141
|
-
name: 'send_contextual_message',
|
|
142
|
-
description: 'Send a message into another Canon conversation or contact with private cross-session self-context',
|
|
143
|
-
inputSchema: {
|
|
144
|
-
type: 'object',
|
|
145
|
-
properties: {
|
|
146
|
-
source_conversation_id: {
|
|
147
|
-
type: 'string',
|
|
148
|
-
description: 'The Canon conversation where this cross-session intent originated',
|
|
149
|
-
},
|
|
150
|
-
target_conversation_id: {
|
|
151
|
-
type: 'string',
|
|
152
|
-
description: 'The destination Canon conversation to send into',
|
|
153
|
-
},
|
|
154
|
-
target_user_id: {
|
|
155
|
-
type: 'string',
|
|
156
|
-
description: 'The destination Canon user/contact to reach out to',
|
|
157
|
-
},
|
|
158
|
-
text: {
|
|
159
|
-
type: 'string',
|
|
160
|
-
description: 'Message text to send',
|
|
161
|
-
},
|
|
162
|
-
self_context: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
description: 'Required { type: "cross_session", context: "why I am saying this here" }',
|
|
165
|
-
},
|
|
166
|
-
session_selection: {
|
|
167
|
-
type: 'string',
|
|
168
|
-
enum: ['new', 'continue_latest', 'continue_or_create'],
|
|
169
|
-
description: 'Optional direct-agent session behavior when target_user_id is used',
|
|
170
|
-
},
|
|
171
|
-
message_options: {
|
|
172
|
-
type: 'object',
|
|
173
|
-
description: 'Optional Canon send options for the contextual outbound message',
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
required: ['source_conversation_id', 'text', 'self_context'],
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
name: 'list_conversations',
|
|
181
|
-
description: "List Canon conversations this agent participates in",
|
|
182
|
-
inputSchema: {
|
|
183
|
-
type: 'object',
|
|
184
|
-
properties: {
|
|
185
|
-
limit: {
|
|
186
|
-
type: 'number',
|
|
187
|
-
description: 'Max conversations to return (default: 20)',
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
141
|
{
|
|
193
142
|
name: 'list_agents',
|
|
194
143
|
description: 'List registered Canon agent profiles and their lock status',
|
|
@@ -220,6 +169,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
220
169
|
required: ['conversation_id', 'typing'],
|
|
221
170
|
},
|
|
222
171
|
},
|
|
172
|
+
// Canonical verbs — projections of canon.verbs.v1 (@canonmsg/agent-tools).
|
|
173
|
+
...canonVerbToolDefinitions(),
|
|
223
174
|
],
|
|
224
175
|
}));
|
|
225
176
|
// ── Tool handlers ──────────────────────────────────────────────────────
|
|
@@ -322,62 +273,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
322
273
|
],
|
|
323
274
|
};
|
|
324
275
|
}
|
|
325
|
-
case 'send_contextual_message': {
|
|
326
|
-
const parsed = parseSendContextualMessageArgs(args);
|
|
327
|
-
if (!parsed.ok)
|
|
328
|
-
return toolArgumentError(parsed.error);
|
|
329
|
-
const { sourceConversationId, targetConversationId, targetUserId, text, selfContext, sessionSelection, messageOptions, } = parsed.value;
|
|
330
|
-
const contextualMessageOptions = messageOptions;
|
|
331
|
-
const result = await client.sendContextualMessage({
|
|
332
|
-
sourceConversationId,
|
|
333
|
-
...(targetConversationId ? { targetConversationId } : {}),
|
|
334
|
-
...(targetUserId ? { targetUserId } : {}),
|
|
335
|
-
text,
|
|
336
|
-
selfContext,
|
|
337
|
-
...(sessionSelection ? { sessionSelection } : {}),
|
|
338
|
-
...(contextualMessageOptions
|
|
339
|
-
? {
|
|
340
|
-
messageOptions: {
|
|
341
|
-
...contextualMessageOptions,
|
|
342
|
-
metadata: {
|
|
343
|
-
...(contextualMessageOptions.metadata ?? {}),
|
|
344
|
-
...TURN_COMPLETE_METADATA,
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
}
|
|
348
|
-
: { messageOptions: { metadata: TURN_COMPLETE_METADATA } }),
|
|
349
|
-
});
|
|
350
|
-
return {
|
|
351
|
-
content: [
|
|
352
|
-
{
|
|
353
|
-
type: 'text',
|
|
354
|
-
text: result.status === 'messaged'
|
|
355
|
-
? `Contextual message sent (${result.messageId}) via self-context ${result.selfContextId}`
|
|
356
|
-
: `Contextual message not sent: ${result.status}`,
|
|
357
|
-
},
|
|
358
|
-
],
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
case 'list_conversations': {
|
|
362
|
-
const rawArgs = args && typeof args === 'object' ? args : {};
|
|
363
|
-
const limit = typeof rawArgs.limit === 'number' && Number.isFinite(rawArgs.limit)
|
|
364
|
-
? Math.max(1, Math.min(100, Math.floor(rawArgs.limit)))
|
|
365
|
-
: 20;
|
|
366
|
-
const convos = await client.getConversations();
|
|
367
|
-
const slice = convos.slice(0, limit);
|
|
368
|
-
const summary = slice.map((c) => ({
|
|
369
|
-
id: c.id,
|
|
370
|
-
type: c.type,
|
|
371
|
-
name: c.name,
|
|
372
|
-
members: c.memberIds.length,
|
|
373
|
-
lastMessage: c.lastMessage?.text?.slice(0, 100) || null,
|
|
374
|
-
}));
|
|
375
|
-
return {
|
|
376
|
-
content: [
|
|
377
|
-
{ type: 'text', text: JSON.stringify(summary, null, 2) },
|
|
378
|
-
],
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
276
|
case 'list_agents': {
|
|
382
277
|
const profiles = loadProfiles();
|
|
383
278
|
const entries = Object.entries(profiles).map(([name, p]) => {
|
|
@@ -413,13 +308,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
413
308
|
],
|
|
414
309
|
};
|
|
415
310
|
}
|
|
416
|
-
default:
|
|
311
|
+
default: {
|
|
312
|
+
const name = request.params.name;
|
|
313
|
+
if (isCanonToolVerb(name)) {
|
|
314
|
+
const rawArgs = args && typeof args === 'object' ? { ...args } : {};
|
|
315
|
+
const verbArgs = name === 'send_to' ? stampSendToTurnComplete(rawArgs) : rawArgs;
|
|
316
|
+
const verbResult = await executeCanonVerbTool(client, name, verbArgs);
|
|
317
|
+
return {
|
|
318
|
+
content: verbResult.content.map((item) => ({ type: 'text', text: item.text })),
|
|
319
|
+
...(verbResult.isError ? { isError: true } : {}),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
417
322
|
return {
|
|
418
323
|
content: [
|
|
419
|
-
{ type: 'text', text: `Unknown tool: ${
|
|
324
|
+
{ type: 'text', text: `Unknown tool: ${name}` },
|
|
420
325
|
],
|
|
421
326
|
isError: true,
|
|
422
327
|
};
|
|
328
|
+
}
|
|
423
329
|
}
|
|
424
330
|
});
|
|
425
331
|
// ── Inbound message handler ────────────────────────────────────────────
|
package/dist/tool-policy.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { ApprovalManager, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core';
|
|
2
2
|
export type ClaudeToolPolicyDecision = {
|
|
3
3
|
behavior: 'allow';
|
|
4
|
+
/** Replacement tool input (the SDK dispatches this instead). */
|
|
5
|
+
updatedInput?: Record<string, unknown>;
|
|
4
6
|
} | {
|
|
5
7
|
behavior: 'deny';
|
|
6
8
|
message: string;
|
|
7
9
|
};
|
|
8
10
|
export declare const CLAUDE_NATIVE_APPROVAL_PERMISSION_MODE = "default";
|
|
11
|
+
export declare function canonVerbFromToolName(toolName: string): string | null;
|
|
9
12
|
export declare function parseAllowedNonOwnerClaudeTools(value: string | null | undefined): string[];
|
|
10
13
|
export declare function isSensitiveNonOwnerClaudeTool(toolName: string, allowedNonOwnerTools?: Iterable<string>): boolean;
|
|
11
14
|
export declare function decideClaudeToolPermission(input: {
|
package/dist/tool-policy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const NON_OWNER_DENY_MESSAGE = 'Only the agent owner can authorize runtime tool use in this chat.';
|
|
2
|
+
const NON_OWNER_CROSS_CONVERSATION_DENY_MESSAGE = 'Non-owner turns may only target the conversation they came from.';
|
|
2
3
|
const NATIVE_APPROVAL_UNAVAILABLE_MESSAGE = 'Runtime action approval is not available for this conversation.';
|
|
3
4
|
const NATIVE_APPROVAL_DENIED_MESSAGE = 'The user denied this action.';
|
|
4
5
|
export const CLAUDE_NATIVE_APPROVAL_PERMISSION_MODE = 'default';
|
|
@@ -36,9 +37,52 @@ function isCanonOutboundTool(normalizedToolName) {
|
|
|
36
37
|
return normalizedToolName.endsWith('__reply')
|
|
37
38
|
|| normalizedToolName.endsWith('__send_message')
|
|
38
39
|
|| normalizedToolName.endsWith('__send_contextual_message')
|
|
40
|
+
|| normalizedToolName.endsWith('__send_to')
|
|
41
|
+
|| normalizedToolName.endsWith('__share_contact')
|
|
39
42
|
|| normalizedToolName === 'reply'
|
|
40
43
|
|| normalizedToolName === 'send_message'
|
|
41
|
-
|| normalizedToolName === 'send_contextual_message'
|
|
44
|
+
|| normalizedToolName === 'send_contextual_message'
|
|
45
|
+
|| normalizedToolName === 'send_to'
|
|
46
|
+
|| normalizedToolName === 'share_contact';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Canon's own verb tools (the in-process `canon` MCP mount): `mcp__canon__<verb>`.
|
|
50
|
+
* They need their own policy lane — the HITL verbs ARE the approval/input
|
|
51
|
+
* mechanism, so routing them through approvalManager.requestApproval would
|
|
52
|
+
* gate an approval card behind an approval card.
|
|
53
|
+
*/
|
|
54
|
+
const CANON_VERB_TOOL_PREFIX = 'mcp__canon__';
|
|
55
|
+
const CANON_HITL_OR_READ_VERBS = new Set([
|
|
56
|
+
'react',
|
|
57
|
+
'request_input',
|
|
58
|
+
'request_approval',
|
|
59
|
+
'check_approval',
|
|
60
|
+
'send_card',
|
|
61
|
+
'request_card',
|
|
62
|
+
'list_contacts',
|
|
63
|
+
'list_contact_requests',
|
|
64
|
+
'list_conversations',
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Verbs a NON-owner conversation member may trigger: same-conversation HITL
|
|
68
|
+
* and display surfaces, whose responder routing and owner-only escalation
|
|
69
|
+
* (secret/sudo, session rules) are enforced server-side. Cross-conversation
|
|
70
|
+
* verbs (send_to, share_contact) and the reads (contacts, conversation
|
|
71
|
+
* lists — they expose the agent's other relationships to this conversation)
|
|
72
|
+
* stay owner-only until the provenance trigger-token primitive lands.
|
|
73
|
+
*/
|
|
74
|
+
const CANON_NON_OWNER_ALLOWED_VERBS = new Set([
|
|
75
|
+
'request_input',
|
|
76
|
+
'request_approval',
|
|
77
|
+
'check_approval',
|
|
78
|
+
'send_card',
|
|
79
|
+
'request_card',
|
|
80
|
+
]);
|
|
81
|
+
export function canonVerbFromToolName(toolName) {
|
|
82
|
+
const normalized = normalizeToolName(toolName);
|
|
83
|
+
if (!normalized.startsWith(CANON_VERB_TOOL_PREFIX))
|
|
84
|
+
return null;
|
|
85
|
+
return normalized.slice(CANON_VERB_TOOL_PREFIX.length) || null;
|
|
42
86
|
}
|
|
43
87
|
function isMcpTool(normalizedToolName) {
|
|
44
88
|
return normalizedToolName.startsWith('mcp__');
|
|
@@ -100,12 +144,12 @@ export function classifyClaudeApprovalCategory(toolName) {
|
|
|
100
144
|
if (normalized === 'webfetch' || normalized === 'websearch') {
|
|
101
145
|
return 'network';
|
|
102
146
|
}
|
|
147
|
+
if (isCanonOutboundTool(normalized) || canonVerbFromToolName(normalized)) {
|
|
148
|
+
return 'canon';
|
|
149
|
+
}
|
|
103
150
|
if (isMcpTool(normalized) || normalized === 'listmcpresources' || normalized === 'readmcpresource') {
|
|
104
151
|
return 'mcp';
|
|
105
152
|
}
|
|
106
|
-
if (isCanonOutboundTool(normalized)) {
|
|
107
|
-
return 'canon';
|
|
108
|
-
}
|
|
109
153
|
return 'tool';
|
|
110
154
|
}
|
|
111
155
|
export function classifyClaudeApprovalRisk(toolName, toolInput) {
|
|
@@ -161,6 +205,39 @@ export function buildClaudeApprovalDetails(toolName, toolInput) {
|
|
|
161
205
|
return details;
|
|
162
206
|
}
|
|
163
207
|
export async function decideClaudeToolPermissionForMode(input) {
|
|
208
|
+
const canonVerb = canonVerbFromToolName(input.toolName);
|
|
209
|
+
if (canonVerb) {
|
|
210
|
+
// Non-owner turns: same-conversation HITL/display verbs are allowed
|
|
211
|
+
// (members may instruct the agent; the server enforces responder
|
|
212
|
+
// membership and owner-only escalation); everything cross-conversation
|
|
213
|
+
// stays owner-only. "Same-conversation" is ENFORCED here, not assumed:
|
|
214
|
+
// the server only checks agent membership in the target, so without this
|
|
215
|
+
// pin a member of conversation A could aim cards/questions at any other
|
|
216
|
+
// conversation the agent belongs to. A mismatching explicit target is
|
|
217
|
+
// denied; otherwise the turn's own conversation is stamped into the
|
|
218
|
+
// input (updatedInput is what the SDK actually dispatches).
|
|
219
|
+
if (!input.isOwnerTurn) {
|
|
220
|
+
if (CANON_NON_OWNER_ALLOWED_VERBS.has(canonVerb)) {
|
|
221
|
+
const target = input.toolInput.conversationId;
|
|
222
|
+
if (typeof target === 'string' && target !== input.conversationId) {
|
|
223
|
+
return { behavior: 'deny', message: NON_OWNER_CROSS_CONVERSATION_DENY_MESSAGE };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
behavior: 'allow',
|
|
227
|
+
updatedInput: { ...input.toolInput, conversationId: input.conversationId },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return { behavior: 'deny', message: NON_OWNER_DENY_MESSAGE };
|
|
231
|
+
}
|
|
232
|
+
// HITL + read verbs are allowed without an approval card: the HITL verbs
|
|
233
|
+
// themselves render the human-facing card (recursion otherwise), and the
|
|
234
|
+
// reads are side-effect free. Outbound verbs (send_to, share_contact)
|
|
235
|
+
// fall through to the mode policy — in native-approval mode they gate
|
|
236
|
+
// like any other side effect; in default mode owner turns allow.
|
|
237
|
+
if (CANON_HITL_OR_READ_VERBS.has(canonVerb)) {
|
|
238
|
+
return { behavior: 'allow' };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
164
241
|
if (input.permissionMode !== CLAUDE_NATIVE_APPROVAL_PERMISSION_MODE) {
|
|
165
242
|
return decideClaudeToolPermission({
|
|
166
243
|
toolName: input.toolName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
".claude-plugin"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host",
|
|
26
|
+
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../rich-cards ../agent-sdk ../coding-agent-host ../agent-tools",
|
|
27
27
|
"build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
28
28
|
"dev": "npm run prepare:workspace-deps && tsc --watch",
|
|
29
29
|
"prepack": "npm run build",
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.204",
|
|
34
|
-
"@canonmsg/agent-sdk": "^5.1.
|
|
34
|
+
"@canonmsg/agent-sdk": "^5.1.2",
|
|
35
35
|
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
36
|
-
"@canonmsg/
|
|
36
|
+
"@canonmsg/agent-tools": "^0.2.0",
|
|
37
|
+
"@canonmsg/core": "^4.3.0",
|
|
38
|
+
"@canonmsg/rich-cards": "^0.8.1",
|
|
37
39
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
38
40
|
},
|
|
39
41
|
"engines": {
|