@canonmsg/claude-code-plugin 0.32.0 → 0.34.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/agent-context-state.d.ts +15 -0
- package/dist/agent-context-state.js +35 -0
- package/dist/canon-user-content.d.ts +3 -3
- package/dist/canon-user-content.js +4 -4
- package/dist/communication-policy.d.ts +2 -0
- package/dist/communication-policy.js +4 -0
- package/dist/host.d.ts +2 -2
- package/dist/host.js +128 -259
- 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 -1
- package/dist/server.js +253 -144
- package/dist/session-state.d.ts +6 -46
- package/dist/session-state.js +1 -74
- package/dist/tool-policy.js +2 -7
- package/package.json +6 -6
package/dist/server.js
CHANGED
|
@@ -16,8 +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 {
|
|
20
|
-
|
|
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';
|
|
21
23
|
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
22
24
|
|
|
23
25
|
USAGE
|
|
@@ -32,13 +34,12 @@ phone-controlled local sessions, use canon-claude instead.`;
|
|
|
32
34
|
// ── MCP server ─────────────────────────────────────────────────────────
|
|
33
35
|
const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
|
|
34
36
|
capabilities: {
|
|
35
|
-
tools: {},
|
|
37
|
+
tools: { listChanged: true },
|
|
36
38
|
experimental: { 'claude/channel': {} },
|
|
37
39
|
},
|
|
38
40
|
});
|
|
39
41
|
let client = null;
|
|
40
42
|
let stream = null;
|
|
41
|
-
let agentContext = null;
|
|
42
43
|
let approvalManager = null;
|
|
43
44
|
let approvalServer = null;
|
|
44
45
|
let rtdb = null;
|
|
@@ -46,10 +47,33 @@ const conversationCache = new Map();
|
|
|
46
47
|
const TURN_COMPLETE_METADATA = {
|
|
47
48
|
turnSemantics: 'turn_complete',
|
|
48
49
|
};
|
|
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,118 +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
|
-
|
|
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
|
+
});
|
|
181
231
|
// ── Tool handlers ──────────────────────────────────────────────────────
|
|
182
232
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
183
233
|
if (!client) {
|
|
@@ -192,10 +242,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
192
242
|
const parsed = parseReplyArgs(args);
|
|
193
243
|
if (!parsed.ok)
|
|
194
244
|
return toolArgumentError(parsed.error);
|
|
195
|
-
const { conversationId, text } = parsed.value;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
199
261
|
return {
|
|
200
262
|
content: [
|
|
201
263
|
{
|
|
@@ -209,25 +271,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
209
271
|
const parsed = parseSendMessageArgs(args);
|
|
210
272
|
if (!parsed.ok)
|
|
211
273
|
return toolArgumentError(parsed.error);
|
|
212
|
-
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;
|
|
213
275
|
const opts = {};
|
|
214
276
|
const metadata = TURN_COMPLETE_METADATA;
|
|
215
|
-
if (mediaPath) {
|
|
216
|
-
const result = await sendMediaFileMessage(client, conversationId, mediaPath, text, {
|
|
217
|
-
...(fileName ? { fileName } : {}),
|
|
218
|
-
...(mimeType ? { mimeType } : {}),
|
|
219
|
-
...(durationMs != null ? { durationMs } : {}),
|
|
220
|
-
metadata,
|
|
221
|
-
});
|
|
222
|
-
return {
|
|
223
|
-
content: [
|
|
224
|
-
{
|
|
225
|
-
type: 'text',
|
|
226
|
-
text: `Message sent (${result.messageId})`,
|
|
227
|
-
},
|
|
228
|
-
],
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
277
|
if (contentType === 'image' && imageUrl) {
|
|
232
278
|
opts.contentType = 'image';
|
|
233
279
|
opts.attachments = [{ kind: 'image', url: imageUrl }];
|
|
@@ -268,7 +314,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
268
314
|
...(opts.metadata ?? {}),
|
|
269
315
|
...metadata,
|
|
270
316
|
};
|
|
271
|
-
|
|
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
|
+
}
|
|
272
344
|
return {
|
|
273
345
|
content: [
|
|
274
346
|
{
|
|
@@ -302,7 +374,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
302
374
|
const parsed = parseSetTypingArgs(args);
|
|
303
375
|
if (!parsed.ok)
|
|
304
376
|
return toolArgumentError(parsed.error);
|
|
305
|
-
const { conversationId, typing, status } = parsed.value;
|
|
377
|
+
const { conversationId, sourceMessageId, typing, status } = parsed.value;
|
|
378
|
+
if (!inboundReplyAuthority.validate({ conversationId, sourceMessageId })) {
|
|
379
|
+
return replyAuthorityError();
|
|
380
|
+
}
|
|
306
381
|
await client.setTyping(conversationId, typing, status);
|
|
307
382
|
return {
|
|
308
383
|
content: [
|
|
@@ -315,13 +390,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
315
390
|
}
|
|
316
391
|
default: {
|
|
317
392
|
const name = request.params.name;
|
|
318
|
-
if (
|
|
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
|
+
}
|
|
397
|
+
const result = await communicationBinding.execute(client, name, args);
|
|
398
|
+
return {
|
|
399
|
+
content: result.content.map((item) => ({ type: 'text', text: item.text })),
|
|
400
|
+
...(result.isError ? { isError: true } : {}),
|
|
401
|
+
};
|
|
320
402
|
}
|
|
321
|
-
if (isCanonToolVerb(name)) {
|
|
403
|
+
if (isCanonToolVerb(name) && !isCommunicateReplacedVerb(name)) {
|
|
322
404
|
const rawArgs = args && typeof args === 'object' ? { ...args } : {};
|
|
323
|
-
const
|
|
324
|
-
|
|
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
|
+
});
|
|
325
422
|
return {
|
|
326
423
|
content: verbResult.content.map((item) => ({ type: 'text', text: item.text })),
|
|
327
424
|
...(verbResult.isError ? { isError: true } : {}),
|
|
@@ -340,6 +437,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
340
437
|
async function handleInboundMessage(payload) {
|
|
341
438
|
const m = payload.message;
|
|
342
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
|
+
});
|
|
343
445
|
// Track turn provenance separately from the owner-visible approval route.
|
|
344
446
|
// Non-owner direct chats do not include the owner, so approval cards still go
|
|
345
447
|
// to the last owner conversation while the hook bypasses cached session rules.
|
|
@@ -358,9 +460,11 @@ async function handleInboundMessage(payload) {
|
|
|
358
460
|
return;
|
|
359
461
|
}
|
|
360
462
|
}
|
|
463
|
+
inboundReplyAuthority.authorize(pendingReplySource);
|
|
361
464
|
// meta must be Record<string, string> — non-string values cause silent drops
|
|
362
465
|
const meta = {
|
|
363
466
|
conversation_id: payload.conversationId,
|
|
467
|
+
source_message_id: m.id,
|
|
364
468
|
sender_id: m.senderId,
|
|
365
469
|
sender_name: m.senderName || m.senderId,
|
|
366
470
|
sender_type: m.senderType || 'human',
|
|
@@ -402,7 +506,7 @@ async function handleInboundMessage(payload) {
|
|
|
402
506
|
id: m.id,
|
|
403
507
|
attachments: m.attachments ?? [],
|
|
404
508
|
}, {
|
|
405
|
-
agentId:
|
|
509
|
+
agentId: agentContextState.current?.agentId ?? 'claude-code',
|
|
406
510
|
conversationId: payload.conversationId,
|
|
407
511
|
});
|
|
408
512
|
}
|
|
@@ -445,6 +549,7 @@ async function handleInboundMessage(payload) {
|
|
|
445
549
|
console.error('[canon] Notification sent to Claude Code');
|
|
446
550
|
}
|
|
447
551
|
catch (err) {
|
|
552
|
+
inboundReplyAuthority.revoke(pendingReplySource);
|
|
448
553
|
const msg = err instanceof Error ? err.message : String(err);
|
|
449
554
|
console.error(`[canon] Failed to send notification: ${msg}`);
|
|
450
555
|
}
|
|
@@ -465,6 +570,7 @@ async function startChannel() {
|
|
|
465
570
|
await verifyResolvedAgentEnvironment(resolvedRuntime);
|
|
466
571
|
}
|
|
467
572
|
catch (err) {
|
|
573
|
+
agentContextState.settleUnavailable();
|
|
468
574
|
resolvedRuntime?.lockHandle?.release();
|
|
469
575
|
console.error(err instanceof Error ? err.message : err);
|
|
470
576
|
return;
|
|
@@ -476,18 +582,21 @@ async function startChannel() {
|
|
|
476
582
|
// Get agent identity — try /agents/me first, fall back to /agents/auth-token
|
|
477
583
|
let agentId;
|
|
478
584
|
try {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
585
|
+
const context = await client.getAgentMe();
|
|
586
|
+
agentContextState.set(context);
|
|
587
|
+
agentId = context.agentId;
|
|
588
|
+
console.error(`[canon] Connected as ${context.displayName || agentId}`);
|
|
482
589
|
}
|
|
483
590
|
catch {
|
|
484
591
|
// /agents/me may not be deployed — fall back to auth-token exchange
|
|
485
592
|
try {
|
|
486
593
|
const auth = await client.getAuthToken();
|
|
487
594
|
agentId = auth.agentId;
|
|
595
|
+
agentContextState.settleUnavailable();
|
|
488
596
|
console.error(`[canon] Authenticated as ${agentId}`);
|
|
489
597
|
}
|
|
490
598
|
catch (err) {
|
|
599
|
+
agentContextState.settleUnavailable();
|
|
491
600
|
const msg = err instanceof Error ? err.message : String(err);
|
|
492
601
|
console.error(`[canon] Failed to authenticate: ${msg}`);
|
|
493
602
|
return;
|
|
@@ -509,7 +618,7 @@ async function startChannel() {
|
|
|
509
618
|
runtime: 'claude-code',
|
|
510
619
|
profile,
|
|
511
620
|
agentId,
|
|
512
|
-
agentName:
|
|
621
|
+
agentName: agentContextState.current?.displayName ?? profileAgentName,
|
|
513
622
|
cwd: process.cwd(),
|
|
514
623
|
launchCommand: ['canon-channel-server'],
|
|
515
624
|
pid: process.pid,
|
|
@@ -542,7 +651,7 @@ async function startChannel() {
|
|
|
542
651
|
handler: {
|
|
543
652
|
onMessage: handleInboundMessage,
|
|
544
653
|
onAgentContext: (ctx) => {
|
|
545
|
-
|
|
654
|
+
agentContextState.set(ctx);
|
|
546
655
|
},
|
|
547
656
|
onConnected: () => {
|
|
548
657
|
console.error('[canon] SSE stream connected');
|
|
@@ -560,8 +669,8 @@ async function startChannel() {
|
|
|
560
669
|
console.error(`[canon] SSE start error: ${msg}`);
|
|
561
670
|
});
|
|
562
671
|
// Start approval system
|
|
563
|
-
if (
|
|
564
|
-
approvalManager = new ApprovalManager(client, agentId,
|
|
672
|
+
if (agentContextState.current?.ownerId) {
|
|
673
|
+
approvalManager = new ApprovalManager(client, agentId, agentContextState.current.ownerId);
|
|
565
674
|
approvalServer = new ApprovalHttpServer(approvalManager, () => approvalConversationId, {
|
|
566
675
|
token: process.env.CANON_CLAUDE_APPROVAL_TOKEN,
|
|
567
676
|
getIsOwnerTurn: () => triggerIsOwner,
|
|
@@ -586,9 +695,9 @@ export async function main() {
|
|
|
586
695
|
// Graceful shutdown — release agent lock and clear session state
|
|
587
696
|
const shutdown = () => {
|
|
588
697
|
// Mark all conversations as inactive
|
|
589
|
-
if (
|
|
698
|
+
if (agentContextState.current) {
|
|
590
699
|
for (const convoId of conversationCache.keys()) {
|
|
591
|
-
rtdb?.clearSessionState(convoId,
|
|
700
|
+
rtdb?.clearSessionState(convoId, agentContextState.current.agentId).catch(() => { });
|
|
592
701
|
}
|
|
593
702
|
}
|
|
594
703
|
approvalManager?.dispose();
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { 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 {
|
|
3
|
+
import type { AgentReplyAuthorityV1 } from '@canonmsg/backend-contracts';
|
|
4
4
|
import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
5
5
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
6
6
|
export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
|
|
@@ -24,8 +24,8 @@ export interface ClaudeInputEnvelope {
|
|
|
24
24
|
turnKey: string;
|
|
25
25
|
isOwnerTurn: boolean;
|
|
26
26
|
requestingUserId: string | null;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/** Opaque server capability; never rendered into the Claude prompt. */
|
|
28
|
+
replyAuthority: AgentReplyAuthorityV1 | null;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* The per-turn modes an inbound message resolves from its participant context,
|
|
@@ -34,6 +34,7 @@ export interface ClaudeInputEnvelope {
|
|
|
34
34
|
export interface ClaudeTurnModes {
|
|
35
35
|
artifactRoutingMode?: ClaudeArtifactRoutingMode;
|
|
36
36
|
turnVerbosity?: TurnVerbosity;
|
|
37
|
+
replyAuthority?: AgentReplyAuthorityV1 | null;
|
|
37
38
|
}
|
|
38
39
|
export interface ClaudeRuntimeControlError {
|
|
39
40
|
value: string;
|
|
@@ -105,7 +106,7 @@ export declare function createClaudeInputEnvelope(input: {
|
|
|
105
106
|
artifactBaseline?: TurnArtifactSnapshot | null;
|
|
106
107
|
isOwnerTurn?: boolean;
|
|
107
108
|
requestingUserId?: string | null;
|
|
108
|
-
|
|
109
|
+
replyAuthority?: AgentReplyAuthorityV1 | null;
|
|
109
110
|
}): ClaudeInputEnvelope;
|
|
110
111
|
export declare function resolveClaudeTurnResponseRouting(turn: Pick<ClaudeInputEnvelope, 'kind' | 'isOwnerTurn' | 'requestingUserId'> | null | undefined, ownerId: string | null | undefined, ownerOnly?: boolean): {
|
|
111
112
|
isOwnerTurn: boolean;
|
|
@@ -819,44 +820,3 @@ export declare function confirmClaudeInterrupt(input: {
|
|
|
819
820
|
active: boolean;
|
|
820
821
|
interrupt: () => Promise<unknown>;
|
|
821
822
|
}): Promise<'confirmed' | 'defer'>;
|
|
822
|
-
/** Requested session-control values from the control plane (raw, pre-validation). */
|
|
823
|
-
export interface ClaudeSessionControlInput {
|
|
824
|
-
model?: string;
|
|
825
|
-
permissionMode?: string;
|
|
826
|
-
effort?: string;
|
|
827
|
-
ultracode?: string;
|
|
828
|
-
}
|
|
829
|
-
/** Mutable mirror of the live session's control state — mutated in place on success. */
|
|
830
|
-
export interface ClaudeSessionControlState {
|
|
831
|
-
model?: string;
|
|
832
|
-
permissionMode?: string;
|
|
833
|
-
effort?: string;
|
|
834
|
-
ultracode?: string;
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Injected validators + side effects so the apply logic stays host-agnostic and
|
|
838
|
-
* unit-testable. The host wires these to the live SDK query and its runtime
|
|
839
|
-
* control-error bookkeeping; tests inject fakes.
|
|
840
|
-
*/
|
|
841
|
-
export interface ClaudeSessionControlDeps {
|
|
842
|
-
state: ClaudeSessionControlState;
|
|
843
|
-
parsePermissionMode: (value: unknown) => PermissionMode | null;
|
|
844
|
-
normalizeEffort: (value: unknown) => string | undefined;
|
|
845
|
-
parseUltracode: (value: unknown) => string | undefined;
|
|
846
|
-
setModel: (model: string) => Promise<void>;
|
|
847
|
-
setPermissionMode: (mode: PermissionMode) => Promise<void>;
|
|
848
|
-
applyEffort: (level: string) => Promise<void>;
|
|
849
|
-
applyUltracode: (enabled: boolean) => Promise<void>;
|
|
850
|
-
setRuntimeControlError: (controlId: string, value: string, error: unknown) => void;
|
|
851
|
-
clearRuntimeControlError: (controlId: string) => void;
|
|
852
|
-
publishSnapshot: () => void;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Apply a session-control request to the live Claude session. Mirrors the host's
|
|
856
|
-
* control semantics (validation, only-if-different checks, error recording,
|
|
857
|
-
* clear-on-success). Whenever a live op is invoked — on success OR caught
|
|
858
|
-
* failure — publishSnapshot() is called exactly once so the app-subscribed
|
|
859
|
-
* /agent-session snapshot converges immediately instead of waiting for the next
|
|
860
|
-
* heartbeat. If nothing changed, publishSnapshot() is not called.
|
|
861
|
-
*/
|
|
862
|
-
export declare function applyClaudeSessionControl(control: ClaudeSessionControlInput, deps: ClaudeSessionControlDeps): Promise<void>;
|