@canonmsg/core 0.18.0 → 0.19.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.
@@ -1,8 +1,15 @@
1
- import type { ApprovalRequestMetadata, ApprovalReplyMetadata, SessionRule } from './approval-types.js';
1
+ import type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalNativeRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, SessionRule } from './approval-types.js';
2
2
  export declare function generateApprovalId(): string;
3
3
  export declare function redactSecrets(text: string, patterns: string[]): string;
4
4
  export declare function buildApprovalRequest(approvalId: string, toolName: string, toolInput: Record<string, unknown>, opts: {
5
5
  riskLevel?: 'normal' | 'destructive';
6
+ risk?: ApprovalRisk;
7
+ category?: ApprovalRequestCategory;
8
+ runtimeId?: string;
9
+ turnId?: string;
10
+ native?: ApprovalNativeRequestMetadata;
11
+ toolSummary?: string;
12
+ details?: ApprovalRequestDetail[];
6
13
  expiresAt: string;
7
14
  redactPatterns?: string[];
8
15
  }): {
@@ -27,22 +27,37 @@ export function redactSecrets(text, patterns) {
27
27
  }
28
28
  return result;
29
29
  }
30
+ function redactApprovalDetails(details, patterns) {
31
+ if (!details?.length)
32
+ return undefined;
33
+ return details.slice(0, 8).map((detail) => ({
34
+ ...detail,
35
+ label: detail.label.slice(0, 80),
36
+ value: patterns?.length
37
+ ? redactSecrets(detail.value.slice(0, 500), patterns)
38
+ : detail.value.slice(0, 500),
39
+ }));
40
+ }
30
41
  // ── Build approval request message ──────────────────────────────────
31
42
  export function buildApprovalRequest(approvalId, toolName, toolInput, opts) {
32
- let summary = summarizeToolInput(toolName, toolInput);
43
+ let summary = opts.toolSummary ?? summarizeToolInput(toolName, toolInput);
33
44
  if (opts.redactPatterns?.length) {
34
45
  summary = redactSecrets(summary, opts.redactPatterns);
35
46
  }
47
+ summary = summary.slice(0, 1000);
48
+ const risk = opts.risk ?? opts.riskLevel ?? 'normal';
49
+ const riskLevel = risk === 'destructive' ? 'destructive' : (opts.riskLevel ?? 'normal');
50
+ const details = redactApprovalDetails(opts.details, opts.redactPatterns);
36
51
  const timeoutMin = Math.round((new Date(opts.expiresAt).getTime() - Date.now()) / 60_000);
37
52
  const lines = [
38
- `Tool Approval Required [${approvalId}]`,
53
+ `Action Approval Required [${approvalId}]`,
39
54
  '',
40
55
  `Tool: ${toolName}`,
41
56
  summary,
42
57
  '',
43
58
  ];
44
- if (opts.riskLevel === 'destructive') {
45
- lines.push('This is a destructive operation');
59
+ if (risk === 'high' || risk === 'destructive') {
60
+ lines.push(risk === 'destructive' ? 'This is a destructive operation' : 'This is a high-risk operation');
46
61
  lines.push('');
47
62
  }
48
63
  lines.push(`Reply "approve" or "deny" (expires in ${timeoutMin}m)`);
@@ -51,7 +66,13 @@ export function buildApprovalRequest(approvalId, toolName, toolInput, opts) {
51
66
  approvalId,
52
67
  toolName,
53
68
  toolSummary: summary,
54
- riskLevel: opts.riskLevel ?? 'normal',
69
+ ...(opts.category ? { category: opts.category } : {}),
70
+ risk,
71
+ riskLevel,
72
+ ...(opts.runtimeId ? { runtimeId: opts.runtimeId } : {}),
73
+ ...(opts.turnId ? { turnId: opts.turnId } : {}),
74
+ ...(opts.native ? { native: opts.native } : {}),
75
+ ...(details ? { details } : {}),
55
76
  expiresAt: opts.expiresAt,
56
77
  };
57
78
  return { text: lines.join('\n'), metadata };
@@ -77,7 +98,8 @@ export function buildApprovalOutcome(approvalId, toolName, toolSummary, decision
77
98
  return `Expired [${approvalId}] -- ${toolName}: ${short} (auto-denied)`;
78
99
  }
79
100
  if (reason === 'session-rule') {
80
- return `Auto-approved (session rule) -- ${toolName}: ${short}`;
101
+ const label = decision === 'allow' ? 'Auto-approved' : 'Auto-denied';
102
+ return `${label} (session rule) -- ${toolName}: ${short}`;
81
103
  }
82
104
  return `${icon} [${approvalId}] -- ${toolName}: ${short}`;
83
105
  }
@@ -1,5 +1,5 @@
1
1
  import type { CanonClient } from './client.js';
2
- import type { ApprovalConfig, ApprovalResult, SessionRule } from './approval-types.js';
2
+ import type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalResult, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, SessionRule } from './approval-types.js';
3
3
  /**
4
4
  * Platform-agnostic approval protocol for Canon.
5
5
  *
@@ -27,6 +27,15 @@ export declare class ApprovalManager {
27
27
  */
28
28
  requestApproval(conversationId: string, toolName: string, toolInput: Record<string, unknown>, opts?: {
29
29
  riskLevel?: 'normal' | 'destructive';
30
+ risk?: ApprovalRisk;
31
+ category?: ApprovalRequestCategory;
32
+ runtimeId?: string;
33
+ turnId?: string;
34
+ native?: ApprovalNativeRequestMetadata;
35
+ toolSummary?: string;
36
+ details?: ApprovalRequestDetail[];
37
+ ignoreSessionRules?: boolean;
38
+ allowSessionRule?: boolean;
30
39
  }): Promise<ApprovalResult>;
31
40
  /**
32
41
  * Feed an inbound message to the approval manager.
@@ -38,7 +47,7 @@ export declare class ApprovalManager {
38
47
  metadata?: Record<string, unknown>;
39
48
  }): boolean;
40
49
  /** Check if a tool would be auto-resolved by a session rule */
41
- checkSessionRules(toolName: string): SessionRule | null;
50
+ checkSessionRules(toolName: string, conversationId?: string): SessionRule | null;
42
51
  getSessionRules(): SessionRule[];
43
52
  clearSessionRules(): void;
44
53
  get pendingCount(): number;
@@ -1,5 +1,5 @@
1
- import { DEFAULT_APPROVAL_CONFIG } from './approval-types.js';
2
- import { generateApprovalId, buildApprovalRequest, buildApprovalOutcome, parseTextApprovalReply, } from './approval-format.js';
1
+ import { DEFAULT_APPROVAL_CONFIG, parseApprovalReplyMetadata } from './approval-types.js';
2
+ import { generateApprovalId, buildApprovalRequest, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
3
3
  // ── ApprovalManager ─────────────────────────────────────────────────
4
4
  /**
5
5
  * Platform-agnostic approval protocol for Canon.
@@ -34,11 +34,14 @@ export class ApprovalManager {
34
34
  */
35
35
  async requestApproval(conversationId, toolName, toolInput, opts) {
36
36
  // Check session rules first
37
- const matchingRule = this.checkSessionRules(toolName);
37
+ const matchingRule = opts?.ignoreSessionRules ? null : this.checkSessionRules(toolName, conversationId);
38
38
  if (matchingRule) {
39
39
  const decision = matchingRule.type === 'deny-tool' ? 'deny' : 'allow';
40
40
  // Send silent log (fire-and-forget)
41
- const summary = this.summarizeTool(toolName, toolInput);
41
+ const rawSummary = opts?.toolSummary ?? this.summarizeTool(toolName, toolInput);
42
+ const summary = this.config.redactPatterns.length
43
+ ? redactSecrets(rawSummary, this.config.redactPatterns)
44
+ : rawSummary;
42
45
  const logMsg = buildApprovalOutcome('', toolName, summary, decision, 'session-rule');
43
46
  this.client.sendMessage(conversationId, logMsg, {
44
47
  metadata: { type: 'approval_outcome', decision, reason: 'session-rule' },
@@ -49,6 +52,13 @@ export class ApprovalManager {
49
52
  const expiresAt = new Date(Date.now() + this.config.timeoutSeconds * 1000).toISOString();
50
53
  const { text, metadata } = buildApprovalRequest(approvalId, toolName, toolInput, {
51
54
  riskLevel: opts?.riskLevel,
55
+ risk: opts?.risk,
56
+ category: opts?.category,
57
+ runtimeId: opts?.runtimeId,
58
+ turnId: opts?.turnId,
59
+ native: opts?.native,
60
+ toolSummary: opts?.toolSummary,
61
+ details: opts?.details,
52
62
  expiresAt,
53
63
  redactPatterns: this.config.redactPatterns,
54
64
  });
@@ -61,8 +71,7 @@ export class ApprovalManager {
61
71
  return new Promise((resolve) => {
62
72
  const timer = setTimeout(() => {
63
73
  this.pending.delete(approvalId);
64
- const summary = this.summarizeTool(toolName, toolInput);
65
- const msg = buildApprovalOutcome(approvalId, toolName, summary, 'deny', 'timeout');
74
+ const msg = buildApprovalOutcome(approvalId, toolName, metadata.toolSummary, 'deny', 'timeout');
66
75
  this.client.sendMessage(conversationId, msg, {
67
76
  metadata: {
68
77
  type: 'approval_outcome',
@@ -77,7 +86,8 @@ export class ApprovalManager {
77
86
  approvalId,
78
87
  conversationId,
79
88
  toolName,
80
- toolSummary: this.summarizeTool(toolName, toolInput),
89
+ toolSummary: metadata.toolSummary,
90
+ allowSessionRule: opts?.allowSessionRule !== false,
81
91
  resolve,
82
92
  timer,
83
93
  });
@@ -95,11 +105,10 @@ export class ApprovalManager {
95
105
  return false;
96
106
  // Try structured metadata first (from card UI)
97
107
  if (message.metadata?.type === 'approval_reply') {
98
- const m = message.metadata;
99
- const approvalId = m.approvalId;
100
- const decision = m.decision;
101
- const sessionRule = m.sessionRule;
102
- return this.resolveApproval(approvalId, decision, sessionRule, conversationId);
108
+ const parsed = parseApprovalReplyMetadata(message.metadata);
109
+ if (!parsed)
110
+ return false;
111
+ return this.resolveApproval(parsed.approvalId, parsed.decision, parsed.sessionRule, conversationId);
103
112
  }
104
113
  // Fall back to text parsing
105
114
  if (message.text) {
@@ -119,23 +128,31 @@ export class ApprovalManager {
119
128
  return false;
120
129
  }
121
130
  /** Check if a tool would be auto-resolved by a session rule */
122
- checkSessionRules(toolName) {
131
+ checkSessionRules(toolName, conversationId) {
123
132
  this.pruneExpiredRules();
124
- for (const rule of this.rules) {
133
+ for (const stored of this.rules) {
134
+ if (conversationId && stored.conversationId !== conversationId)
135
+ continue;
136
+ const { rule } = stored;
125
137
  if (rule.type === 'approve-all')
126
138
  return rule;
127
139
  if ((rule.type === 'approve-tool' || rule.type === 'deny-tool') &&
128
140
  rule.toolPattern) {
129
- const re = new RegExp(`^${rule.toolPattern}$`, 'i');
130
- if (re.test(toolName))
131
- return rule;
141
+ try {
142
+ const re = new RegExp(`^${rule.toolPattern}$`, 'i');
143
+ if (re.test(toolName))
144
+ return rule;
145
+ }
146
+ catch {
147
+ continue;
148
+ }
132
149
  }
133
150
  }
134
151
  return null;
135
152
  }
136
153
  getSessionRules() {
137
154
  this.pruneExpiredRules();
138
- return [...this.rules];
155
+ return this.rules.map((stored) => stored.rule);
139
156
  }
140
157
  clearSessionRules() {
141
158
  this.rules = [];
@@ -160,11 +177,18 @@ export class ApprovalManager {
160
177
  }
161
178
  return false;
162
179
  }
180
+ if (entry.conversationId !== conversationId) {
181
+ return false;
182
+ }
163
183
  clearTimeout(entry.timer);
164
184
  this.pending.delete(approvalId);
165
- // Store session rule if provided
166
- if (sessionRule) {
167
- this.rules.push(sessionRule);
185
+ const acceptedSessionRule = entry.allowSessionRule ? sessionRule : undefined;
186
+ // Store session rule if provided and allowed for this request
187
+ if (acceptedSessionRule) {
188
+ this.rules.push({
189
+ conversationId: entry.conversationId,
190
+ rule: acceptedSessionRule,
191
+ });
168
192
  }
169
193
  // Send confirmation (fire-and-forget)
170
194
  const msg = buildApprovalOutcome(approvalId, entry.toolName, entry.toolSummary, decision, 'replied');
@@ -177,15 +201,15 @@ export class ApprovalManager {
177
201
  },
178
202
  }).catch(() => { });
179
203
  // If session rule was set, log that too
180
- if (sessionRule) {
181
- const ruleDesc = this.describeRule(sessionRule);
204
+ if (acceptedSessionRule) {
205
+ const ruleDesc = this.describeRule(acceptedSessionRule);
182
206
  this.client
183
207
  .sendMessage(conversationId, `Session rule set: ${ruleDesc}`, {
184
208
  metadata: { type: 'approval_outcome', decision, reason: 'session-rule' },
185
209
  })
186
210
  .catch(() => { });
187
211
  }
188
- entry.resolve({ decision, sessionRule });
212
+ entry.resolve({ decision, ...(acceptedSessionRule ? { sessionRule: acceptedSessionRule } : {}) });
189
213
  return true;
190
214
  }
191
215
  findMostRecentPending(conversationId) {
@@ -199,10 +223,10 @@ export class ApprovalManager {
199
223
  }
200
224
  pruneExpiredRules() {
201
225
  const now = Date.now();
202
- this.rules = this.rules.filter((r) => {
203
- if (!r.expiresAt)
226
+ this.rules = this.rules.filter(({ rule }) => {
227
+ if (!rule.expiresAt)
204
228
  return true;
205
- return new Date(r.expiresAt).getTime() > now;
229
+ return new Date(rule.expiresAt).getTime() > now;
206
230
  });
207
231
  }
208
232
  summarizeTool(toolName, toolInput) {
@@ -4,7 +4,17 @@ export interface ApprovalRequestMetadata {
4
4
  toolName: string;
5
5
  /** Pre-computed, redacted summary — raw toolInput is never stored */
6
6
  toolSummary: string;
7
+ /** Broad action family for richer Canon approval cards. */
8
+ category?: ApprovalRequestCategory;
9
+ /** Preferred v2 risk signal. `riskLevel` remains for older clients. */
10
+ risk?: ApprovalRisk;
7
11
  riskLevel?: 'normal' | 'destructive';
12
+ runtimeId?: string;
13
+ turnId?: string;
14
+ /** Native runtime correlation handle. Canon stores this opaquely for plugins/SDK bridges. */
15
+ native?: ApprovalNativeRequestMetadata;
16
+ /** Redacted, normalized details. Raw tool input must not be stored here. */
17
+ details?: ApprovalRequestDetail[];
8
18
  expiresAt: string;
9
19
  }
10
20
  export interface ApprovalOutcomeMetadata {
@@ -19,6 +29,33 @@ export interface ApprovalReplyMetadata {
19
29
  decision: 'allow' | 'deny';
20
30
  sessionRule?: SessionRule;
21
31
  }
32
+ export type ApprovalRequestCategory = 'command' | 'file' | 'network' | 'browser' | 'mcp' | 'plugin' | 'canon' | 'tool';
33
+ export type ApprovalRisk = 'low' | 'normal' | 'high' | 'destructive';
34
+ export interface ApprovalRequestDetail {
35
+ label: string;
36
+ value: string;
37
+ monospace?: boolean;
38
+ }
39
+ export interface ApprovalNativeRequestMetadata {
40
+ runtime?: string;
41
+ method?: string;
42
+ requestId?: string;
43
+ provider?: string;
44
+ origin?: string;
45
+ surface?: string;
46
+ threadId?: string;
47
+ turnId?: string;
48
+ runId?: string;
49
+ itemId?: string;
50
+ toolCallId?: string;
51
+ approvalId?: string;
52
+ pluginId?: string;
53
+ sessionKey?: string;
54
+ cwd?: string;
55
+ model?: string;
56
+ nodeId?: string;
57
+ handles?: Record<string, string>;
58
+ }
22
59
  export interface SessionRule {
23
60
  type: 'approve-all' | 'approve-tool' | 'deny-tool';
24
61
  /** Tool name pattern (regex) — only for approve-tool/deny-tool */
@@ -37,3 +74,6 @@ export interface ApprovalConfig {
37
74
  redactPatterns: string[];
38
75
  }
39
76
  export declare const DEFAULT_APPROVAL_CONFIG: ApprovalConfig;
77
+ export declare function parseSessionRule(value: unknown): SessionRule | null;
78
+ export declare function parseApprovalReplyMetadata(value: unknown): ApprovalReplyMetadata | null;
79
+ export declare function parseApprovalRequestMetadata(value: unknown): ApprovalRequestMetadata | null;
@@ -7,3 +7,192 @@ export const DEFAULT_APPROVAL_CONFIG = {
7
7
  '(?:api[_-]?key|token|secret|password)\\s*[:=]\\s*\\S+',
8
8
  ],
9
9
  };
10
+ function isRecord(value) {
11
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
12
+ }
13
+ function normalizeString(value, maxLength) {
14
+ if (typeof value !== 'string')
15
+ return null;
16
+ const trimmed = value.trim();
17
+ if (!trimmed || trimmed.length > maxLength)
18
+ return null;
19
+ return trimmed;
20
+ }
21
+ function normalizeCategory(value) {
22
+ return value === 'command'
23
+ || value === 'file'
24
+ || value === 'network'
25
+ || value === 'browser'
26
+ || value === 'mcp'
27
+ || value === 'plugin'
28
+ || value === 'canon'
29
+ || value === 'tool'
30
+ ? value
31
+ : undefined;
32
+ }
33
+ function normalizeRisk(value) {
34
+ return value === 'low'
35
+ || value === 'normal'
36
+ || value === 'high'
37
+ || value === 'destructive'
38
+ ? value
39
+ : undefined;
40
+ }
41
+ function normalizeDetails(value) {
42
+ if (!Array.isArray(value))
43
+ return undefined;
44
+ const details = value.slice(0, 8).flatMap((entry) => {
45
+ if (!isRecord(entry))
46
+ return [];
47
+ const label = normalizeString(entry.label, 80);
48
+ const detailValue = normalizeString(entry.value, 500);
49
+ if (!label || !detailValue)
50
+ return [];
51
+ return [{
52
+ label,
53
+ value: detailValue,
54
+ ...(entry.monospace === true ? { monospace: true } : {}),
55
+ }];
56
+ });
57
+ return details.length > 0 ? details : undefined;
58
+ }
59
+ function normalizeNativeRequest(value) {
60
+ if (!isRecord(value))
61
+ return undefined;
62
+ const native = {};
63
+ for (const key of [
64
+ 'runtime',
65
+ 'method',
66
+ 'requestId',
67
+ 'provider',
68
+ 'origin',
69
+ 'surface',
70
+ 'threadId',
71
+ 'turnId',
72
+ 'runId',
73
+ 'itemId',
74
+ 'toolCallId',
75
+ 'approvalId',
76
+ 'pluginId',
77
+ 'sessionKey',
78
+ 'cwd',
79
+ 'model',
80
+ 'nodeId',
81
+ ]) {
82
+ const normalized = normalizeString(value[key], 256);
83
+ if (normalized)
84
+ native[key] = normalized;
85
+ }
86
+ const handles = isRecord(value.handles) ? value.handles : null;
87
+ if (handles) {
88
+ const normalizedHandles = {};
89
+ for (const [key, raw] of Object.entries(handles).slice(0, 16)) {
90
+ const normalizedKey = /^[a-zA-Z0-9_.:-]{1,80}$/.test(key) ? key : null;
91
+ const normalizedValue = normalizeString(raw, 256);
92
+ if (normalizedKey && normalizedValue) {
93
+ normalizedHandles[normalizedKey] = normalizedValue;
94
+ }
95
+ }
96
+ if (Object.keys(normalizedHandles).length > 0) {
97
+ native.handles = normalizedHandles;
98
+ }
99
+ }
100
+ return Object.keys(native).length > 0 ? native : undefined;
101
+ }
102
+ export function parseSessionRule(value) {
103
+ if (!isRecord(value))
104
+ return null;
105
+ const type = value.type;
106
+ if (type !== 'approve-all' && type !== 'approve-tool' && type !== 'deny-tool') {
107
+ return null;
108
+ }
109
+ let expiresAt;
110
+ if (value.expiresAt !== undefined) {
111
+ if (value.expiresAt === null) {
112
+ expiresAt = null;
113
+ }
114
+ else {
115
+ const normalizedExpiresAt = normalizeString(value.expiresAt, 128);
116
+ if (!normalizedExpiresAt)
117
+ return null;
118
+ const expiresMs = Date.parse(normalizedExpiresAt);
119
+ if (!Number.isFinite(expiresMs) || expiresMs <= Date.now())
120
+ return null;
121
+ expiresAt = new Date(expiresMs).toISOString();
122
+ }
123
+ }
124
+ if (type === 'approve-all') {
125
+ return {
126
+ type,
127
+ ...(expiresAt !== undefined ? { expiresAt } : {}),
128
+ };
129
+ }
130
+ const toolPattern = normalizeString(value.toolPattern, 128);
131
+ if (!toolPattern || !/^[\w.*:-]+$/.test(toolPattern)) {
132
+ return null;
133
+ }
134
+ return {
135
+ type,
136
+ toolPattern,
137
+ ...(expiresAt !== undefined ? { expiresAt } : {}),
138
+ };
139
+ }
140
+ export function parseApprovalReplyMetadata(value) {
141
+ if (!isRecord(value) || value.type !== 'approval_reply')
142
+ return null;
143
+ const approvalId = normalizeString(value.approvalId, 128);
144
+ if (!approvalId)
145
+ return null;
146
+ const decision = value.decision;
147
+ if (decision !== 'allow' && decision !== 'deny')
148
+ return null;
149
+ let sessionRule;
150
+ if (value.sessionRule !== undefined) {
151
+ const parsed = parseSessionRule(value.sessionRule);
152
+ if (!parsed)
153
+ return null;
154
+ sessionRule = parsed;
155
+ }
156
+ return {
157
+ type: 'approval_reply',
158
+ approvalId,
159
+ decision,
160
+ ...(sessionRule ? { sessionRule } : {}),
161
+ };
162
+ }
163
+ export function parseApprovalRequestMetadata(value) {
164
+ if (!isRecord(value) || value.type !== 'approval_request')
165
+ return null;
166
+ const approvalId = normalizeString(value.approvalId, 128);
167
+ const toolName = normalizeString(value.toolName, 128);
168
+ const toolSummary = normalizeString(value.toolSummary, 1000);
169
+ const expiresAt = normalizeString(value.expiresAt, 128);
170
+ if (!approvalId || !toolName || !toolSummary || !expiresAt)
171
+ return null;
172
+ const expiresMs = Date.parse(expiresAt);
173
+ if (!Number.isFinite(expiresMs))
174
+ return null;
175
+ const riskLevel = value.riskLevel === 'destructive' || value.riskLevel === 'normal'
176
+ ? value.riskLevel
177
+ : undefined;
178
+ const risk = normalizeRisk(value.risk) ?? (riskLevel === 'destructive' ? 'destructive' : undefined);
179
+ const category = normalizeCategory(value.category);
180
+ const runtimeId = normalizeString(value.runtimeId, 128) ?? undefined;
181
+ const turnId = normalizeString(value.turnId, 128) ?? undefined;
182
+ const native = normalizeNativeRequest(value.native);
183
+ const details = normalizeDetails(value.details);
184
+ return {
185
+ type: 'approval_request',
186
+ approvalId,
187
+ toolName,
188
+ toolSummary,
189
+ ...(category ? { category } : {}),
190
+ ...(risk ? { risk } : {}),
191
+ ...(riskLevel ? { riskLevel } : {}),
192
+ ...(runtimeId ? { runtimeId } : {}),
193
+ ...(turnId ? { turnId } : {}),
194
+ ...(native ? { native } : {}),
195
+ ...(details ? { details } : {}),
196
+ expiresAt: new Date(expiresMs).toISOString(),
197
+ };
198
+ }
package/dist/browser.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
2
  export { resolveCanonBaseUrl } from './base-url.js';
3
3
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
4
- export type { AddMemberResult, AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentRuntime, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonResolveAdmissionResult, ContactAddedPayload, ContactApprovedPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, ResolvedAdmission, SessionConfig, TurnUpdatedPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
4
+ export type { AddMemberResult, AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentRuntime, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonResolveAdmissionResult, ContactAddedPayload, ContactApprovedPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeProvenance, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, ResolvedAdmission, SessionConfig, TurnUpdatedPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
5
+ export { buildRuntimeProvenance, resolveRuntimeProvenance, } from './provenance.js';
5
6
  export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
6
7
  export type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
7
8
  export type { CanonSelfContext, CanonSelfContextType, SelfContextPromptRenderOptions, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, } from './self-context.js';
@@ -13,6 +14,7 @@ export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots,
13
14
  export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
14
15
  export type { DeliveryIntent, InboundDisposition, RuntimeCapabilities, TriggerDecision, TurnLifecycleState, TurnMessageSemantics, TurnMetadata, TurnState, } from './turn-protocol.js';
15
16
  export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
16
- export type { ApprovalRequestMetadata, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
17
+ export type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalNativeRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
18
+ export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
17
19
  export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, } from './runtime-cards.js';
18
20
  export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
package/dist/browser.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
2
  export { resolveCanonBaseUrl } from './base-url.js';
3
3
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
4
+ export { buildRuntimeProvenance, resolveRuntimeProvenance, } from './provenance.js';
4
5
  export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
5
6
  export { buildSelfContextPromptLines, normalizeSelfContexts, } from './self-context.js';
6
7
  export { buildAgentSessionSnapshot } from './agent-session.js';
@@ -8,4 +9,5 @@ export { CLAUDE_EFFORT_OPTIONS, EXECUTION_MODE_CONTROL_OPTIONS, RUNTIME_NEW_SESS
8
9
  export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
9
10
  export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
10
11
  export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
12
+ export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
11
13
  export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, } from './runtime-cards.js';
@@ -1,4 +1,4 @@
1
- import { type AgentClientType, type AgentRuntime, type CanonConversation, type CanonGroupContext, type CanonGroupContextMode, type CanonMembershipChange, type CanonMessage, type CanonMessagesPage, type MessageCreatedPayload } from './types.js';
1
+ import { type AgentClientType, type AgentRuntime, type CanonConversation, type CanonGroupContext, type CanonGroupContextMode, type CanonMembershipChange, type CanonMessage, type CanonMessagesPage, type CanonRuntimeProvenance, type MessageCreatedPayload } from './types.js';
2
2
  import { type CanonClient } from './client.js';
3
3
  import { type SessionWorkspaceConfig } from './execution-environment.js';
4
4
  import { type ResolvedAgentBehaviorPolicy } from './policy.js';
@@ -23,13 +23,31 @@ export interface HostInboundParticipantContext {
23
23
  }
24
24
  type HostInboundMessage = {
25
25
  id?: string | null;
26
+ senderId?: string | null;
27
+ senderName?: string | null;
26
28
  text?: string | null;
27
29
  contentType?: CanonMessage['contentType'] | null;
28
30
  attachments?: CanonMessage['attachments'];
29
31
  senderType?: CanonMessage['senderType'];
30
32
  mentions?: string[] | null;
31
33
  contactCard?: CanonMessage['contactCard'];
34
+ replyTo?: string | null;
35
+ replyToPosition?: number | null;
36
+ deleted?: boolean | null;
32
37
  };
38
+ export interface CanonReplyContext {
39
+ messageId: string;
40
+ senderId?: string | null;
41
+ senderName?: string | null;
42
+ senderType?: CanonMessage['senderType'] | null;
43
+ text?: string | null;
44
+ contentType?: CanonMessage['contentType'] | null;
45
+ attachments?: CanonMessage['attachments'];
46
+ contactCard?: CanonMessage['contactCard'];
47
+ body: string;
48
+ replyToPosition?: number | null;
49
+ found: boolean;
50
+ }
33
51
  export declare function buildCanonHostPrompt(input: {
34
52
  hostLabel: string;
35
53
  content: string;
@@ -37,9 +55,11 @@ export declare function buildCanonHostPrompt(input: {
37
55
  participantContext: HostInboundParticipantContext;
38
56
  behavior?: ResolvedAgentBehaviorPolicy | null;
39
57
  selfContexts?: MessageCreatedPayload['selfContexts'];
58
+ replyContext?: CanonReplyContext | null;
40
59
  buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
41
60
  sessionContextLines?: string[];
42
61
  }): string;
62
+ export declare function buildCanonReplyContextLines(replyContext: CanonReplyContext | null): string[];
43
63
  /**
44
64
  * Render the **text portion** of an inbound Canon message. Images are
45
65
  * referenced by short placeholders — their actual bytes are delivered to the
@@ -60,12 +80,18 @@ export declare function renderCanonHostInboundContent(message: HostInboundMessag
60
80
  durationMs?: number;
61
81
  index: number;
62
82
  }>): string;
83
+ export declare function resolveCanonReplyContext(input: {
84
+ message: HostInboundMessage;
85
+ messages?: ReadonlyArray<HostInboundMessage> | null;
86
+ }): CanonReplyContext | null;
63
87
  export declare function buildHydratedInboundContext(input: {
64
88
  agentId: string;
89
+ conversationId: string;
65
90
  conversation: CanonConversation | null;
66
91
  page?: CanonMessagesPage | null;
67
92
  activeSelfContextId?: string | null;
68
93
  selfContexts?: MessageCreatedPayload['selfContexts'];
94
+ provenance?: MessageCreatedPayload['provenance'];
69
95
  message: HostInboundMessage;
70
96
  senderName: string;
71
97
  isOwner: boolean;
@@ -78,6 +104,8 @@ export declare function buildHydratedInboundContext(input: {
78
104
  behavior?: ResolvedAgentBehaviorPolicy | null;
79
105
  activeSelfContextId: string | null;
80
106
  selfContexts: NonNullable<MessageCreatedPayload['selfContexts']>;
107
+ provenance: CanonRuntimeProvenance;
108
+ replyContext: CanonReplyContext | null;
81
109
  hydratedFromPage: boolean;
82
110
  };
83
111
  export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
@@ -3,6 +3,7 @@ import { buildAgentSessionSnapshot } from './agent-session.js';
3
3
  import { buildConversationWorktreeSpec, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, } from './execution-environment.js';
4
4
  import { buildBehaviorPolicyLines, buildParticipationHistorySnapshot, } from './policy.js';
5
5
  import { buildSelfContextPromptLines, normalizeSelfContexts, resolveMessageActiveSelfContextId, selectActiveSelfContexts, } from './self-context.js';
6
+ import { resolveRuntimeProvenance } from './provenance.js';
6
7
  import { rtdbRead } from './rtdb-rest.js';
7
8
  import { createRuntimeStatePublisher } from './runtime-state-publisher.js';
8
9
  const HOST_INBOUND_CONTACT_CARD_ACTION_CAPABILITIES = Object.freeze({
@@ -29,11 +30,32 @@ export function buildCanonHostPrompt(input) {
29
30
  ? ['Canon session state:', ...input.sessionContextLines]
30
31
  : []),
31
32
  `Conversation ID: ${input.conversationId}`,
33
+ ...buildCanonReplyContextLines(input.replyContext ?? null),
32
34
  '',
33
35
  'New Canon message:',
34
36
  input.content,
35
37
  ].join('\n');
36
38
  }
39
+ export function buildCanonReplyContextLines(replyContext) {
40
+ if (!replyContext)
41
+ return [];
42
+ const sender = replyContext.senderName || replyContext.senderId || 'unknown sender';
43
+ const senderType = replyContext.senderType ? `, ${replyContext.senderType}` : '';
44
+ const position = replyContext.replyToPosition != null
45
+ ? ` at ${formatReplyPosition(replyContext.replyToPosition)}`
46
+ : '';
47
+ const header = replyContext.found
48
+ ? `This message is replying to ${sender}${senderType} (message ${replyContext.messageId}${position}).`
49
+ : `This message is replying to message ${replyContext.messageId}${position}, but that message was not present in fetched history.`;
50
+ return [
51
+ '',
52
+ 'Reply context:',
53
+ header,
54
+ ...(replyContext.found
55
+ ? ['Replied message:', replyContext.body]
56
+ : []),
57
+ ];
58
+ }
37
59
  /**
38
60
  * Render the **text portion** of an inbound Canon message. Images are
39
61
  * referenced by short placeholders — their actual bytes are delivered to the
@@ -62,6 +84,35 @@ export function renderCanonHostInboundContent(message, materialized) {
62
84
  const rendered = [...placeholders, body].filter(Boolean).join('\n');
63
85
  return rendered || '[Empty message]';
64
86
  }
87
+ export function resolveCanonReplyContext(input) {
88
+ const replyTo = normalizeOptionalString(input.message.replyTo);
89
+ if (!replyTo)
90
+ return null;
91
+ const referenced = input.messages
92
+ ?.find((message) => message.id === replyTo && message.deleted !== true)
93
+ ?? null;
94
+ if (!referenced) {
95
+ return {
96
+ messageId: replyTo,
97
+ body: '',
98
+ replyToPosition: input.message.replyToPosition ?? null,
99
+ found: false,
100
+ };
101
+ }
102
+ return {
103
+ messageId: replyTo,
104
+ senderId: referenced.senderId ?? null,
105
+ senderName: referenced.senderName ?? null,
106
+ senderType: referenced.senderType ?? null,
107
+ text: referenced.text ?? null,
108
+ contentType: referenced.contentType ?? null,
109
+ attachments: referenced.attachments ?? [],
110
+ ...(referenced.contactCard ? { contactCard: referenced.contactCard } : {}),
111
+ body: renderCanonHostInboundContent(referenced),
112
+ replyToPosition: input.message.replyToPosition ?? null,
113
+ found: true,
114
+ };
115
+ }
65
116
  function describeContactCard(card) {
66
117
  const parts = [`${card.userType} · userId: ${card.userId}`];
67
118
  if (card.ownerName)
@@ -88,7 +139,8 @@ function describeContactCard(card) {
88
139
  }
89
140
  function describeAttachment(attachment, materialized) {
90
141
  if (attachment.kind === 'image') {
91
- return '[Image attached]';
142
+ const ref = materialized?.path ? ` ${materialized.path}` : '';
143
+ return `[Image attached${ref}]`;
92
144
  }
93
145
  if (attachment.kind === 'audio') {
94
146
  const durationMs = materialized?.durationMs ?? attachment.durationMs;
@@ -101,6 +153,16 @@ function describeAttachment(attachment, materialized) {
101
153
  const ref = materialized?.path ? ` ${materialized.path}` : '';
102
154
  return `[File: ${label}${ref}]`;
103
155
  }
156
+ function formatReplyPosition(positionMs) {
157
+ if (!Number.isFinite(positionMs) || positionMs < 0)
158
+ return `${positionMs}ms`;
159
+ const totalSeconds = Math.floor(positionMs / 1000);
160
+ const minutes = Math.floor(totalSeconds / 60);
161
+ const seconds = totalSeconds % 60;
162
+ return minutes > 0
163
+ ? `${minutes}:${seconds.toString().padStart(2, '0')}`
164
+ : `${seconds}s`;
165
+ }
104
166
  export function buildHydratedInboundContext(input) {
105
167
  const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
106
168
  const activeSelfContextId = resolveMessageActiveSelfContextId({
@@ -112,6 +174,21 @@ export function buildHydratedInboundContext(input) {
112
174
  ...(input.page?.selfContexts ?? []),
113
175
  ...(input.selfContexts ?? []),
114
176
  ], activeSelfContextId);
177
+ const resolvedActiveSelfContextId = activeSelfContexts.length > 0 ? activeSelfContextId : null;
178
+ const provenance = resolveRuntimeProvenance({
179
+ provenance: input.provenance,
180
+ conversationId: input.conversationId,
181
+ conversationType: input.conversation?.type ?? 'unknown',
182
+ memberCount: input.conversation?.memberIds?.length ?? null,
183
+ senderId: input.message.senderId ?? '',
184
+ senderName: input.senderName,
185
+ senderType: input.message.senderType ?? 'human',
186
+ isOwner: input.isOwner,
187
+ agentId: input.agentId,
188
+ mentions: input.message.mentions ?? [],
189
+ activeSelfContextId: resolvedActiveSelfContextId,
190
+ selfContexts: activeSelfContexts,
191
+ });
115
192
  const groupContext = buildCanonGroupContext({
116
193
  conversation: input.conversation,
117
194
  messages: [
@@ -140,8 +217,13 @@ export function buildHydratedInboundContext(input) {
140
217
  currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
141
218
  },
142
219
  behavior: input.page?.behavior ?? input.conversation?.behavior,
143
- activeSelfContextId: activeSelfContexts.length > 0 ? activeSelfContextId : null,
220
+ activeSelfContextId: resolvedActiveSelfContextId,
144
221
  selfContexts: activeSelfContexts,
222
+ provenance,
223
+ replyContext: resolveCanonReplyContext({
224
+ message: input.message,
225
+ messages: input.page?.messages ?? [],
226
+ }),
145
227
  hydratedFromPage: input.page != null,
146
228
  };
147
229
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
- export type { AddMemberResult, AgentCapabilities, AgentClientType, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonGroupContext, CanonGroupContextMode, CanonKnownRecentParticipant, CanonMembershipChange, CanonResolveAdmissionResult, ConversationUpdatedPayload, ContactAddedPayload, ContactApprovedPayload, ContactCardPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonMessage, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
2
+ export type { AddMemberResult, AgentCapabilities, AgentClientType, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonGroupContext, CanonGroupContextMode, CanonKnownRecentParticipant, CanonMembershipChange, CanonResolveAdmissionResult, ConversationUpdatedPayload, ContactAddedPayload, ContactApprovedPayload, ContactCardPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonMessage, CanonRuntimeProvenance, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
3
+ export { buildRuntimeProvenance, resolveRuntimeProvenance, } from './provenance.js';
3
4
  export type { CanonSelfContext, CanonSelfContextType, SelfContextPromptRenderOptions, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, } from './self-context.js';
4
5
  export { buildSelfContextPromptLines, normalizeSelfContexts, resolveMessageActiveSelfContextId, selectActiveSelfContexts, } from './self-context.js';
5
6
  export { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, buildWorkspaceRootId, discoverWorkspaceProjects, } from './workspace-discovery.js';
@@ -16,8 +17,8 @@ export type { DeliveryIntent, TurnMessageSemantics, InboundDisposition, TurnLife
16
17
  export { ackRegistrationApproval, registerAndWaitForApproval, submitRegistrationRequest, waitForRegistrationApproval, } from './registration.js';
17
18
  export { ApprovalManager } from './approval-manager.js';
18
19
  export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
19
- export { DEFAULT_APPROVAL_CONFIG, } from './approval-types.js';
20
- export type { ApprovalRequestMetadata, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
20
+ export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
21
+ export type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalNativeRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
21
22
  export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, } from './runtime-cards.js';
22
23
  export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
23
24
  export { createStreamingHelper } from './streaming.js';
@@ -32,8 +33,8 @@ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, build
32
33
  export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
33
34
  export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, readRuntimeActivity, writeRuntimeActivity, removeRuntimeActivityItem, clearRuntimeActivity, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
34
35
  export type { AgentSessionSnapshotPatch, RTDBClientHandle, RuntimeActivityPayloadData, RuntimeInfoPayloadData, SessionStatePayload, TurnStatePayload, } from './rtdb-rest.js';
35
- export { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
36
- export type { HostInboundParticipantContext, } from './host-runtime.js';
36
+ export { buildCanonHostPrompt, buildCanonReplyContextLines, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveCanonReplyContext, resolveHostWorkspaceCwd, } from './host-runtime.js';
37
+ export type { CanonReplyContext, HostInboundParticipantContext, } from './host-runtime.js';
37
38
  export { buildCanonGroupContext, buildCanonKnownRecentParticipants, buildCompactGroupContextLines, diffCanonMemberIds, } from './group-context.js';
38
39
  export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
39
40
  export type { RuntimeStatePublisher, RuntimeStatePublisherOptions, RuntimeStreamingPayload, ClearRuntimeActivityOptions, } from './runtime-state-publisher.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Types
2
2
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
3
+ export { buildRuntimeProvenance, resolveRuntimeProvenance, } from './provenance.js';
3
4
  export { buildSelfContextPromptLines, normalizeSelfContexts, resolveMessageActiveSelfContextId, selectActiveSelfContexts, } from './self-context.js';
4
5
  export { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, buildWorkspaceRootId, discoverWorkspaceProjects, } from './workspace-discovery.js';
5
6
  // Client
@@ -16,7 +17,7 @@ export { ackRegistrationApproval, registerAndWaitForApproval, submitRegistration
16
17
  // Approval
17
18
  export { ApprovalManager } from './approval-manager.js';
18
19
  export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
19
- export { DEFAULT_APPROVAL_CONFIG, } from './approval-types.js';
20
+ export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
20
21
  export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, } from './runtime-cards.js';
21
22
  // Streaming (RTDB helpers)
22
23
  export { createStreamingHelper } from './streaming.js';
@@ -31,7 +32,7 @@ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, build
31
32
  // RTDB REST helpers (token exchange, session state, generic read/write)
32
33
  export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, readRuntimeActivity, writeRuntimeActivity, removeRuntimeActivityItem, clearRuntimeActivity, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
33
34
  // Runtime host plumbing
34
- export { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
35
+ export { buildCanonHostPrompt, buildCanonReplyContextLines, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveCanonReplyContext, resolveHostWorkspaceCwd, } from './host-runtime.js';
35
36
  export { buildCanonGroupContext, buildCanonKnownRecentParticipants, buildCompactGroupContextLines, diffCanonMemberIds, } from './group-context.js';
36
37
  export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
37
38
  // Message formatting (LLM-facing text projection)
@@ -15,26 +15,14 @@ export function formatCanonMessageAsText(message) {
15
15
  const cardText = formatContactCard(message.contactCard);
16
16
  return trimmedText ? `${cardText}\n${trimmedText}` : cardText;
17
17
  }
18
- const attachment = pickPrimaryAttachment(message.attachments);
19
- if (attachment?.kind === 'image') {
20
- return trimmedText ? `[image] ${trimmedText}` : '[image]';
21
- }
22
- if (attachment?.kind === 'audio') {
23
- const seconds = typeof attachment.durationMs === 'number'
24
- ? ` ${Math.round(attachment.durationMs / 1000)}s`
25
- : '';
26
- return trimmedText ? `[audio${seconds}] ${trimmedText}` : `[audio${seconds}]`;
27
- }
28
- if (attachment?.kind === 'file') {
29
- const label = attachment.fileName?.trim() || 'file';
30
- return trimmedText ? `[${label}] ${trimmedText}` : `[${label}]`;
18
+ const attachmentLabels = (message.attachments ?? [])
19
+ .filter((attachment) => Boolean(attachment.url))
20
+ .map(formatAttachment);
21
+ if (attachmentLabels.length > 0) {
22
+ return [...attachmentLabels, trimmedText].filter(Boolean).join('\n');
31
23
  }
32
24
  return trimmedText || '[message]';
33
25
  }
34
- function pickPrimaryAttachment(attachments) {
35
- const first = attachments?.[0];
36
- return first?.url ? first : null;
37
- }
38
26
  function formatContactCard(card) {
39
27
  const displayName = card.displayName?.trim() || 'Unknown';
40
28
  const parts = [card.userType, `userId: ${card.userId}`];
@@ -44,3 +32,16 @@ function formatContactCard(card) {
44
32
  parts.push(`about: ${card.about}`);
45
33
  return `[Contact card] "${displayName}" — ${parts.join(' · ')}`;
46
34
  }
35
+ function formatAttachment(attachment) {
36
+ if (attachment.kind === 'image') {
37
+ return '[image]';
38
+ }
39
+ if (attachment.kind === 'audio') {
40
+ const seconds = typeof attachment.durationMs === 'number'
41
+ ? ` ${Math.round(attachment.durationMs / 1000)}s`
42
+ : '';
43
+ return `[audio${seconds}]`;
44
+ }
45
+ const label = attachment.fileName?.trim() || 'file';
46
+ return `[${label}]`;
47
+ }
@@ -0,0 +1,31 @@
1
+ import type { CanonConversation, CanonMessage, CanonRuntimeProvenance } from './types.js';
2
+ import type { CanonSelfContext } from './self-context.js';
3
+ export declare function buildRuntimeProvenance(input: {
4
+ conversationId: string;
5
+ conversationType?: CanonConversation['type'] | 'unknown' | string | null;
6
+ memberCount?: number | null;
7
+ senderId: string;
8
+ senderName?: string | null;
9
+ senderType?: CanonMessage['senderType'] | string | null;
10
+ isOwner?: boolean | null;
11
+ agentId?: string | null;
12
+ mentions?: readonly string[] | null;
13
+ mentionedAgent?: boolean | null;
14
+ activeSelfContextId?: string | null;
15
+ selfContexts?: readonly CanonSelfContext[] | null;
16
+ }): CanonRuntimeProvenance;
17
+ export declare function resolveRuntimeProvenance(input: {
18
+ provenance?: CanonRuntimeProvenance | null;
19
+ conversationId: string;
20
+ conversationType?: CanonConversation['type'] | 'unknown' | string | null;
21
+ memberCount?: number | null;
22
+ senderId: string;
23
+ senderName?: string | null;
24
+ senderType?: CanonMessage['senderType'] | string | null;
25
+ isOwner?: boolean | null;
26
+ agentId?: string | null;
27
+ mentions?: readonly string[] | null;
28
+ mentionedAgent?: boolean | null;
29
+ activeSelfContextId?: string | null;
30
+ selfContexts?: readonly CanonSelfContext[] | null;
31
+ }): CanonRuntimeProvenance;
@@ -0,0 +1,71 @@
1
+ function normalizeConversationType(value) {
2
+ return value === 'direct' || value === 'group' ? value : 'unknown';
3
+ }
4
+ function normalizeSenderType(value) {
5
+ return value === 'ai_agent' ? 'ai_agent' : 'human';
6
+ }
7
+ function normalizeMemberCount(value) {
8
+ if (value == null)
9
+ return value;
10
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
11
+ }
12
+ function normalizeName(value) {
13
+ if (typeof value !== 'string')
14
+ return value == null ? value : undefined;
15
+ const trimmed = value.trim();
16
+ return trimmed ? trimmed : null;
17
+ }
18
+ function findSelfContextType(selfContexts, activeSelfContextId) {
19
+ if (!activeSelfContextId)
20
+ return null;
21
+ return selfContexts?.find((entry) => entry.id === activeSelfContextId)?.type ?? null;
22
+ }
23
+ export function buildRuntimeProvenance(input) {
24
+ const activeSelfContextId = typeof input.activeSelfContextId === 'string' && input.activeSelfContextId.trim()
25
+ ? input.activeSelfContextId.trim()
26
+ : null;
27
+ const mentionedAgent = typeof input.mentionedAgent === 'boolean'
28
+ ? input.mentionedAgent
29
+ : Boolean(input.agentId && input.mentions?.includes(input.agentId));
30
+ return {
31
+ conversation: {
32
+ id: input.conversationId,
33
+ type: normalizeConversationType(input.conversationType),
34
+ ...(input.memberCount !== undefined
35
+ ? { memberCount: normalizeMemberCount(input.memberCount) }
36
+ : {}),
37
+ },
38
+ sender: {
39
+ id: input.senderId,
40
+ ...(input.senderName !== undefined ? { name: normalizeName(input.senderName) } : {}),
41
+ type: normalizeSenderType(input.senderType),
42
+ isOwner: input.isOwner === true,
43
+ },
44
+ mentionedAgent,
45
+ activeSelfContext: activeSelfContextId
46
+ ? {
47
+ id: activeSelfContextId,
48
+ type: findSelfContextType(input.selfContexts, activeSelfContextId),
49
+ }
50
+ : null,
51
+ };
52
+ }
53
+ export function resolveRuntimeProvenance(input) {
54
+ if (!input.provenance) {
55
+ return buildRuntimeProvenance(input);
56
+ }
57
+ return buildRuntimeProvenance({
58
+ conversationId: input.provenance.conversation?.id || input.conversationId,
59
+ conversationType: input.provenance.conversation?.type ?? input.conversationType,
60
+ memberCount: input.provenance.conversation?.memberCount ?? input.memberCount,
61
+ senderId: input.provenance.sender?.id || input.senderId,
62
+ senderName: input.provenance.sender?.name ?? input.senderName,
63
+ senderType: input.provenance.sender?.type ?? input.senderType,
64
+ isOwner: input.provenance.sender?.isOwner ?? input.isOwner,
65
+ agentId: input.agentId,
66
+ mentions: input.mentions,
67
+ mentionedAgent: input.provenance.mentionedAgent ?? input.mentionedAgent,
68
+ activeSelfContextId: input.provenance.activeSelfContext?.id ?? input.activeSelfContextId,
69
+ selfContexts: input.selfContexts,
70
+ });
71
+ }
@@ -8,12 +8,12 @@ export const EXECUTION_MODE_CONTROL_OPTIONS = [
8
8
  {
9
9
  value: 'worktree',
10
10
  label: 'Isolated worktree',
11
- description: 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.',
11
+ description: 'Best-effort git worktree for this conversation. This is not a security sandbox; if unavailable, Canon may fall back to the shared project.',
12
12
  },
13
13
  {
14
14
  value: 'locked',
15
15
  label: 'Use shared project',
16
- description: 'Runs directly in the selected project folder. Changes happen there.',
16
+ description: 'Runs directly in the selected project folder. File changes happen there.',
17
17
  },
18
18
  ];
19
19
  export function buildRuntimeWorkspaceControlOptions(workspaces) {
package/dist/types.d.ts CHANGED
@@ -81,6 +81,24 @@ export interface CanonMessagesPage {
81
81
  activeSelfContextIdByMessageId?: Record<string, string>;
82
82
  selfContexts?: CanonSelfContext[];
83
83
  }
84
+ export interface CanonRuntimeProvenance {
85
+ conversation: {
86
+ id: string;
87
+ type: CanonConversation['type'] | 'unknown';
88
+ memberCount?: number | null;
89
+ };
90
+ sender: {
91
+ id: string;
92
+ name?: string | null;
93
+ type: CanonMessage['senderType'];
94
+ isOwner: boolean;
95
+ };
96
+ mentionedAgent: boolean;
97
+ activeSelfContext?: {
98
+ id: string;
99
+ type?: CanonSelfContext['type'] | string | null;
100
+ } | null;
101
+ }
84
102
  export type CanonContactRequestStatus = 'pending' | 'approved' | 'rejected' | 'expired';
85
103
  export interface CanonContactRequest {
86
104
  id: string;
@@ -88,6 +106,10 @@ export interface CanonContactRequest {
88
106
  requesterName: string;
89
107
  requesterAvatarUrl: string | null;
90
108
  targetId: string;
109
+ targetName?: string | null;
110
+ targetAvatarUrl?: string | null;
111
+ targetUserType?: 'human' | 'ai_agent' | null;
112
+ targetOwnerId?: string | null;
91
113
  approverId: string;
92
114
  message: string | null;
93
115
  status: CanonContactRequestStatus;
@@ -415,6 +437,7 @@ export interface MessageCreatedPayload {
415
437
  behavior?: ResolvedAgentBehaviorPolicy;
416
438
  activeSelfContextId?: string | null;
417
439
  selfContexts?: CanonSelfContext[];
440
+ provenance?: CanonRuntimeProvenance;
418
441
  message: {
419
442
  id: string;
420
443
  senderId: string;
@@ -616,6 +639,7 @@ export interface SessionConfig {
616
639
  export interface PermissionModeOption {
617
640
  value: string;
618
641
  label: string;
642
+ description?: string;
619
643
  }
620
644
  export declare const CLAUDE_PERMISSION_MODE_OPTIONS: readonly [{
621
645
  readonly value: "default";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",