@canonmsg/backend-contracts 2.2.0 → 3.0.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.
@@ -29,7 +29,7 @@
29
29
  * endpoints validate against these schemas directly).
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.CANON_VERB_SCHEMA_REFS = exports.CANON_VERBS_JSON_SCHEMA = void 0;
32
+ exports.CANON_VERB_SCHEMA_REFS = exports.CANON_VERBS_JSON_SCHEMA = exports.VERB_SESSION_CONFIG_JSON_SCHEMA = exports.VERB_NATIVE_METADATA_JSON_SCHEMA = void 0;
33
33
  exports.getVerbInputSchema = getVerbInputSchema;
34
34
  exports.getVerbResultSchema = getVerbResultSchema;
35
35
  const verbContract_js_1 = require("./verbContract.js");
@@ -252,12 +252,89 @@ const runtimeCardDef = {
252
252
  blocks: { type: 'array', minItems: 1, maxItems: verbContract_js_1.VERB_LIMITS.cardServerBlocks },
253
253
  },
254
254
  };
255
- const nativeDef = {
255
+ const runtimeCorrelationValueDef = {
256
+ type: 'string',
257
+ pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeCorrelationValue,
258
+ maxLength: verbContract_js_1.VERB_LIMITS.nativeValueChars,
259
+ };
260
+ const runtimeMethodDef = {
261
+ type: 'string',
262
+ pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeMethod,
263
+ maxLength: verbContract_js_1.VERB_LIMITS.nativeValueChars,
264
+ };
265
+ exports.VERB_NATIVE_METADATA_JSON_SCHEMA = {
256
266
  type: 'object',
257
- description: `Runtime correlation handles (<= ${verbContract_js_1.VERB_LIMITS.nativeKeys} keys, string values <= `
258
- + `${verbContract_js_1.VERB_LIMITS.nativeValueChars} chars or a nested handles map <= ${verbContract_js_1.VERB_LIMITS.nativeHandles}; `
259
- + 'functions/src/api/interactionKinds.ts normalizeNative).',
260
- additionalProperties: true,
267
+ description: 'Content-free runtime correlation ids. Commands, paths, prompts, summaries, '
268
+ + 'and model labels are not permitted in this public envelope.',
269
+ maxProperties: verbContract_js_1.VERB_LIMITS.nativeKeys,
270
+ additionalProperties: false,
271
+ properties: {
272
+ runtime: runtimeCorrelationValueDef,
273
+ method: runtimeMethodDef,
274
+ requestId: runtimeCorrelationValueDef,
275
+ provider: runtimeCorrelationValueDef,
276
+ origin: runtimeCorrelationValueDef,
277
+ surface: runtimeCorrelationValueDef,
278
+ threadId: runtimeCorrelationValueDef,
279
+ turnId: runtimeCorrelationValueDef,
280
+ runId: runtimeCorrelationValueDef,
281
+ itemId: runtimeCorrelationValueDef,
282
+ toolCallId: runtimeCorrelationValueDef,
283
+ approvalId: runtimeCorrelationValueDef,
284
+ pluginId: runtimeCorrelationValueDef,
285
+ sessionKey: runtimeCorrelationValueDef,
286
+ nodeId: runtimeCorrelationValueDef,
287
+ handles: {
288
+ type: 'object',
289
+ maxProperties: verbContract_js_1.VERB_LIMITS.nativeHandles,
290
+ propertyNames: { pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeHandleKey },
291
+ additionalProperties: runtimeCorrelationValueDef,
292
+ },
293
+ },
294
+ };
295
+ const sessionConfigValueDef = {
296
+ type: 'string',
297
+ pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeOptionValue,
298
+ maxLength: verbContract_js_1.VERB_LIMITS.sessionConfigValueChars,
299
+ };
300
+ exports.VERB_SESSION_CONFIG_JSON_SCHEMA = {
301
+ type: 'object',
302
+ description: 'Typed setup policy for an agent target. Values are stable runtime-advertised '
303
+ + 'option ids and are validated against the target descriptor before session creation; '
304
+ + 'labels, prompts, workspace paths, and arbitrary fields are not permitted.',
305
+ additionalProperties: false,
306
+ properties: {
307
+ model: sessionConfigValueDef,
308
+ permissionMode: sessionConfigValueDef,
309
+ effort: sessionConfigValueDef,
310
+ workspaceId: {
311
+ type: 'string',
312
+ pattern: verbContract_js_1.VERB_ID_PATTERNS.workspaceOptionId,
313
+ maxLength: verbContract_js_1.VERB_LIMITS.sessionConfigValueChars,
314
+ },
315
+ executionMode: { enum: ['worktree', 'locked'] },
316
+ runtimeControlValues: {
317
+ type: 'object',
318
+ maxProperties: verbContract_js_1.VERB_LIMITS.sessionConfigValues,
319
+ propertyNames: {
320
+ pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeControlId,
321
+ not: {
322
+ enum: [
323
+ 'model',
324
+ 'workspace',
325
+ 'executionMode',
326
+ 'permissionMode',
327
+ 'effort',
328
+ 'interrupt',
329
+ 'runtimeControlValues',
330
+ 'updatedAt',
331
+ 'updatedBy',
332
+ ],
333
+ },
334
+ },
335
+ additionalProperties: sessionConfigValueDef,
336
+ },
337
+ },
261
338
  };
262
339
  const timeoutFields = (maxMs, ceilingNote) => ({
263
340
  timeoutMs: {
@@ -270,7 +347,7 @@ const timeoutFields = (maxMs, ceilingNote) => ({
270
347
  type: 'integer',
271
348
  minimum: 0,
272
349
  description: 'Absolute epoch-ms deadline. The REST interaction creators REQUIRE a deadline '
273
- + '(and /runtime-input/request also requires kind): bindings must fill defaults '
350
+ + '(input interactions also require input subtype kind): bindings must fill defaults '
274
351
  + 'and convert timeoutMs into expiresAt before calling the endpoint.',
275
352
  },
276
353
  });
@@ -313,7 +390,7 @@ const send_to_input = {
313
390
  },
314
391
  sessionSelection: REF('sessionSelection'),
315
392
  sessionConfig: {
316
- type: ['object', 'null'],
393
+ oneOf: [REF('sessionConfig'), { type: 'null' }],
317
394
  description: 'Coding-agent session setup (model/permissionMode/effort/workspaceId/executionMode); '
318
395
  + 'only applied to agent targets, stripped for humans.',
319
396
  },
@@ -544,7 +621,7 @@ const check_approval_input = {
544
621
  };
545
622
  const check_approval_result = {
546
623
  description: "'unknown' is NOT a denial — only 'resolved' carries a decision. Canonical statuses map "
547
- + 'from the /runtime-approval/consume wire as: allow/deny -> resolved.decision, timeout -> '
624
+ + 'from the approval interaction consume result as: allow/deny -> resolved.decision, timeout -> '
548
625
  + 'expired, pending -> pending, HTTP 404 RUNTIME_APPROVAL_NOT_FOUND -> unknown.',
549
626
  oneOf: [
550
627
  {
@@ -1005,7 +1082,8 @@ exports.CANON_VERBS_JSON_SCHEMA = {
1005
1082
  unifiedDiff: unifiedDiffDef,
1006
1083
  sessionRule: sessionRuleDef,
1007
1084
  runtimeCard: runtimeCardDef,
1008
- native: nativeDef,
1085
+ native: exports.VERB_NATIVE_METADATA_JSON_SCHEMA,
1086
+ sessionConfig: exports.VERB_SESSION_CONFIG_JSON_SCHEMA,
1009
1087
  send_to_input,
1010
1088
  send_to_result,
1011
1089
  request_input_input,
@@ -32,6 +32,7 @@ exports.projectVerbIntentToWire = projectVerbIntentToWire;
32
32
  exports.mergeVerbWireToIntent = mergeVerbWireToIntent;
33
33
  exports.findVerbWireEnvelopeViolations = findVerbWireEnvelopeViolations;
34
34
  const verbContract_js_1 = require("./verbContract.js");
35
+ const verbSchemas_js_1 = require("./verbSchemas.js");
35
36
  exports.CANON_VERB_WIRE_SCHEMA_VERSION = 'canon.verb-wire.v1';
36
37
  exports.CANON_VERB_WIRE_SCHEMA_ID = 'https://canonmsg.com/schemas/canon.verb-wire.v1.json';
37
38
  // ---------------------------------------------------------------------------
@@ -204,7 +205,9 @@ const wireEnvelopeDef = {
204
205
  },
205
206
  ],
206
207
  },
207
- sessionConfig: { type: ['object', 'null'] },
208
+ sessionConfig: {
209
+ oneOf: [verbSchemas_js_1.VERB_SESSION_CONFIG_JSON_SCHEMA, { type: 'null' }],
210
+ },
208
211
  idempotencyKey: {
209
212
  type: 'string',
210
213
  pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId,
@@ -227,7 +230,7 @@ const wireEnvelopeDef = {
227
230
  + 'overload the mention slot.',
228
231
  },
229
232
  turn: { $ref: '#/$defs/turn' },
230
- native: { type: 'object' },
233
+ native: verbSchemas_js_1.VERB_NATIVE_METADATA_JSON_SCHEMA,
231
234
  runtimeId: { type: 'string', maxLength: verbContract_js_1.VERB_LIMITS.turnIdChars },
232
235
  limit: { type: 'integer', minimum: 1 },
233
236
  },
@@ -10,6 +10,7 @@ export interface SerializedContactRequest {
10
10
  targetUserType: 'human' | 'ai_agent';
11
11
  targetOwnerId?: string | null;
12
12
  approverId: string;
13
+ /** Always null on agent-facing DTOs; free-form content stays in authorized review storage. */
13
14
  message: string | null;
14
15
  status: SerializedContactRequestStatus;
15
16
  kind: 'dm' | 'group_invite';
@@ -71,7 +71,10 @@ export function serializeContactRequest(requestId, data) {
71
71
  targetAvatarUrl: typeof data.targetAvatarUrl === 'string' ? data.targetAvatarUrl : null,
72
72
  targetUserType: data.targetUserType,
73
73
  approverId: data.approverId,
74
- message: typeof data.message === 'string' ? data.message : null,
74
+ // Free-form request content is owner-review material. This shared DTO is
75
+ // delivered to the target agent over REST/SSE, so it must never cross the
76
+ // admission boundary before the human approver accepts the reach-out.
77
+ message: null,
75
78
  status: normalizeStatus(data.status),
76
79
  kind: kindValue,
77
80
  createdAt: normalizeTimestamp(data.createdAt),
@@ -0,0 +1,25 @@
1
+ export declare const CANON_ENVIRONMENT_CONTRACTS: Readonly<{
2
+ readonly 'canon-dev-v1': Readonly<{
3
+ name: "dev";
4
+ projectId: "canonmail-dev";
5
+ region: "europe-west1";
6
+ }>;
7
+ readonly 'canon-prod-v1': Readonly<{
8
+ name: "prod";
9
+ projectId: "canonmail-prod";
10
+ region: "europe-west1";
11
+ }>;
12
+ readonly 'canon-legacy-v1': Readonly<{
13
+ name: "legacy";
14
+ projectId: "project-007a8e71-ba01-49e6-8aa";
15
+ region: "us-central1";
16
+ }>;
17
+ }>;
18
+ export type CanonEnvironmentId = keyof typeof CANON_ENVIRONMENT_CONTRACTS;
19
+ export type CanonEnvironmentName = (typeof CANON_ENVIRONMENT_CONTRACTS)[CanonEnvironmentId]['name'];
20
+ export type CanonEnvironmentContract = {
21
+ [EnvironmentId in CanonEnvironmentId]: {
22
+ environmentId: EnvironmentId;
23
+ } & (typeof CANON_ENVIRONMENT_CONTRACTS)[EnvironmentId];
24
+ }[CanonEnvironmentId];
25
+ export declare function getCanonEnvironmentContract(value: string): CanonEnvironmentContract;
@@ -0,0 +1,25 @@
1
+ export const CANON_ENVIRONMENT_CONTRACTS = Object.freeze({
2
+ 'canon-dev-v1': Object.freeze({
3
+ name: 'dev',
4
+ projectId: 'canonmail-dev',
5
+ region: 'europe-west1',
6
+ }),
7
+ 'canon-prod-v1': Object.freeze({
8
+ name: 'prod',
9
+ projectId: 'canonmail-prod',
10
+ region: 'europe-west1',
11
+ }),
12
+ 'canon-legacy-v1': Object.freeze({
13
+ name: 'legacy',
14
+ projectId: 'project-007a8e71-ba01-49e6-8aa',
15
+ region: 'us-central1',
16
+ }),
17
+ });
18
+ export function getCanonEnvironmentContract(value) {
19
+ const environmentId = value.trim();
20
+ const contract = CANON_ENVIRONMENT_CONTRACTS[environmentId];
21
+ if (!contract) {
22
+ throw new Error(`Unsupported Canon environment ID: ${value}`);
23
+ }
24
+ return Object.freeze({ environmentId, ...contract });
25
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './diffRedaction.js';
2
+ export * from './environment.js';
2
3
  export * from './media.js';
3
4
  export * from './message.js';
4
5
  export * from './runtimeCardFields.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './diffRedaction.js';
2
+ export * from './environment.js';
2
3
  export * from './media.js';
3
4
  export * from './message.js';
4
5
  export * from './runtimeCardFields.js';
@@ -67,6 +67,12 @@ export declare const VERB_ID_PATTERNS: {
67
67
  readonly actionId: "^[A-Za-z0-9_.:-]{1,80}$";
68
68
  readonly questionId: "^[A-Za-z0-9_.:-]{1,120}$";
69
69
  readonly sessionRuleToolPattern: "^[\\w.*:-]{1,128}$";
70
+ readonly runtimeCorrelationValue: "^[A-Za-z0-9@_][A-Za-z0-9_.:@+\\-]{0,255}$";
71
+ readonly runtimeMethod: "^[A-Za-z0-9_][A-Za-z0-9_./:\\-]{0,255}$";
72
+ readonly runtimeOptionValue: "^[A-Za-z0-9@_][A-Za-z0-9_.:/@+\\-]{0,255}$";
73
+ readonly workspaceOptionId: "^[A-Za-z0-9@_][A-Za-z0-9_.:@+\\-]{0,255}$";
74
+ readonly runtimeHandleKey: "^[A-Za-z0-9_.:-]{1,80}$";
75
+ readonly runtimeControlId: "^[A-Za-z0-9_-]{1,80}$";
70
76
  };
71
77
  /**
72
78
  * Server-enforced limits, single-sourced. Each value cites its enforcement
@@ -134,6 +140,9 @@ export declare const VERB_LIMITS: {
134
140
  readonly nativeKeys: 24;
135
141
  readonly nativeValueChars: 256;
136
142
  readonly nativeHandles: 16;
143
+ /** Initial agent-session setup carried by send_to. */
144
+ readonly sessionConfigValues: 24;
145
+ readonly sessionConfigValueChars: 256;
137
146
  /** Deadlines (functions/src/utils/runtimeRequestHelpers.ts). */
138
147
  readonly minTimeoutMs: 1000;
139
148
  /** input/card/plan ceiling (30 minutes). */
@@ -181,6 +190,50 @@ export type VerbSessionSelection = {
181
190
  mode: 'specific';
182
191
  conversationId: string;
183
192
  };
193
+ /**
194
+ * Public setup policy for the target agent session. Every value is a stable
195
+ * runtime-advertised option id, never a label, prompt, path, or other free
196
+ * text. The server also validates each selection against the target's live
197
+ * descriptor before creating a session.
198
+ */
199
+ export interface VerbSessionConfig {
200
+ model?: string;
201
+ permissionMode?: string;
202
+ effort?: string;
203
+ runtimeControlValues?: Record<string, string>;
204
+ workspaceId?: string;
205
+ executionMode?: 'worktree' | 'locked';
206
+ }
207
+ /**
208
+ * Bounded runtime correlation carried in the public wire envelope. These are
209
+ * opaque identifiers only; workspace paths, commands, prompts, summaries,
210
+ * and model labels belong in encrypted content or owner-local runtime state.
211
+ */
212
+ export interface VerbNativeMetadata {
213
+ runtime?: string;
214
+ method?: string;
215
+ requestId?: string;
216
+ provider?: string;
217
+ origin?: string;
218
+ surface?: string;
219
+ threadId?: string;
220
+ turnId?: string;
221
+ runId?: string;
222
+ itemId?: string;
223
+ toolCallId?: string;
224
+ approvalId?: string;
225
+ pluginId?: string;
226
+ sessionKey?: string;
227
+ nodeId?: string;
228
+ handles?: Record<string, string>;
229
+ }
230
+ export declare const VERB_NATIVE_METADATA_KEYS: readonly ["runtime", "method", "requestId", "provider", "origin", "surface", "threadId", "turnId", "runId", "itemId", "toolCallId", "approvalId", "pluginId", "sessionKey", "nodeId", "handles"];
231
+ /**
232
+ * Keep only the typed, content-free runtime correlation contract. Invalid or
233
+ * unknown fields are omitted on compatibility REST paths; canon.verb-wire.v1
234
+ * rejects the same fields through its strict JSON Schema.
235
+ */
236
+ export declare function normalizeVerbNativeMetadata(value: unknown): VerbNativeMetadata | undefined;
184
237
  export interface VerbMediaAttachment {
185
238
  kind: 'image' | 'audio' | 'video' | 'file';
186
239
  /** Must come from /media/upload (Canon storage) or the GIF picker — server allowlisted. */
@@ -242,7 +295,7 @@ export interface SendToInput {
242
295
  /** Only meaningful for user targets that are agents. Default: continue_or_create. */
243
296
  sessionSelection?: VerbSessionSelection;
244
297
  /** Coding-agent session setup; only applied when the target is an agent. */
245
- sessionConfig?: Record<string, unknown> | null;
298
+ sessionConfig?: VerbSessionConfig | null;
246
299
  messageOptions?: VerbMessageOptions;
247
300
  }
248
301
  export type SendToResult = {
@@ -308,7 +361,7 @@ export interface RequestInputInput {
308
361
  secretName?: string;
309
362
  sensitive?: boolean;
310
363
  responseUserId?: string;
311
- native?: Record<string, unknown>;
364
+ native?: VerbNativeMetadata;
312
365
  turnId?: string;
313
366
  /** Relative deadline; server ceiling is VERB_LIMITS.maxTimeoutMs (30 min). */
314
367
  timeoutMs?: number;
@@ -376,7 +429,7 @@ export interface RequestApprovalInput {
376
429
  category?: VerbApprovalCategory;
377
430
  details?: VerbApprovalDetail[];
378
431
  diff?: VerbUnifiedDiff;
379
- native?: Record<string, unknown>;
432
+ native?: VerbNativeMetadata;
380
433
  runtimeId?: string;
381
434
  turnId?: string;
382
435
  responseUserId?: string;
@@ -455,7 +508,7 @@ export interface SendCardInput {
455
508
  conversationId?: string;
456
509
  card: VerbRuntimeCard;
457
510
  cardId?: string;
458
- native?: Record<string, unknown>;
511
+ native?: VerbNativeMetadata;
459
512
  runtimeId?: string;
460
513
  turnId?: string;
461
514
  }
@@ -719,6 +772,9 @@ export declare const CANON_VERB_LIMITS_ARTIFACT: {
719
772
  readonly nativeKeys: 24;
720
773
  readonly nativeValueChars: 256;
721
774
  readonly nativeHandles: 16;
775
+ /** Initial agent-session setup carried by send_to. */
776
+ readonly sessionConfigValues: 24;
777
+ readonly sessionConfigValueChars: 256;
722
778
  /** Deadlines (functions/src/utils/runtimeRequestHelpers.ts). */
723
779
  readonly minTimeoutMs: 1000;
724
780
  /** input/card/plan ceiling (30 minutes). */
@@ -746,6 +802,12 @@ export declare const CANON_VERB_LIMITS_ARTIFACT: {
746
802
  readonly actionId: "^[A-Za-z0-9_.:-]{1,80}$";
747
803
  readonly questionId: "^[A-Za-z0-9_.:-]{1,120}$";
748
804
  readonly sessionRuleToolPattern: "^[\\w.*:-]{1,128}$";
805
+ readonly runtimeCorrelationValue: "^[A-Za-z0-9@_][A-Za-z0-9_.:@+\\-]{0,255}$";
806
+ readonly runtimeMethod: "^[A-Za-z0-9_][A-Za-z0-9_./:\\-]{0,255}$";
807
+ readonly runtimeOptionValue: "^[A-Za-z0-9@_][A-Za-z0-9_.:/@+\\-]{0,255}$";
808
+ readonly workspaceOptionId: "^[A-Za-z0-9@_][A-Za-z0-9_.:@+\\-]{0,255}$";
809
+ readonly runtimeHandleKey: "^[A-Za-z0-9_.:-]{1,80}$";
810
+ readonly runtimeControlId: "^[A-Za-z0-9_-]{1,80}$";
749
811
  };
750
812
  readonly byteSemantics: {
751
813
  readonly 'send_to.text': "utf8_bytes<=messageTextBytes";
@@ -67,6 +67,12 @@ export const VERB_ID_PATTERNS = {
67
67
  actionId: '^[A-Za-z0-9_.:-]{1,80}$',
68
68
  questionId: '^[A-Za-z0-9_.:-]{1,120}$',
69
69
  sessionRuleToolPattern: '^[\\w.*:-]{1,128}$',
70
+ runtimeCorrelationValue: '^[A-Za-z0-9@_][A-Za-z0-9_.:@+\\-]{0,255}$',
71
+ runtimeMethod: '^[A-Za-z0-9_][A-Za-z0-9_./:\\-]{0,255}$',
72
+ runtimeOptionValue: '^[A-Za-z0-9@_][A-Za-z0-9_.:/@+\\-]{0,255}$',
73
+ workspaceOptionId: '^[A-Za-z0-9@_][A-Za-z0-9_.:@+\\-]{0,255}$',
74
+ runtimeHandleKey: '^[A-Za-z0-9_.:-]{1,80}$',
75
+ runtimeControlId: '^[A-Za-z0-9_-]{1,80}$',
70
76
  };
71
77
  /**
72
78
  * Server-enforced limits, single-sourced. Each value cites its enforcement
@@ -134,6 +140,9 @@ export const VERB_LIMITS = {
134
140
  nativeKeys: 24,
135
141
  nativeValueChars: 256,
136
142
  nativeHandles: 16,
143
+ /** Initial agent-session setup carried by send_to. */
144
+ sessionConfigValues: 24,
145
+ sessionConfigValueChars: 256,
137
146
  /** Deadlines (functions/src/utils/runtimeRequestHelpers.ts). */
138
147
  minTimeoutMs: 1000,
139
148
  /** input/card/plan ceiling (30 minutes). */
@@ -181,6 +190,65 @@ export const CANON_VERB_NAMES = [
181
190
  'list_contact_requests',
182
191
  'list_conversations',
183
192
  ];
193
+ export const VERB_NATIVE_METADATA_KEYS = [
194
+ 'runtime',
195
+ 'method',
196
+ 'requestId',
197
+ 'provider',
198
+ 'origin',
199
+ 'surface',
200
+ 'threadId',
201
+ 'turnId',
202
+ 'runId',
203
+ 'itemId',
204
+ 'toolCallId',
205
+ 'approvalId',
206
+ 'pluginId',
207
+ 'sessionKey',
208
+ 'nodeId',
209
+ 'handles',
210
+ ];
211
+ const VERB_NATIVE_STRING_KEYS = VERB_NATIVE_METADATA_KEYS.filter((key) => key !== 'handles');
212
+ const RUNTIME_CORRELATION_VALUE_PATTERN = new RegExp(VERB_ID_PATTERNS.runtimeCorrelationValue);
213
+ const RUNTIME_METHOD_PATTERN = new RegExp(VERB_ID_PATTERNS.runtimeMethod);
214
+ const RUNTIME_HANDLE_KEY_PATTERN = new RegExp(VERB_ID_PATTERNS.runtimeHandleKey);
215
+ function isPlainRecord(value) {
216
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
217
+ }
218
+ /**
219
+ * Keep only the typed, content-free runtime correlation contract. Invalid or
220
+ * unknown fields are omitted on compatibility REST paths; canon.verb-wire.v1
221
+ * rejects the same fields through its strict JSON Schema.
222
+ */
223
+ export function normalizeVerbNativeMetadata(value) {
224
+ if (!isPlainRecord(value))
225
+ return undefined;
226
+ const native = {};
227
+ for (const key of VERB_NATIVE_STRING_KEYS) {
228
+ const raw = value[key];
229
+ if (typeof raw !== 'string')
230
+ continue;
231
+ const normalized = raw.trim();
232
+ const pattern = key === 'method'
233
+ ? RUNTIME_METHOD_PATTERN
234
+ : RUNTIME_CORRELATION_VALUE_PATTERN;
235
+ if (pattern.test(normalized))
236
+ native[key] = normalized;
237
+ }
238
+ if (isPlainRecord(value.handles)) {
239
+ const handles = {};
240
+ for (const [key, raw] of Object.entries(value.handles).slice(0, VERB_LIMITS.nativeHandles)) {
241
+ if (!RUNTIME_HANDLE_KEY_PATTERN.test(key) || typeof raw !== 'string')
242
+ continue;
243
+ const normalized = raw.trim();
244
+ if (RUNTIME_CORRELATION_VALUE_PATTERN.test(normalized))
245
+ handles[key] = normalized;
246
+ }
247
+ if (Object.keys(handles).length > 0)
248
+ native.handles = handles;
249
+ }
250
+ return Object.keys(native).length > 0 ? native : undefined;
251
+ }
184
252
  /**
185
253
  * Error code on the 400 a create_group receives when NO member is directly
186
254
  * addable (approval-required members can only be invited to an existing