@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/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 { 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, 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
- tools: [
71
- {
72
- name: 'reply',
73
- description: 'Reply to a Canon conversation',
74
- inputSchema: {
75
- type: 'object',
76
- properties: {
77
- conversation_id: {
78
- type: 'string',
79
- description: 'The conversation ID to reply to',
80
- },
81
- text: {
82
- type: 'string',
83
- description: 'Message text to send',
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
- name: 'send_message',
91
- description: 'Send a message to a Canon conversation (supports text, images, audio, video, files)',
92
- inputSchema: {
93
- type: 'object',
94
- properties: {
95
- conversation_id: {
96
- type: 'string',
97
- description: 'The conversation ID',
98
- },
99
- text: {
100
- type: 'string',
101
- description: 'Message text',
102
- },
103
- content_type: {
104
- type: 'string',
105
- enum: ['text', 'image', 'audio', 'video', 'file'],
106
- description: 'Content type (default: text)',
107
- },
108
- image_url: {
109
- type: 'string',
110
- description: 'Image URL (when content_type is image)',
111
- },
112
- audio_url: {
113
- type: 'string',
114
- description: 'Audio URL (when content_type is audio)',
115
- },
116
- video_url: {
117
- type: 'string',
118
- description: 'Video URL (when content_type is video)',
119
- },
120
- file_url: {
121
- type: 'string',
122
- description: 'File URL (when content_type is file)',
123
- },
124
- media_path: {
125
- type: 'string',
126
- description: 'Local image/audio/video/file path to upload and send',
127
- },
128
- file_name: {
129
- type: 'string',
130
- description: 'Optional display filename for file attachments',
131
- },
132
- mime_type: {
133
- type: 'string',
134
- description: 'Optional MIME type for media_path uploads',
135
- },
136
- duration_ms: {
137
- type: 'number',
138
- description: 'Optional audio duration in milliseconds for media_path uploads',
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
- name: 'list_agents',
146
- description: 'List registered Canon agent profiles and their lock status',
147
- inputSchema: {
148
- type: 'object',
149
- properties: {},
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
- name: 'set_typing',
154
- description: 'Show or hide typing indicator in a Canon conversation',
155
- inputSchema: {
156
- type: 'object',
157
- properties: {
158
- conversation_id: {
159
- type: 'string',
160
- description: 'The conversation ID',
161
- },
162
- typing: {
163
- type: 'boolean',
164
- description: 'Whether to show typing indicator',
165
- },
166
- status: {
167
- type: 'string',
168
- enum: ['typing', 'thinking'],
169
- description: 'Typing status (default: typing). Use "thinking" when processing/reasoning.',
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
- // Canonical verbs — projections of canon.verbs.v1 (@canonmsg/agent-tools).
176
- // Channel mode owns no turn, so `no_reply` is an ack-only no-op here: the
177
- // standalone session has no final delivery for it to suppress.
178
- ...canonVerbToolDefinitions().filter((definition) => !OWNER_BOUND_COMMUNICATION_VERBS.has(definition.name)),
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
- const result = await client.sendMessage(conversationId, text, {
197
- metadata: TURN_COMPLETE_METADATA,
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
- const result = await client.sendMessage(conversationId, text, opts);
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 (OWNER_BOUND_COMMUNICATION_VERBS.has(name)) {
319
- return toolArgumentError(`${name} requires an owner-authored Canon turn and is unavailable in standalone channel mode.`);
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 verbArgs = name === 'send_to' ? stampSendToTurnComplete(rawArgs) : rawArgs;
324
- const verbResult = await executeCanonVerbTool(client, name, verbArgs);
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: agentContext?.agentId ?? 'claude-code',
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
- agentContext = await client.getAgentMe();
480
- agentId = agentContext.agentId;
481
- console.error(`[canon] Connected as ${agentContext.displayName || agentId}`);
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: agentContext?.displayName ?? profileAgentName,
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
- agentContext = ctx;
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 (agentContext?.ownerId) {
564
- approvalManager = new ApprovalManager(client, agentId, agentContext.ownerId);
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 (agentContext) {
698
+ if (agentContextState.current) {
590
699
  for (const convoId of conversationCache.keys()) {
591
- rtdb?.clearSessionState(convoId, agentContext.agentId).catch(() => { });
700
+ rtdb?.clearSessionState(convoId, agentContextState.current.agentId).catch(() => { });
592
701
  }
593
702
  }
594
703
  approvalManager?.dispose();
@@ -1,6 +1,6 @@
1
- import type { PermissionMode, SDKMessageOrigin, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
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 { OwnerBoundCanonContactTarget } from '@canonmsg/agent-tools';
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
- /** Trusted contact-card target resolved from the owner message's reply. */
28
- replyContactTarget?: OwnerBoundCanonContactTarget;
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
- replyContactTarget?: OwnerBoundCanonContactTarget;
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>;