@canonmsg/core 0.20.1 → 0.22.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.
@@ -5,6 +5,7 @@ export declare function buildApprovalRequest(approvalId: string, toolName: strin
5
5
  riskLevel?: 'normal' | 'destructive';
6
6
  risk?: ApprovalRisk;
7
7
  category?: ApprovalRequestCategory;
8
+ responseUserId?: string;
8
9
  runtimeId?: string;
9
10
  turnId?: string;
10
11
  native?: ApprovalNativeRequestMetadata;
@@ -64,6 +64,7 @@ export function buildApprovalRequest(approvalId, toolName, toolInput, opts) {
64
64
  const metadata = {
65
65
  type: 'approval_request',
66
66
  approvalId,
67
+ ...(opts.responseUserId ? { responseUserId: opts.responseUserId.slice(0, 128) } : {}),
67
68
  toolName,
68
69
  toolSummary: summary,
69
70
  ...(opts.category ? { category: opts.category } : {}),
@@ -50,10 +50,11 @@ export class ApprovalManager {
50
50
  }
51
51
  const approvalId = generateApprovalId();
52
52
  const expiresAt = new Date(Date.now() + this.config.timeoutSeconds * 1000).toISOString();
53
- const { text, metadata } = buildApprovalRequest(approvalId, toolName, toolInput, {
53
+ const { metadata } = buildApprovalRequest(approvalId, toolName, toolInput, {
54
54
  riskLevel: opts?.riskLevel,
55
55
  risk: opts?.risk,
56
56
  category: opts?.category,
57
+ responseUserId: this.ownerId,
57
58
  runtimeId: opts?.runtimeId,
58
59
  turnId: opts?.turnId,
59
60
  native: opts?.native,
@@ -62,25 +63,47 @@ export class ApprovalManager {
62
63
  expiresAt,
63
64
  redactPatterns: this.config.redactPatterns,
64
65
  });
65
- // Send the approval request message with metadata
66
- // Spread to satisfy Record<string, unknown> without double cast
67
- await this.client.sendMessage(conversationId, text, {
68
- metadata: { ...metadata },
66
+ await this.client.createRuntimeApprovalRequest({
67
+ conversationId,
68
+ approvalId,
69
+ toolName,
70
+ toolSummary: metadata.toolSummary,
71
+ expiresAt: Date.parse(expiresAt),
72
+ responseUserId: this.ownerId,
73
+ ...(opts?.riskLevel ? { riskLevel: opts.riskLevel } : {}),
74
+ ...(opts?.risk ? { risk: opts.risk } : {}),
75
+ ...(opts?.category ? { category: opts.category } : {}),
76
+ ...(opts?.runtimeId ? { runtimeId: opts.runtimeId } : {}),
77
+ ...(opts?.turnId ? { turnId: opts.turnId } : {}),
78
+ ...(metadata.native ? { native: metadata.native } : {}),
79
+ ...(metadata.details ? { details: metadata.details } : {}),
80
+ ...(opts?.allowSessionRule === false ? { allowSessionRule: false } : {}),
69
81
  });
70
82
  // Wait for reply or timeout
71
83
  return new Promise((resolve) => {
72
- const timer = setTimeout(() => {
84
+ let settled = false;
85
+ let timer;
86
+ const finish = (result, reason) => {
87
+ if (settled)
88
+ return;
89
+ settled = true;
90
+ clearTimeout(timer);
73
91
  this.pending.delete(approvalId);
74
- const msg = buildApprovalOutcome(approvalId, toolName, metadata.toolSummary, 'deny', 'timeout');
75
- this.client.sendMessage(conversationId, msg, {
76
- metadata: {
77
- type: 'approval_outcome',
78
- approvalId,
79
- decision: 'deny',
80
- reason: 'timeout',
81
- },
82
- }).catch(() => { });
83
- resolve({ decision: 'deny' });
92
+ if (reason === 'timeout') {
93
+ const msg = buildApprovalOutcome(approvalId, toolName, metadata.toolSummary, 'deny', 'timeout');
94
+ this.client.sendMessage(conversationId, msg, {
95
+ metadata: {
96
+ type: 'approval_outcome',
97
+ approvalId,
98
+ decision: 'deny',
99
+ reason: 'timeout',
100
+ },
101
+ }).catch(() => { });
102
+ }
103
+ resolve(result);
104
+ };
105
+ timer = setTimeout(() => {
106
+ finish({ decision: 'deny' }, 'timeout');
84
107
  }, this.config.timeoutSeconds * 1000);
85
108
  this.pending.set(approvalId, {
86
109
  approvalId,
@@ -88,9 +111,32 @@ export class ApprovalManager {
88
111
  toolName,
89
112
  toolSummary: metadata.toolSummary,
90
113
  allowSessionRule: opts?.allowSessionRule !== false,
91
- resolve,
114
+ resolve: (result) => finish(result, 'replied'),
92
115
  timer,
93
116
  });
117
+ const poll = async () => {
118
+ while (this.pending.has(approvalId)) {
119
+ try {
120
+ const response = await this.client.consumeRuntimeApprovalResponse({
121
+ conversationId,
122
+ approvalId,
123
+ });
124
+ if (response.status === 'allow' || response.status === 'deny') {
125
+ this.resolveApproval(approvalId, response.status, response.sessionRule, conversationId);
126
+ return;
127
+ }
128
+ if (response.status === 'timeout') {
129
+ finish({ decision: 'deny' }, 'timeout');
130
+ return;
131
+ }
132
+ }
133
+ catch {
134
+ // Keep polling through transient API errors until the local timeout.
135
+ }
136
+ await new Promise((resume) => setTimeout(resume, 1000));
137
+ }
138
+ };
139
+ void poll();
94
140
  });
95
141
  }
96
142
  /**
@@ -1,6 +1,8 @@
1
1
  export interface ApprovalRequestMetadata {
2
2
  type: 'approval_request';
3
3
  approvalId: string;
4
+ /** Canon user who should answer this card. UI hint only; reply APIs still enforce authorization. */
5
+ responseUserId?: string;
4
6
  toolName: string;
5
7
  /** Pre-computed, redacted summary — raw toolInput is never stored */
6
8
  toolSummary: string;
@@ -172,6 +172,7 @@ export function parseApprovalRequestMetadata(value) {
172
172
  const expiresMs = Date.parse(expiresAt);
173
173
  if (!Number.isFinite(expiresMs))
174
174
  return null;
175
+ const responseUserId = normalizeString(value.responseUserId, 128) ?? undefined;
175
176
  const riskLevel = value.riskLevel === 'destructive' || value.riskLevel === 'normal'
176
177
  ? value.riskLevel
177
178
  : undefined;
@@ -184,6 +185,7 @@ export function parseApprovalRequestMetadata(value) {
184
185
  return {
185
186
  type: 'approval_request',
186
187
  approvalId,
188
+ ...(responseUserId ? { responseUserId } : {}),
187
189
  toolName,
188
190
  toolSummary,
189
191
  ...(category ? { category } : {}),
package/dist/client.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type CanonMessage, type CanonConversation, type CanonContact, type CanonContactRequest, type CanonMessagesPage, type CanonResolveAdmissionResult, type AgentContext, type AddMemberResult, type CreateContactRequestResult, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
2
2
  import type { RuntimeInputKind } from './runtime-cards.js';
3
+ import type { ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, SessionRule } from './approval-types.js';
4
+ import type { RuntimeInputChoice, RuntimeInputNativeMetadata } from './runtime-cards.js';
3
5
  import type { SendContextualMessageOptions, SendContextualMessageResult } from './self-context.js';
4
6
  import type { InboundDisposition } from './turn-protocol.js';
5
7
  /**
@@ -59,10 +61,19 @@ export declare class CanonClient {
59
61
  inputId: string;
60
62
  kind: RuntimeInputKind;
61
63
  expiresAt: number;
64
+ title?: string;
65
+ prompt?: string;
66
+ choices?: RuntimeInputChoice[];
67
+ secretName?: string;
68
+ native?: RuntimeInputNativeMetadata;
69
+ sensitive?: boolean;
70
+ responseUserId?: string;
71
+ turnId?: string;
62
72
  }): Promise<{
63
73
  success: true;
64
74
  inputId: string;
65
75
  expiresAt: number;
76
+ messageId?: string;
66
77
  }>;
67
78
  consumeRuntimeInputResponse(options: {
68
79
  conversationId: string;
@@ -86,6 +97,52 @@ export declare class CanonClient {
86
97
  inputId: string;
87
98
  kind: RuntimeInputKind;
88
99
  }>;
100
+ createRuntimeApprovalRequest(options: {
101
+ conversationId: string;
102
+ approvalId?: string;
103
+ toolName: string;
104
+ toolSummary: string;
105
+ expiresAt: number;
106
+ responseUserId?: string;
107
+ riskLevel?: 'normal' | 'destructive';
108
+ risk?: ApprovalRisk;
109
+ category?: ApprovalRequestCategory;
110
+ runtimeId?: string;
111
+ turnId?: string;
112
+ native?: ApprovalNativeRequestMetadata;
113
+ details?: ApprovalRequestDetail[];
114
+ allowSessionRule?: boolean;
115
+ }): Promise<{
116
+ success: true;
117
+ approvalId: string;
118
+ expiresAt: number;
119
+ messageId?: string;
120
+ }>;
121
+ consumeRuntimeApprovalResponse(options: {
122
+ conversationId: string;
123
+ approvalId: string;
124
+ cancel?: boolean;
125
+ }): Promise<{
126
+ status: 'pending';
127
+ approvalId: string;
128
+ expiresAt?: number;
129
+ } | {
130
+ status: 'allow';
131
+ approvalId: string;
132
+ sessionRule?: SessionRule;
133
+ } | {
134
+ status: 'deny';
135
+ approvalId: string;
136
+ sessionRule?: SessionRule;
137
+ } | {
138
+ status: 'timeout';
139
+ approvalId: string;
140
+ }>;
141
+ updateRuntimeStatus(options: {
142
+ runtime: string;
143
+ hostMode?: boolean;
144
+ runtimeDescriptor?: Record<string, unknown>;
145
+ }): Promise<void>;
89
146
  static register(baseUrl: string | undefined, body: {
90
147
  name: string;
91
148
  description: string;
package/dist/client.js CHANGED
@@ -339,6 +339,35 @@ export class CanonClient {
339
339
  throw new CanonApiError(res.status, await res.text());
340
340
  return res.json();
341
341
  }
342
+ async createRuntimeApprovalRequest(options) {
343
+ const res = await fetch(`${this.baseUrl}/runtime-approval/request`, {
344
+ method: 'POST',
345
+ headers: this.authHeaders(),
346
+ body: JSON.stringify(options),
347
+ });
348
+ if (!res.ok)
349
+ throw new CanonApiError(res.status, await res.text());
350
+ return res.json();
351
+ }
352
+ async consumeRuntimeApprovalResponse(options) {
353
+ const res = await fetch(`${this.baseUrl}/runtime-approval/consume`, {
354
+ method: 'POST',
355
+ headers: this.authHeaders(),
356
+ body: JSON.stringify(options),
357
+ });
358
+ if (!res.ok)
359
+ throw new CanonApiError(res.status, await res.text());
360
+ return res.json();
361
+ }
362
+ async updateRuntimeStatus(options) {
363
+ const res = await fetch(`${this.baseUrl}/runtime/status`, {
364
+ method: 'POST',
365
+ headers: this.authHeaders(),
366
+ body: JSON.stringify(options),
367
+ });
368
+ if (!res.ok)
369
+ throw new CanonApiError(res.status, await res.text());
370
+ }
342
371
  // ── Static unauthenticated registration endpoints ────────────────────
343
372
  static async register(baseUrl, body) {
344
373
  const url = baseUrl || DEFAULT_BASE_URL;
@@ -11,6 +11,8 @@ export interface RuntimeQuestionDefinition {
11
11
  export interface ClaudeQuestionMetadata {
12
12
  type: 'claude_question';
13
13
  questionId: string;
14
+ /** Canon user who should answer this card. UI hint only; reply APIs still enforce authorization. */
15
+ responseUserId?: string;
14
16
  questions: RuntimeQuestionDefinition[];
15
17
  }
16
18
  export interface ClaudeQuestionReplyMetadata {
@@ -21,6 +23,8 @@ export interface ClaudeQuestionReplyMetadata {
21
23
  export interface PlanApprovalMetadata {
22
24
  type: 'plan_approval';
23
25
  planId: string;
26
+ /** Canon user who should answer this card. UI hint only; reply APIs still enforce authorization. */
27
+ responseUserId?: string;
24
28
  title?: string;
25
29
  summary?: string;
26
30
  body?: string;
@@ -54,6 +58,8 @@ export interface RuntimeInputNativeMetadata {
54
58
  export interface RuntimeInputRequestMetadata {
55
59
  type: 'runtime_input_request';
56
60
  inputId: string;
61
+ /** Canon user who should answer this card. UI hint only; reply APIs still enforce authorization. */
62
+ responseUserId?: string;
57
63
  kind: RuntimeInputKind;
58
64
  prompt: string;
59
65
  title?: string;
@@ -78,7 +84,9 @@ export interface RuntimeInputOutcomeMetadata {
78
84
  status: RuntimeInputResolutionStatus;
79
85
  reason?: 'submitted' | 'cancelled' | 'timeout' | 'expired' | 'interrupted';
80
86
  }
81
- export declare function buildQuestionRequest(questionId: string, questions: RuntimeQuestionDefinition[]): {
87
+ export declare function buildQuestionRequest(questionId: string, questions: RuntimeQuestionDefinition[], options?: {
88
+ responseUserId?: string;
89
+ }): {
82
90
  text: string;
83
91
  metadata: ClaudeQuestionMetadata;
84
92
  };
@@ -68,7 +68,7 @@ function assertNoSensitiveRuntimeInputPayload(value) {
68
68
  && !('secret' in value)
69
69
  && !('rawValue' in value);
70
70
  }
71
- export function buildQuestionRequest(questionId, questions) {
71
+ export function buildQuestionRequest(questionId, questions, options) {
72
72
  const text = questions.length === 1
73
73
  ? questions[0]?.question || 'Question'
74
74
  : `Please answer ${questions.length} questions.`;
@@ -77,6 +77,7 @@ export function buildQuestionRequest(questionId, questions) {
77
77
  metadata: {
78
78
  type: 'claude_question',
79
79
  questionId,
80
+ ...(options?.responseUserId ? { responseUserId: options.responseUserId.slice(0, 128) } : {}),
80
81
  questions,
81
82
  },
82
83
  };
@@ -124,6 +125,7 @@ export function buildRuntimeInputRequest(inputId, input) {
124
125
  metadata: {
125
126
  type: 'runtime_input_request',
126
127
  inputId,
128
+ ...(input.responseUserId ? { responseUserId: input.responseUserId.slice(0, 128) } : {}),
127
129
  kind: input.kind,
128
130
  prompt: input.prompt.slice(0, 1000),
129
131
  title: title.slice(0, 120),
@@ -171,6 +173,7 @@ export function parseRuntimeInputRequestMetadata(value) {
171
173
  if (!assertNoSensitiveRuntimeInputPayload(value))
172
174
  return null;
173
175
  const inputId = normalizeString(value.inputId, 128);
176
+ const responseUserId = normalizeString(value.responseUserId, 128) ?? undefined;
174
177
  const kind = normalizeRuntimeInputKind(value.kind);
175
178
  const prompt = normalizeString(value.prompt, 1000);
176
179
  const expiresAt = normalizeString(value.expiresAt, 128);
@@ -186,6 +189,7 @@ export function parseRuntimeInputRequestMetadata(value) {
186
189
  return {
187
190
  type: 'runtime_input_request',
188
191
  inputId,
192
+ ...(responseUserId ? { responseUserId } : {}),
189
193
  kind,
190
194
  prompt,
191
195
  ...(title ? { title } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.20.1",
3
+ "version": "0.22.0",
4
4
  "description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",