@canonmsg/agent-sdk 1.4.1 → 1.5.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/README.md CHANGED
@@ -130,6 +130,7 @@ Current rules of thumb:
130
130
  - `selectionPolicy: 'required_explicit'` means Canon should require the user to make a choice instead of silently inheriting a default
131
131
  - `workspaceRoots` and `writableRoots` document allowed roots and let Canon group project choices. Canon still stores the selected concrete `workspaceId`; it does not send arbitrary root-relative paths to generic SDK agents.
132
132
  - Publishing a descriptor does not automatically make your SDK agent enforce those controls. If you advertise model, workspace, execution mode, or runtime-native controls, your runtime must actually read and apply the stored config.
133
+ - Message handlers receive `ctx.provenance`, a Canon-computed sender/conversation context for the latest inbound message in the batch. Use it when your runtime wants owner-only tools, group mention policy, or self-context-aware behavior; Canon does not impose a sandbox on SDK agents.
133
134
 
134
135
  ## Delivery Modes
135
136
 
@@ -42,6 +42,9 @@ export declare class CanonAgent {
42
42
  private readonly reachOutInFlight;
43
43
  private agentId;
44
44
  private agentContext;
45
+ private approvalManager;
46
+ private approvalManagerAgentId;
47
+ private approvalManagerOwnerId;
45
48
  private cachedConversationIds;
46
49
  private running;
47
50
  private runtimeHeartbeatTimer;
@@ -53,6 +56,8 @@ export declare class CanonAgent {
53
56
  private readonly conversationMemberIds;
54
57
  private readonly pendingMembershipChanges;
55
58
  constructor(options: CanonAgentOptions);
59
+ private ensureApprovalManager;
60
+ private filterApprovalReplyMessages;
56
61
  on(event: 'message', handler: MessageHandler): void;
57
62
  on(event: 'contactRequest', handler: ContactRequestHandler): void;
58
63
  on(event: 'contactApproved', handler: ContactRequestHandler): void;
@@ -1,4 +1,4 @@
1
- import { CanonClient, buildCanonGroupContext, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, selectActiveSelfContexts, } from '@canonmsg/core';
1
+ import { ApprovalManager, CanonClient, buildCanonGroupContext, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, } from '@canonmsg/core';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
@@ -205,6 +205,9 @@ export class CanonAgent {
205
205
  reachOutInFlight = new Map();
206
206
  agentId = null;
207
207
  agentContext = null;
208
+ approvalManager = null;
209
+ approvalManagerAgentId = null;
210
+ approvalManagerOwnerId = null;
208
211
  cachedConversationIds = [];
209
212
  running = false;
210
213
  runtimeHeartbeatTimer = null;
@@ -257,6 +260,47 @@ export class CanonAgent {
257
260
  }
258
261
  }
259
262
  }
263
+ ensureApprovalManager(agentContext = this.agentContext) {
264
+ const agentId = agentContext?.agentId || this.agentId;
265
+ const ownerId = agentContext?.ownerId;
266
+ if (!agentId || !ownerId)
267
+ return null;
268
+ if (this.approvalManager
269
+ && this.approvalManagerAgentId === agentId
270
+ && this.approvalManagerOwnerId === ownerId) {
271
+ return this.approvalManager;
272
+ }
273
+ if (this.approvalManager?.pendingCount) {
274
+ return this.approvalManager;
275
+ }
276
+ this.approvalManager?.dispose();
277
+ this.approvalManager = new ApprovalManager(this.apiClient, agentId, ownerId);
278
+ this.approvalManagerAgentId = agentId;
279
+ this.approvalManagerOwnerId = ownerId;
280
+ return this.approvalManager;
281
+ }
282
+ filterApprovalReplyMessages(conversationId, messages) {
283
+ const manager = this.ensureApprovalManager();
284
+ if (!manager) {
285
+ return messages.filter((message) => {
286
+ const metadata = message.metadata;
287
+ return !(metadata && typeof metadata === 'object' && !Array.isArray(metadata)
288
+ && metadata.type === 'approval_reply');
289
+ });
290
+ }
291
+ return messages.filter((message) => {
292
+ const metadata = message.metadata;
293
+ const metadataRecord = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
294
+ ? metadata
295
+ : null;
296
+ const consumed = manager.handleMessage(conversationId, {
297
+ senderId: message.senderId,
298
+ ...(typeof message.text === 'string' ? { text: message.text } : {}),
299
+ ...(metadataRecord ? { metadata: metadataRecord } : {}),
300
+ });
301
+ return !consumed && metadataRecord?.type !== 'approval_reply';
302
+ });
303
+ }
260
304
  on(event, handler) {
261
305
  if (event === 'message') {
262
306
  this.handler = handler;
@@ -411,9 +455,9 @@ export class CanonAgent {
411
455
  this.agentId = agentId;
412
456
  console.log(`[canon-sdk] Authenticated as ${agentId}`);
413
457
  // 2. Wire debouncer to handler
414
- this.debouncer.setCallback(async (conversationId, messages) => {
458
+ this.debouncer.setCallback(async (conversationId, messages, provenanceByMessageId) => {
415
459
  this.rememberConversationId(conversationId);
416
- await this.handleMessages(conversationId, messages);
460
+ await this.handleMessages(conversationId, messages, provenanceByMessageId);
417
461
  });
418
462
  // 3. Fetch conversations (used for delivery mode + session state)
419
463
  let conversations = [];
@@ -437,6 +481,7 @@ export class CanonAgent {
437
481
  // 3b. Fetch agent context (identity, owner, access level)
438
482
  try {
439
483
  this.agentContext = await this.apiClient.getAgentMe();
484
+ this.ensureApprovalManager(this.agentContext);
440
485
  }
441
486
  catch {
442
487
  console.warn('[canon-sdk] Failed to fetch agent context — owner/access info unavailable');
@@ -462,6 +507,7 @@ export class CanonAgent {
462
507
  const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, this.apiClient);
463
508
  rtm.setOnAgentContext((ctx) => {
464
509
  this.agentContext = ctx;
510
+ this.ensureApprovalManager(ctx);
465
511
  });
466
512
  rtm.setContactRequestHandlers({
467
513
  onContactRequest: (request) => {
@@ -950,7 +996,12 @@ export class CanonAgent {
950
996
  }
951
997
  return publisher;
952
998
  }
953
- async handleMessages(conversationId, messages) {
999
+ async handleMessages(conversationId, messages, provenanceByMessageId) {
1000
+ const actionableMessages = this.filterApprovalReplyMessages(conversationId, messages);
1001
+ if (actionableMessages.length === 0) {
1002
+ return;
1003
+ }
1004
+ messages = actionableMessages;
954
1005
  if (!this.handler) {
955
1006
  console.warn(`[canon-sdk] No message handler registered — messages for ${conversationId} dropped. Call agent.on('message', handler) before starting.`);
956
1007
  return;
@@ -961,14 +1012,14 @@ export class CanonAgent {
961
1012
  await this.notifyMessageInterrupt(conversationId, abortSignal);
962
1013
  if (this.sessionManager) {
963
1014
  await this.sessionManager.enqueue(conversationId, messages, async (session, newMessages) => {
964
- await this.executeHandler(conversationId, newMessages, session);
1015
+ await this.executeHandler(conversationId, newMessages, session, provenanceByMessageId);
965
1016
  }, { toFront: shouldInterrupt });
966
1017
  }
967
1018
  else {
968
- await this.executeHandler(conversationId, messages);
1019
+ await this.executeHandler(conversationId, messages, undefined, provenanceByMessageId);
969
1020
  }
970
1021
  }
971
- async executeHandler(conversationId, messages, session) {
1022
+ async executeHandler(conversationId, messages, session, provenanceByMessageId) {
972
1023
  if (!this.handler)
973
1024
  return;
974
1025
  const turnId = randomUUID();
@@ -1147,6 +1198,32 @@ export class CanonAgent {
1147
1198
  inboundPolicy: 'approval-required',
1148
1199
  groupJoinPolicy: 'approval-required',
1149
1200
  };
1201
+ const provenance = latestMessage
1202
+ ? resolveRuntimeProvenance({
1203
+ provenance: provenanceByMessageId?.get(latestMessage.id) ?? null,
1204
+ conversationId,
1205
+ conversationType: conversation.type,
1206
+ memberCount: conversation.memberIds.length,
1207
+ senderId: latestMessage.senderId,
1208
+ senderName: latestMessage.senderName ?? latestMessage.senderId,
1209
+ senderType: latestMessage.senderType,
1210
+ isOwner: latestMessage.isOwner,
1211
+ agentId: agent.agentId,
1212
+ mentions: latestMessage.mentions,
1213
+ activeSelfContextId,
1214
+ selfContexts,
1215
+ })
1216
+ : resolveRuntimeProvenance({
1217
+ conversationId,
1218
+ conversationType: conversation.type,
1219
+ memberCount: conversation.memberIds.length,
1220
+ senderId: '',
1221
+ senderType: 'human',
1222
+ isOwner: false,
1223
+ agentId: agent.agentId,
1224
+ activeSelfContextId,
1225
+ selfContexts,
1226
+ });
1150
1227
  if (replyContext?.found && replyContext.attachments?.length) {
1151
1228
  try {
1152
1229
  const materializedReply = await materializeReplyContextMedia(replyContext, {
@@ -1194,6 +1271,52 @@ export class CanonAgent {
1194
1271
  ...(options ?? {}),
1195
1272
  sourceConversationId: conversationId,
1196
1273
  });
1274
+ const requestApproval = async (request) => {
1275
+ throwIfAborted();
1276
+ const manager = this.ensureApprovalManager(agent);
1277
+ if (!manager) {
1278
+ return { decision: 'deny' };
1279
+ }
1280
+ shouldPersistTurnState = true;
1281
+ try {
1282
+ try {
1283
+ await this.apiClient.clearStreaming(conversationId);
1284
+ }
1285
+ catch { }
1286
+ await writeTurn('waiting_input');
1287
+ try {
1288
+ await this.apiClient.setTyping(conversationId, false);
1289
+ }
1290
+ catch { }
1291
+ const result = await manager.requestApproval(conversationId, request.toolName, request.toolInput ?? {}, {
1292
+ riskLevel: request.riskLevel,
1293
+ risk: request.risk,
1294
+ category: request.category,
1295
+ runtimeId: request.runtimeId,
1296
+ turnId: request.turnId ?? turnId,
1297
+ native: request.native,
1298
+ toolSummary: request.toolSummary,
1299
+ details: request.details,
1300
+ ignoreSessionRules: request.ignoreSessionRules,
1301
+ allowSessionRule: request.allowSessionRule,
1302
+ });
1303
+ throwIfAborted();
1304
+ shouldPersistTurnState = false;
1305
+ try {
1306
+ await this.apiClient.setTyping(conversationId, true, 'thinking');
1307
+ }
1308
+ catch { }
1309
+ await setLiveState('thinking', 'Thinking...', 'thinking');
1310
+ return result;
1311
+ }
1312
+ catch (error) {
1313
+ if (abortController.signal.aborted || isAbortLikeError(error)) {
1314
+ throw error;
1315
+ }
1316
+ shouldPersistTurnState = false;
1317
+ return { decision: 'deny' };
1318
+ }
1319
+ };
1197
1320
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
1198
1321
  const replyWithFile = async (filePath, text = '', options) => {
1199
1322
  throwIfAborted();
@@ -1254,6 +1377,8 @@ export class CanonAgent {
1254
1377
  agent,
1255
1378
  activeSelfContextId,
1256
1379
  selfContexts,
1380
+ provenance,
1381
+ requestApproval,
1257
1382
  abortSignal: abortController.signal,
1258
1383
  media: {
1259
1384
  materialize: (message = hydratedMessages[hydratedMessages.length - 1], options) => {
@@ -1,13 +1,14 @@
1
- import type { CanonMessage } from '@canonmsg/core';
1
+ import type { CanonMessage, CanonRuntimeProvenance } from '@canonmsg/core';
2
2
  export declare class Debouncer {
3
3
  private debounceMs;
4
4
  private pending;
5
+ private provenanceByMessageId;
5
6
  private timers;
6
7
  private orderedFlags;
7
8
  private callback;
8
9
  constructor(debounceMs: number);
9
- setCallback(cb: (conversationId: string, messages: CanonMessage[]) => void): void;
10
- add(conversationId: string, message: CanonMessage): void;
10
+ setCallback(cb: (conversationId: string, messages: CanonMessage[], provenanceByMessageId: ReadonlyMap<string, CanonRuntimeProvenance>) => void): void;
11
+ add(conversationId: string, message: CanonMessage, provenance?: CanonRuntimeProvenance | null): void;
11
12
  private flush;
12
13
  destroy(): void;
13
14
  }
package/dist/debouncer.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export class Debouncer {
2
2
  debounceMs;
3
3
  pending = new Map();
4
+ provenanceByMessageId = new Map();
4
5
  timers = new Map();
5
6
  // Track whether each conversation's pending messages are already in sorted
6
7
  // order so we can skip the sort on flush when possible (#10)
@@ -12,8 +13,11 @@ export class Debouncer {
12
13
  setCallback(cb) {
13
14
  this.callback = cb;
14
15
  }
15
- add(conversationId, message) {
16
+ add(conversationId, message, provenance) {
16
17
  const existing = this.pending.get(conversationId) || [];
18
+ if (provenance) {
19
+ this.provenanceByMessageId.set(message.id, provenance);
20
+ }
17
21
  // Deduplicate by message ID
18
22
  if (!existing.some((m) => m.id === message.id)) {
19
23
  if (existing.length === 0) {
@@ -50,7 +54,15 @@ export class Debouncer {
50
54
  if (!isOrdered) {
51
55
  messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
52
56
  }
53
- this.callback(conversationId, messages);
57
+ const provenanceByMessageId = new Map();
58
+ for (const message of messages) {
59
+ const provenance = this.provenanceByMessageId.get(message.id);
60
+ if (provenance) {
61
+ provenanceByMessageId.set(message.id, provenance);
62
+ this.provenanceByMessageId.delete(message.id);
63
+ }
64
+ }
65
+ this.callback(conversationId, messages, provenanceByMessageId);
54
66
  }
55
67
  }
56
68
  destroy() {
@@ -59,6 +71,7 @@ export class Debouncer {
59
71
  }
60
72
  this.timers.clear();
61
73
  this.pending.clear();
74
+ this.provenanceByMessageId.clear();
62
75
  this.orderedFlags.clear();
63
76
  }
64
77
  }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
2
  export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
3
- export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
4
- export type { CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
3
+ export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, parseTextApprovalReply, redactSecrets, } from '@canonmsg/core';
4
+ export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, SessionRule, } from '@canonmsg/core';
5
5
  export { SessionManager } from './session-manager.js';
6
6
  export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
7
7
  export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
8
8
  export type { SessionConfig, Session } from './session-manager.js';
9
9
  export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonReplyContext, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
10
- export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
10
+ export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
- export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
2
+ export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, parseTextApprovalReply, redactSecrets, } from '@canonmsg/core';
3
3
  export { SessionManager } from './session-manager.js';
4
4
  export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
package/dist/realtime.js CHANGED
@@ -52,7 +52,7 @@ export class RealtimeManager {
52
52
  ...(m.contactCard ? { contactCard: m.contactCard } : {}),
53
53
  ...(m.metadata ? { metadata: m.metadata } : {}),
54
54
  };
55
- this.debouncer.add(payload.conversationId, message);
55
+ this.debouncer.add(payload.conversationId, message, payload.provenance ?? null);
56
56
  },
57
57
  onAgentContext: (ctx) => {
58
58
  this.onAgentContext?.(ctx);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
2
- import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
1
+ export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
+ import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
3
3
  import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
4
4
  export interface ProgressMessageOptions extends SendMessageOptions {
5
5
  /**
@@ -34,6 +34,28 @@ export interface TurnController {
34
34
  setTool: (text: string) => Promise<void>;
35
35
  setWaitingInput: (text?: string) => Promise<void>;
36
36
  }
37
+ export interface RuntimeApprovalRequest {
38
+ /** Native runtime/tool/action name that is asking for permission. */
39
+ toolName: string;
40
+ /** Structured native input. Raw values are summarized/redacted before storage. */
41
+ toolInput?: Record<string, unknown>;
42
+ /** Optional pre-redacted summary when a runtime already has user-facing copy. */
43
+ toolSummary?: string;
44
+ category?: ApprovalRequestCategory;
45
+ risk?: ApprovalRisk;
46
+ riskLevel?: 'normal' | 'destructive';
47
+ runtimeId?: string;
48
+ turnId?: string;
49
+ native?: ApprovalNativeRequestMetadata;
50
+ details?: ApprovalRequestDetail[];
51
+ /**
52
+ * Ignore in-memory session rules for this approval request. Useful when the
53
+ * action was triggered by someone other than the agent owner.
54
+ */
55
+ ignoreSessionRules?: boolean;
56
+ /** Whether an approval reply may set an in-memory session rule. */
57
+ allowSessionRule?: boolean;
58
+ }
37
59
  export interface MessageHandlerContext {
38
60
  messages: CanonMessage[];
39
61
  history: CanonMessage[];
@@ -79,6 +101,14 @@ export interface MessageHandlerContext {
79
101
  activeSelfContextId: string | null;
80
102
  /** Canon-provided private context explaining this agent's cross-session actions. */
81
103
  selfContexts?: import('@canonmsg/core').CanonSelfContext[];
104
+ /** Trusted Canon provenance for the latest inbound message in this handler batch. */
105
+ provenance: CanonRuntimeProvenance;
106
+ /**
107
+ * Ask the conversation owner to approve a native runtime action. This only
108
+ * renders Canon's inline approval card; runtimes must explicitly wait for
109
+ * and enforce the returned decision in their own approval hook.
110
+ */
111
+ requestApproval: (request: RuntimeApprovalRequest) => Promise<ApprovalResult>;
82
112
  /** Canon-managed local media access for the current conversation. */
83
113
  media: {
84
114
  materialize: (message?: CanonMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^0.18.1"
31
+ "@canonmsg/core": "^0.19.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"