@canonmsg/claude-code-plugin 0.33.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/agent-context-state.d.ts +15 -0
- package/dist/agent-context-state.js +35 -0
- package/dist/communication-policy.d.ts +2 -0
- package/dist/communication-policy.js +4 -0
- package/dist/host.d.ts +1 -3
- package/dist/host.js +78 -144
- package/dist/inbound-reply-authority.d.ts +42 -0
- package/dist/inbound-reply-authority.js +107 -0
- package/dist/mcp-args.d.ts +3 -0
- package/dist/mcp-args.js +12 -0
- package/dist/register.js +2 -0
- package/dist/server.js +246 -141
- package/dist/session-state.d.ts +6 -42
- package/dist/session-state.js +1 -71
- package/dist/tool-policy.js +0 -1
- package/package.json +5 -5
package/dist/mcp-args.d.ts
CHANGED
|
@@ -8,11 +8,13 @@ export type McpParseResult<T> = {
|
|
|
8
8
|
type ContentType = 'text' | 'image' | 'audio' | 'video' | 'file';
|
|
9
9
|
export interface ReplyArgs {
|
|
10
10
|
conversationId: string;
|
|
11
|
+
sourceMessageId: string;
|
|
11
12
|
text: string;
|
|
12
13
|
}
|
|
13
14
|
export declare function parseReplyArgs(value: unknown): McpParseResult<ReplyArgs>;
|
|
14
15
|
export interface SendMessageArgs {
|
|
15
16
|
conversationId: string;
|
|
17
|
+
sourceMessageId: string;
|
|
16
18
|
text: string;
|
|
17
19
|
contentType: ContentType;
|
|
18
20
|
imageUrl?: string;
|
|
@@ -27,6 +29,7 @@ export interface SendMessageArgs {
|
|
|
27
29
|
export declare function parseSendMessageArgs(value: unknown): McpParseResult<SendMessageArgs>;
|
|
28
30
|
export interface SetTypingArgs {
|
|
29
31
|
conversationId: string;
|
|
32
|
+
sourceMessageId: string;
|
|
30
33
|
typing: boolean;
|
|
31
34
|
status?: 'typing' | 'thinking';
|
|
32
35
|
}
|
package/dist/mcp-args.js
CHANGED
|
@@ -42,6 +42,9 @@ export function parseReplyArgs(value) {
|
|
|
42
42
|
const conversationId = requiredString(value, 'conversation_id', 256);
|
|
43
43
|
if (!conversationId.ok)
|
|
44
44
|
return conversationId;
|
|
45
|
+
const sourceMessageId = requiredString(value, 'source_message_id', 256);
|
|
46
|
+
if (!sourceMessageId.ok)
|
|
47
|
+
return sourceMessageId;
|
|
45
48
|
const text = requiredString(value, 'text');
|
|
46
49
|
if (!text.ok)
|
|
47
50
|
return text;
|
|
@@ -49,6 +52,7 @@ export function parseReplyArgs(value) {
|
|
|
49
52
|
ok: true,
|
|
50
53
|
value: {
|
|
51
54
|
conversationId: conversationId.value,
|
|
55
|
+
sourceMessageId: sourceMessageId.value,
|
|
52
56
|
text: text.value,
|
|
53
57
|
},
|
|
54
58
|
};
|
|
@@ -59,6 +63,9 @@ export function parseSendMessageArgs(value) {
|
|
|
59
63
|
const conversationId = requiredString(value, 'conversation_id', 256);
|
|
60
64
|
if (!conversationId.ok)
|
|
61
65
|
return conversationId;
|
|
66
|
+
const sourceMessageId = requiredString(value, 'source_message_id', 256);
|
|
67
|
+
if (!sourceMessageId.ok)
|
|
68
|
+
return sourceMessageId;
|
|
62
69
|
const contentType = parseContentType(value.content_type);
|
|
63
70
|
if (!contentType.ok)
|
|
64
71
|
return contentType;
|
|
@@ -74,6 +81,7 @@ export function parseSendMessageArgs(value) {
|
|
|
74
81
|
ok: true,
|
|
75
82
|
value: {
|
|
76
83
|
conversationId: conversationId.value,
|
|
84
|
+
sourceMessageId: sourceMessageId.value,
|
|
77
85
|
text: optionalString(value, 'text') ?? '',
|
|
78
86
|
contentType: contentType.value,
|
|
79
87
|
...(imageUrl ? { imageUrl } : {}),
|
|
@@ -93,6 +101,9 @@ export function parseSetTypingArgs(value) {
|
|
|
93
101
|
const conversationId = requiredString(value, 'conversation_id', 256);
|
|
94
102
|
if (!conversationId.ok)
|
|
95
103
|
return conversationId;
|
|
104
|
+
const sourceMessageId = requiredString(value, 'source_message_id', 256);
|
|
105
|
+
if (!sourceMessageId.ok)
|
|
106
|
+
return sourceMessageId;
|
|
96
107
|
if (typeof value.typing !== 'boolean') {
|
|
97
108
|
return { ok: false, error: 'typing must be a boolean' };
|
|
98
109
|
}
|
|
@@ -104,6 +115,7 @@ export function parseSetTypingArgs(value) {
|
|
|
104
115
|
ok: true,
|
|
105
116
|
value: {
|
|
106
117
|
conversationId: conversationId.value,
|
|
118
|
+
sourceMessageId: sourceMessageId.value,
|
|
107
119
|
typing: value.typing,
|
|
108
120
|
...(status ? { status } : {}),
|
|
109
121
|
},
|
package/dist/register.js
CHANGED
|
@@ -21,6 +21,7 @@ REQUIRED
|
|
|
21
21
|
|
|
22
22
|
FLAGS
|
|
23
23
|
--profile <name> Local profile name in ~/.canon/agents.json
|
|
24
|
+
--agent-id <id> Reconnect this exact transferred agent identity
|
|
24
25
|
--environment <id> Canon environment ID (or CANON_ENVIRONMENT_ID; default: canon-prod-v1)
|
|
25
26
|
--base-url <url> Canon API base URL override
|
|
26
27
|
--stream-url <url> Canon stream URL override
|
|
@@ -32,6 +33,7 @@ FLAGS
|
|
|
32
33
|
EXAMPLES
|
|
33
34
|
canon-register --name "My Claude" --description "Claude Code agent" --phone "+15551234567"
|
|
34
35
|
canon-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
36
|
+
canon-register --name "Sold Agent" --description "Existing agent" --phone "+15551234567" --agent-id <id>
|
|
35
37
|
|
|
36
38
|
After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/project.`;
|
|
37
39
|
const OPTIONS = {
|
package/dist/server.js
CHANGED
|
@@ -16,7 +16,10 @@ 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 { canonVerbToolDefinitions, createCanonCommunicationBinding, executeCanonVerbTool, isCanonToolVerb, } from '@canonmsg/agent-tools';
|
|
19
|
+
import { canonVerbToolDefinitions, createCanonCommunicationBinding, executeCanonVerbTool, isCanonToolVerb, isCommunicateReplacedVerb, } from '@canonmsg/agent-tools';
|
|
20
|
+
import { isProactiveCommunicationEnabled } from './communication-policy.js';
|
|
21
|
+
import { InboundReplyAuthority, InboundReplyAuthorityError, } from './inbound-reply-authority.js';
|
|
22
|
+
import { AgentContextState } from './agent-context-state.js';
|
|
20
23
|
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
21
24
|
|
|
22
25
|
USAGE
|
|
@@ -31,13 +34,12 @@ phone-controlled local sessions, use canon-claude instead.`;
|
|
|
31
34
|
// ── MCP server ─────────────────────────────────────────────────────────
|
|
32
35
|
const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
|
|
33
36
|
capabilities: {
|
|
34
|
-
tools: {},
|
|
37
|
+
tools: { listChanged: true },
|
|
35
38
|
experimental: { 'claude/channel': {} },
|
|
36
39
|
},
|
|
37
40
|
});
|
|
38
41
|
let client = null;
|
|
39
42
|
let stream = null;
|
|
40
|
-
let agentContext = null;
|
|
41
43
|
let approvalManager = null;
|
|
42
44
|
let approvalServer = null;
|
|
43
45
|
let rtdb = null;
|
|
@@ -46,10 +48,32 @@ const TURN_COMPLETE_METADATA = {
|
|
|
46
48
|
turnSemantics: 'turn_complete',
|
|
47
49
|
};
|
|
48
50
|
const communicationBinding = createCanonCommunicationBinding();
|
|
51
|
+
const inboundReplyAuthority = new InboundReplyAuthority();
|
|
52
|
+
const agentContextState = new AgentContextState(() => {
|
|
53
|
+
void server.sendToolListChanged().catch((error) => {
|
|
54
|
+
console.error(`[canon] Failed to refresh communication tools: ${error instanceof Error ? error.message : error}`);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
49
57
|
/** Last owner-visible conversation to route approval cards into. */
|
|
50
58
|
let approvalConversationId = null;
|
|
51
59
|
/** Whether the message that most recently triggered work came from the owner. */
|
|
52
60
|
let triggerIsOwner = true;
|
|
61
|
+
const INBOUND_SOURCE_PROPERTY = {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'The source_message_id from the inbound Canon channel metadata',
|
|
64
|
+
};
|
|
65
|
+
function requireInboundSource(conversationId, sourceMessageId) {
|
|
66
|
+
if (typeof conversationId !== 'string'
|
|
67
|
+
|| !conversationId
|
|
68
|
+
|| typeof sourceMessageId !== 'string'
|
|
69
|
+
|| !sourceMessageId) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return { conversationId, sourceMessageId };
|
|
73
|
+
}
|
|
74
|
+
function replyAuthorityError() {
|
|
75
|
+
return toolArgumentError('This action must target the exact current inbound Canon message and may only reply once.');
|
|
76
|
+
}
|
|
53
77
|
function getPrimaryAttachment(message) {
|
|
54
78
|
const attachment = message.attachments?.[0];
|
|
55
79
|
if (attachment?.url)
|
|
@@ -66,119 +90,144 @@ function toolArgumentError(error) {
|
|
|
66
90
|
};
|
|
67
91
|
}
|
|
68
92
|
// ── Tool definitions ───────────────────────────────────────────────────
|
|
69
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
94
|
+
await agentContextState.ready;
|
|
95
|
+
return {
|
|
96
|
+
tools: [
|
|
97
|
+
{
|
|
98
|
+
name: 'reply',
|
|
99
|
+
description: 'Reply to the current inbound Canon conversation',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
conversation_id: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'The conversation ID to reply to',
|
|
106
|
+
},
|
|
107
|
+
source_message_id: INBOUND_SOURCE_PROPERTY,
|
|
108
|
+
text: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'Message text to send',
|
|
111
|
+
},
|
|
84
112
|
},
|
|
113
|
+
required: ['conversation_id', 'source_message_id', 'text'],
|
|
85
114
|
},
|
|
86
|
-
required: ['conversation_id', 'text'],
|
|
87
115
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
116
|
+
{
|
|
117
|
+
name: 'send_message',
|
|
118
|
+
description: 'Reply with text or media in the current inbound Canon conversation',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
conversation_id: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'The conversation ID',
|
|
125
|
+
},
|
|
126
|
+
source_message_id: INBOUND_SOURCE_PROPERTY,
|
|
127
|
+
text: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Message text',
|
|
130
|
+
},
|
|
131
|
+
content_type: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
enum: ['text', 'image', 'audio', 'video', 'file'],
|
|
134
|
+
description: 'Content type (default: text)',
|
|
135
|
+
},
|
|
136
|
+
image_url: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Image URL (when content_type is image)',
|
|
139
|
+
},
|
|
140
|
+
audio_url: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'Audio URL (when content_type is audio)',
|
|
143
|
+
},
|
|
144
|
+
video_url: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: 'Video URL (when content_type is video)',
|
|
147
|
+
},
|
|
148
|
+
file_url: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'File URL (when content_type is file)',
|
|
151
|
+
},
|
|
152
|
+
media_path: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
description: 'Local image/audio/video/file path to upload and send',
|
|
155
|
+
},
|
|
156
|
+
file_name: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'Optional display filename for file attachments',
|
|
159
|
+
},
|
|
160
|
+
mime_type: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'Optional MIME type for media_path uploads',
|
|
163
|
+
},
|
|
164
|
+
duration_ms: {
|
|
165
|
+
type: 'number',
|
|
166
|
+
description: 'Optional audio duration in milliseconds for media_path uploads',
|
|
167
|
+
},
|
|
139
168
|
},
|
|
169
|
+
required: ['conversation_id', 'source_message_id'],
|
|
140
170
|
},
|
|
141
|
-
required: ['conversation_id'],
|
|
142
171
|
},
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
{
|
|
173
|
+
name: 'list_agents',
|
|
174
|
+
description: 'List registered Canon agent profiles and their lock status',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {},
|
|
178
|
+
},
|
|
150
179
|
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
{
|
|
181
|
+
name: 'set_typing',
|
|
182
|
+
description: 'Show or hide typing indicator in a Canon conversation',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
conversation_id: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: 'The conversation ID',
|
|
189
|
+
},
|
|
190
|
+
source_message_id: INBOUND_SOURCE_PROPERTY,
|
|
191
|
+
typing: {
|
|
192
|
+
type: 'boolean',
|
|
193
|
+
description: 'Whether to show typing indicator',
|
|
194
|
+
},
|
|
195
|
+
status: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
enum: ['typing', 'thinking'],
|
|
198
|
+
description: 'Typing status (default: typing). Use "thinking" when processing/reasoning.',
|
|
199
|
+
},
|
|
170
200
|
},
|
|
201
|
+
required: ['conversation_id', 'source_message_id', 'typing'],
|
|
171
202
|
},
|
|
172
|
-
required: ['conversation_id', 'typing'],
|
|
173
203
|
},
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
204
|
+
// Canonical verbs — projections of canon.verbs.v1 (@canonmsg/agent-tools).
|
|
205
|
+
// Channel mode owns no turn, so `no_reply` is an ack-only no-op here: the
|
|
206
|
+
// standalone session has no final delivery for it to suppress.
|
|
207
|
+
...canonVerbToolDefinitions({ compactCommunication: true }).map((tool) => ({
|
|
208
|
+
...tool,
|
|
209
|
+
description: `${tool.description} Use source_message_id from the inbound Canon channel metadata.`,
|
|
210
|
+
inputSchema: {
|
|
211
|
+
...tool.inputSchema,
|
|
212
|
+
properties: {
|
|
213
|
+
...(tool.inputSchema.properties ?? {}),
|
|
214
|
+
source_message_id: INBOUND_SOURCE_PROPERTY,
|
|
215
|
+
},
|
|
216
|
+
required: [
|
|
217
|
+
...new Set([
|
|
218
|
+
...(tool.inputSchema.required ?? []),
|
|
219
|
+
'conversationId',
|
|
220
|
+
'source_message_id',
|
|
221
|
+
]),
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
})),
|
|
225
|
+
...(isProactiveCommunicationEnabled(agentContextState.current?.outboundPolicy)
|
|
226
|
+
? communicationBinding.tools
|
|
227
|
+
: []),
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
});
|
|
182
231
|
// ── Tool handlers ──────────────────────────────────────────────────────
|
|
183
232
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
184
233
|
if (!client) {
|
|
@@ -193,10 +242,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
193
242
|
const parsed = parseReplyArgs(args);
|
|
194
243
|
if (!parsed.ok)
|
|
195
244
|
return toolArgumentError(parsed.error);
|
|
196
|
-
const { conversationId, text } = parsed.value;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
245
|
+
const { conversationId, sourceMessageId, text } = parsed.value;
|
|
246
|
+
let result;
|
|
247
|
+
try {
|
|
248
|
+
result = await inboundReplyAuthority.sendVisibleReply({ conversationId, sourceMessageId }, (replyAuthority) => client.sendMessage(conversationId, text, {
|
|
249
|
+
metadata: {
|
|
250
|
+
...TURN_COMPLETE_METADATA,
|
|
251
|
+
turnId: `claude-channel:${sourceMessageId}`,
|
|
252
|
+
},
|
|
253
|
+
...(replyAuthority ? { replyAuthority } : {}),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof InboundReplyAuthorityError)
|
|
258
|
+
return replyAuthorityError();
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
200
261
|
return {
|
|
201
262
|
content: [
|
|
202
263
|
{
|
|
@@ -210,25 +271,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
210
271
|
const parsed = parseSendMessageArgs(args);
|
|
211
272
|
if (!parsed.ok)
|
|
212
273
|
return toolArgumentError(parsed.error);
|
|
213
|
-
const { conversationId, text, contentType, imageUrl, audioUrl, videoUrl, fileUrl, mediaPath, fileName, mimeType, durationMs, } = parsed.value;
|
|
274
|
+
const { conversationId, sourceMessageId, text, contentType, imageUrl, audioUrl, videoUrl, fileUrl, mediaPath, fileName, mimeType, durationMs, } = parsed.value;
|
|
214
275
|
const opts = {};
|
|
215
276
|
const metadata = TURN_COMPLETE_METADATA;
|
|
216
|
-
if (mediaPath) {
|
|
217
|
-
const result = await sendMediaFileMessage(client, conversationId, mediaPath, text, {
|
|
218
|
-
...(fileName ? { fileName } : {}),
|
|
219
|
-
...(mimeType ? { mimeType } : {}),
|
|
220
|
-
...(durationMs != null ? { durationMs } : {}),
|
|
221
|
-
metadata,
|
|
222
|
-
});
|
|
223
|
-
return {
|
|
224
|
-
content: [
|
|
225
|
-
{
|
|
226
|
-
type: 'text',
|
|
227
|
-
text: `Message sent (${result.messageId})`,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
277
|
if (contentType === 'image' && imageUrl) {
|
|
233
278
|
opts.contentType = 'image';
|
|
234
279
|
opts.attachments = [{ kind: 'image', url: imageUrl }];
|
|
@@ -269,7 +314,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
269
314
|
...(opts.metadata ?? {}),
|
|
270
315
|
...metadata,
|
|
271
316
|
};
|
|
272
|
-
|
|
317
|
+
let result;
|
|
318
|
+
try {
|
|
319
|
+
result = await inboundReplyAuthority.sendVisibleReply({ conversationId, sourceMessageId }, (replyAuthority) => mediaPath
|
|
320
|
+
? sendMediaFileMessage(client, conversationId, mediaPath, text, {
|
|
321
|
+
...(fileName ? { fileName } : {}),
|
|
322
|
+
...(mimeType ? { mimeType } : {}),
|
|
323
|
+
...(durationMs != null ? { durationMs } : {}),
|
|
324
|
+
metadata: {
|
|
325
|
+
...metadata,
|
|
326
|
+
turnId: `claude-channel:${sourceMessageId}`,
|
|
327
|
+
},
|
|
328
|
+
...(replyAuthority ? { replyAuthority } : {}),
|
|
329
|
+
})
|
|
330
|
+
: client.sendMessage(conversationId, text, {
|
|
331
|
+
...opts,
|
|
332
|
+
metadata: {
|
|
333
|
+
...metadata,
|
|
334
|
+
turnId: `claude-channel:${sourceMessageId}`,
|
|
335
|
+
},
|
|
336
|
+
...(replyAuthority ? { replyAuthority } : {}),
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (error instanceof InboundReplyAuthorityError)
|
|
341
|
+
return replyAuthorityError();
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
273
344
|
return {
|
|
274
345
|
content: [
|
|
275
346
|
{
|
|
@@ -303,7 +374,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
303
374
|
const parsed = parseSetTypingArgs(args);
|
|
304
375
|
if (!parsed.ok)
|
|
305
376
|
return toolArgumentError(parsed.error);
|
|
306
|
-
const { conversationId, typing, status } = parsed.value;
|
|
377
|
+
const { conversationId, sourceMessageId, typing, status } = parsed.value;
|
|
378
|
+
if (!inboundReplyAuthority.validate({ conversationId, sourceMessageId })) {
|
|
379
|
+
return replyAuthorityError();
|
|
380
|
+
}
|
|
307
381
|
await client.setTyping(conversationId, typing, status);
|
|
308
382
|
return {
|
|
309
383
|
content: [
|
|
@@ -317,15 +391,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
317
391
|
default: {
|
|
318
392
|
const name = request.params.name;
|
|
319
393
|
if (communicationBinding.isToolName(name)) {
|
|
394
|
+
if (!isProactiveCommunicationEnabled(agentContextState.current?.outboundPolicy)) {
|
|
395
|
+
return toolArgumentError('Outbound communication is not enabled by the agent operator.');
|
|
396
|
+
}
|
|
320
397
|
const result = await communicationBinding.execute(client, name, args);
|
|
321
398
|
return {
|
|
322
399
|
content: result.content.map((item) => ({ type: 'text', text: item.text })),
|
|
323
400
|
...(result.isError ? { isError: true } : {}),
|
|
324
401
|
};
|
|
325
402
|
}
|
|
326
|
-
if (isCanonToolVerb(name)) {
|
|
403
|
+
if (isCanonToolVerb(name) && !isCommunicateReplacedVerb(name)) {
|
|
327
404
|
const rawArgs = args && typeof args === 'object' ? { ...args } : {};
|
|
328
|
-
const
|
|
405
|
+
const source = requireInboundSource(rawArgs.conversationId, rawArgs.source_message_id);
|
|
406
|
+
delete rawArgs.source_message_id;
|
|
407
|
+
if (!source)
|
|
408
|
+
return replyAuthorityError();
|
|
409
|
+
if (name === 'no_reply') {
|
|
410
|
+
if (!inboundReplyAuthority.consumeNoReply(source))
|
|
411
|
+
return replyAuthorityError();
|
|
412
|
+
}
|
|
413
|
+
else if (!inboundReplyAuthority.validate(source)) {
|
|
414
|
+
return replyAuthorityError();
|
|
415
|
+
}
|
|
416
|
+
const verbResult = await executeCanonVerbTool(client, name, rawArgs, {
|
|
417
|
+
context: {
|
|
418
|
+
conversationId: source.conversationId,
|
|
419
|
+
sourceMessageId: source.sourceMessageId,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
329
422
|
return {
|
|
330
423
|
content: verbResult.content.map((item) => ({ type: 'text', text: item.text })),
|
|
331
424
|
...(verbResult.isError ? { isError: true } : {}),
|
|
@@ -344,6 +437,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
344
437
|
async function handleInboundMessage(payload) {
|
|
345
438
|
const m = payload.message;
|
|
346
439
|
const convo = conversationCache.get(payload.conversationId);
|
|
440
|
+
const pendingReplySource = inboundReplyAuthority.beginInbound({
|
|
441
|
+
conversationId: payload.conversationId,
|
|
442
|
+
sourceMessageId: m.id,
|
|
443
|
+
...(payload.replyAuthority ? { replyAuthority: payload.replyAuthority } : {}),
|
|
444
|
+
});
|
|
347
445
|
// Track turn provenance separately from the owner-visible approval route.
|
|
348
446
|
// Non-owner direct chats do not include the owner, so approval cards still go
|
|
349
447
|
// to the last owner conversation while the hook bypasses cached session rules.
|
|
@@ -362,9 +460,11 @@ async function handleInboundMessage(payload) {
|
|
|
362
460
|
return;
|
|
363
461
|
}
|
|
364
462
|
}
|
|
463
|
+
inboundReplyAuthority.authorize(pendingReplySource);
|
|
365
464
|
// meta must be Record<string, string> — non-string values cause silent drops
|
|
366
465
|
const meta = {
|
|
367
466
|
conversation_id: payload.conversationId,
|
|
467
|
+
source_message_id: m.id,
|
|
368
468
|
sender_id: m.senderId,
|
|
369
469
|
sender_name: m.senderName || m.senderId,
|
|
370
470
|
sender_type: m.senderType || 'human',
|
|
@@ -406,7 +506,7 @@ async function handleInboundMessage(payload) {
|
|
|
406
506
|
id: m.id,
|
|
407
507
|
attachments: m.attachments ?? [],
|
|
408
508
|
}, {
|
|
409
|
-
agentId:
|
|
509
|
+
agentId: agentContextState.current?.agentId ?? 'claude-code',
|
|
410
510
|
conversationId: payload.conversationId,
|
|
411
511
|
});
|
|
412
512
|
}
|
|
@@ -449,6 +549,7 @@ async function handleInboundMessage(payload) {
|
|
|
449
549
|
console.error('[canon] Notification sent to Claude Code');
|
|
450
550
|
}
|
|
451
551
|
catch (err) {
|
|
552
|
+
inboundReplyAuthority.revoke(pendingReplySource);
|
|
452
553
|
const msg = err instanceof Error ? err.message : String(err);
|
|
453
554
|
console.error(`[canon] Failed to send notification: ${msg}`);
|
|
454
555
|
}
|
|
@@ -469,6 +570,7 @@ async function startChannel() {
|
|
|
469
570
|
await verifyResolvedAgentEnvironment(resolvedRuntime);
|
|
470
571
|
}
|
|
471
572
|
catch (err) {
|
|
573
|
+
agentContextState.settleUnavailable();
|
|
472
574
|
resolvedRuntime?.lockHandle?.release();
|
|
473
575
|
console.error(err instanceof Error ? err.message : err);
|
|
474
576
|
return;
|
|
@@ -480,18 +582,21 @@ async function startChannel() {
|
|
|
480
582
|
// Get agent identity — try /agents/me first, fall back to /agents/auth-token
|
|
481
583
|
let agentId;
|
|
482
584
|
try {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
585
|
+
const context = await client.getAgentMe();
|
|
586
|
+
agentContextState.set(context);
|
|
587
|
+
agentId = context.agentId;
|
|
588
|
+
console.error(`[canon] Connected as ${context.displayName || agentId}`);
|
|
486
589
|
}
|
|
487
590
|
catch {
|
|
488
591
|
// /agents/me may not be deployed — fall back to auth-token exchange
|
|
489
592
|
try {
|
|
490
593
|
const auth = await client.getAuthToken();
|
|
491
594
|
agentId = auth.agentId;
|
|
595
|
+
agentContextState.settleUnavailable();
|
|
492
596
|
console.error(`[canon] Authenticated as ${agentId}`);
|
|
493
597
|
}
|
|
494
598
|
catch (err) {
|
|
599
|
+
agentContextState.settleUnavailable();
|
|
495
600
|
const msg = err instanceof Error ? err.message : String(err);
|
|
496
601
|
console.error(`[canon] Failed to authenticate: ${msg}`);
|
|
497
602
|
return;
|
|
@@ -513,7 +618,7 @@ async function startChannel() {
|
|
|
513
618
|
runtime: 'claude-code',
|
|
514
619
|
profile,
|
|
515
620
|
agentId,
|
|
516
|
-
agentName:
|
|
621
|
+
agentName: agentContextState.current?.displayName ?? profileAgentName,
|
|
517
622
|
cwd: process.cwd(),
|
|
518
623
|
launchCommand: ['canon-channel-server'],
|
|
519
624
|
pid: process.pid,
|
|
@@ -546,7 +651,7 @@ async function startChannel() {
|
|
|
546
651
|
handler: {
|
|
547
652
|
onMessage: handleInboundMessage,
|
|
548
653
|
onAgentContext: (ctx) => {
|
|
549
|
-
|
|
654
|
+
agentContextState.set(ctx);
|
|
550
655
|
},
|
|
551
656
|
onConnected: () => {
|
|
552
657
|
console.error('[canon] SSE stream connected');
|
|
@@ -564,8 +669,8 @@ async function startChannel() {
|
|
|
564
669
|
console.error(`[canon] SSE start error: ${msg}`);
|
|
565
670
|
});
|
|
566
671
|
// Start approval system
|
|
567
|
-
if (
|
|
568
|
-
approvalManager = new ApprovalManager(client, agentId,
|
|
672
|
+
if (agentContextState.current?.ownerId) {
|
|
673
|
+
approvalManager = new ApprovalManager(client, agentId, agentContextState.current.ownerId);
|
|
569
674
|
approvalServer = new ApprovalHttpServer(approvalManager, () => approvalConversationId, {
|
|
570
675
|
token: process.env.CANON_CLAUDE_APPROVAL_TOKEN,
|
|
571
676
|
getIsOwnerTurn: () => triggerIsOwner,
|
|
@@ -590,9 +695,9 @@ export async function main() {
|
|
|
590
695
|
// Graceful shutdown — release agent lock and clear session state
|
|
591
696
|
const shutdown = () => {
|
|
592
697
|
// Mark all conversations as inactive
|
|
593
|
-
if (
|
|
698
|
+
if (agentContextState.current) {
|
|
594
699
|
for (const convoId of conversationCache.keys()) {
|
|
595
|
-
rtdb?.clearSessionState(convoId,
|
|
700
|
+
rtdb?.clearSessionState(convoId, agentContextState.current.agentId).catch(() => { });
|
|
596
701
|
}
|
|
597
702
|
}
|
|
598
703
|
approvalManager?.dispose();
|