@synoi/gap 0.1.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.
Files changed (58) hide show
  1. package/LICENSE +195 -0
  2. package/README.md +223 -0
  3. package/dist/canonicalize.d.ts +19 -0
  4. package/dist/canonicalize.d.ts.map +1 -0
  5. package/dist/canonicalize.js +36 -0
  6. package/dist/canonicalize.js.map +1 -0
  7. package/dist/capabilities.d.ts +605 -0
  8. package/dist/capabilities.d.ts.map +1 -0
  9. package/dist/capabilities.js +53 -0
  10. package/dist/capabilities.js.map +1 -0
  11. package/dist/cdro.d.ts +63 -0
  12. package/dist/cdro.d.ts.map +1 -0
  13. package/dist/cdro.js +16 -0
  14. package/dist/cdro.js.map +1 -0
  15. package/dist/channels.d.ts +107 -0
  16. package/dist/channels.d.ts.map +1 -0
  17. package/dist/channels.js +29 -0
  18. package/dist/channels.js.map +1 -0
  19. package/dist/constants.d.ts +32 -0
  20. package/dist/constants.d.ts.map +1 -0
  21. package/dist/constants.js +36 -0
  22. package/dist/constants.js.map +1 -0
  23. package/dist/index.d.ts +28 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +35 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/oid.d.ts +28 -0
  28. package/dist/oid.d.ts.map +1 -0
  29. package/dist/oid.js +68 -0
  30. package/dist/oid.js.map +1 -0
  31. package/dist/receipts.d.ts +128 -0
  32. package/dist/receipts.d.ts.map +1 -0
  33. package/dist/receipts.js +14 -0
  34. package/dist/receipts.js.map +1 -0
  35. package/dist/revocations.d.ts +65 -0
  36. package/dist/revocations.d.ts.map +1 -0
  37. package/dist/revocations.js +22 -0
  38. package/dist/revocations.js.map +1 -0
  39. package/dist/validate.d.ts +59 -0
  40. package/dist/validate.d.ts.map +1 -0
  41. package/dist/validate.js +835 -0
  42. package/dist/validate.js.map +1 -0
  43. package/dist/workflows.d.ts +186 -0
  44. package/dist/workflows.d.ts.map +1 -0
  45. package/dist/workflows.js +14 -0
  46. package/dist/workflows.js.map +1 -0
  47. package/package.json +55 -0
  48. package/src/canonicalize.ts +38 -0
  49. package/src/capabilities.ts +711 -0
  50. package/src/cdro.ts +92 -0
  51. package/src/channels.ts +183 -0
  52. package/src/constants.ts +46 -0
  53. package/src/index.ts +180 -0
  54. package/src/oid.ts +71 -0
  55. package/src/receipts.ts +169 -0
  56. package/src/revocations.ts +90 -0
  57. package/src/validate.ts +1008 -0
  58. package/src/workflows.ts +241 -0
@@ -0,0 +1,1008 @@
1
+ /**
2
+ * validate.ts -- hand-rolled runtime validators for GAP CDROs.
3
+ *
4
+ * Design: every validator returns `{ ok, errors }`. `ok` is true iff `errors`
5
+ * is empty. Validators are non-throwing. They check shape (type + required
6
+ * fields) without semantic validation (a grant with `expires_at_ms` in the
7
+ * past is "shape-valid" -- separate runtime check rejects it).
8
+ *
9
+ * No zod / no io-ts. The style mirrors synoi-mcp-server/src/tools.ts:
10
+ * minimal, predictable, and easy to debug.
11
+ *
12
+ * Round-trip property: any envelope produced by these types, run through
13
+ * JSON.stringify -> JSON.parse -> validate*, produces ok=true with the same
14
+ * top-level keys + values.
15
+ */
16
+
17
+ import type { GapCdroEnvelope, GapObjectType } from './cdro.js'
18
+ import { GAP_VERSION } from './cdro.js'
19
+ import type {
20
+ GapActorType,
21
+ Capability,
22
+ CapabilityDeclaration,
23
+ CapabilityDeclarationBody,
24
+ CapabilityGrant,
25
+ CapabilityGrantBody,
26
+ CapabilityInvocation,
27
+ CapabilityInvocationBody,
28
+ CapabilityPredicate,
29
+ GrantedCapabilityScope,
30
+ ConsentRecordBody,
31
+ CredentialKind,
32
+ DelegationStep,
33
+ IdentityBinding,
34
+ McpToolCallContext,
35
+ OrchestrationChainBody,
36
+ PipResponseBody,
37
+ TokenBudgetArgs,
38
+ ExternalPipArgs,
39
+ } from './capabilities.js'
40
+ import type { TokenConsumption } from './receipts.js'
41
+ import type {
42
+ ChannelEvent,
43
+ ChannelEventBody,
44
+ } from './channels.js'
45
+ import type {
46
+ StageTransition,
47
+ StageTransitionBody,
48
+ WorkflowDefinition,
49
+ WorkflowDefinitionBody,
50
+ WorkflowInstance,
51
+ WorkflowInstanceBody,
52
+ } from './workflows.js'
53
+ import type {
54
+ GapDecisionReceipt,
55
+ GapDecisionReceiptBody,
56
+ } from './receipts.js'
57
+ import type {
58
+ RevocationEvent,
59
+ RevocationEventBody,
60
+ } from './revocations.js'
61
+
62
+ // -- Result + small helpers --------------------------------------------------
63
+
64
+ export interface ValidationResult {
65
+ ok: boolean
66
+ errors: string[]
67
+ }
68
+
69
+ function ok(): ValidationResult { return { ok: true, errors: [] } }
70
+ function fail(...errors: string[]): ValidationResult { return { ok: false, errors } }
71
+ function merge(...results: ValidationResult[]): ValidationResult {
72
+ const errors: string[] = []
73
+ for (const r of results) errors.push(...r.errors)
74
+ return { ok: errors.length === 0, errors }
75
+ }
76
+
77
+ function isObject(v: unknown): v is Record<string, unknown> {
78
+ return typeof v === 'object' && v !== null && !Array.isArray(v)
79
+ }
80
+ function isString(v: unknown): v is string { return typeof v === 'string' }
81
+ function isNumber(v: unknown): v is number { return typeof v === 'number' && Number.isFinite(v) }
82
+ function isInteger(v: unknown): v is number { return Number.isInteger(v as number) }
83
+ function isBoolean(v: unknown): v is boolean { return typeof v === 'boolean' }
84
+ function isArray(v: unknown): v is unknown[] { return Array.isArray(v) }
85
+
86
+ function requireField(
87
+ parent: string,
88
+ obj: Record<string, unknown>,
89
+ key: string,
90
+ predicate: (v: unknown) => boolean,
91
+ typeName: string,
92
+ ): ValidationResult {
93
+ if (!(key in obj)) return fail(`${parent}.${key}: missing required field`)
94
+ if (!predicate(obj[key])) return fail(`${parent}.${key}: expected ${typeName}`)
95
+ return ok()
96
+ }
97
+
98
+ function optionalField(
99
+ parent: string,
100
+ obj: Record<string, unknown>,
101
+ key: string,
102
+ predicate: (v: unknown) => boolean,
103
+ typeName: string,
104
+ ): ValidationResult {
105
+ if (!(key in obj) || obj[key] === undefined) return ok()
106
+ if (!predicate(obj[key])) return fail(`${parent}.${key}: expected ${typeName}`)
107
+ return ok()
108
+ }
109
+
110
+ function isOneOf<T extends string>(values: readonly T[]): (v: unknown) => v is T {
111
+ return (v: unknown): v is T => typeof v === 'string' && (values as readonly string[]).includes(v)
112
+ }
113
+
114
+ // -- Common envelope check ---------------------------------------------------
115
+
116
+ const GAP_OBJECT_TYPES: readonly GapObjectType[] = [
117
+ 'gap:capability_declaration',
118
+ 'gap:capability_grant',
119
+ 'gap:capability_invocation',
120
+ 'gap:workflow_definition',
121
+ 'gap:workflow_instance',
122
+ 'gap:stage_transition',
123
+ 'gap:channel_event',
124
+ 'gap:decision_receipt',
125
+ 'gap:revocation_event',
126
+ 'gap:federation_handshake', // reserved for GAP 1.1 - accepted but not required for any conformance tier
127
+ 'gap:break_glass_token',
128
+ 'gap:local_override_credential',
129
+ 'gap:lca_root',
130
+ 'gap:erasure_event',
131
+ // Item 1: Agent Delegation Chain
132
+ 'gap:orchestration_chain',
133
+ // Item 4: Consent Version Chain
134
+ 'gap:consent_record',
135
+ // Item 7: Signed PIP Response
136
+ 'gap:pip_response',
137
+ ]
138
+
139
+ function validateEnvelopeShape(x: unknown, expectedType: GapObjectType): ValidationResult {
140
+ if (!isObject(x)) return fail('envelope: expected object')
141
+ const errors: string[] = []
142
+ if (!isString(x['oid'])) errors.push('envelope.oid: expected string')
143
+ else if (!x['oid'].startsWith('sha256:')) errors.push('envelope.oid: expected "sha256:<hex>" prefix')
144
+ if (x['type'] !== expectedType) errors.push(`envelope.type: expected "${expectedType}"`)
145
+ else if (!(GAP_OBJECT_TYPES as readonly string[]).includes(x['type'] as string)) {
146
+ errors.push('envelope.type: unknown GAP object type')
147
+ }
148
+ if (x['gap_version'] !== GAP_VERSION) {
149
+ errors.push(`envelope.gap_version: expected "${GAP_VERSION}"`)
150
+ }
151
+ if (!isString(x['tenant_id'])) errors.push('envelope.tenant_id: expected string')
152
+ if (!isInteger(x['created_at_ms'])) errors.push('envelope.created_at_ms: expected integer')
153
+ if (!isString(x['created_by'])) errors.push('envelope.created_by: expected string')
154
+ if (!('body' in x) || !isObject(x['body'])) errors.push('envelope.body: expected object')
155
+ if ('signature' in x && x['signature'] !== undefined && !isString(x['signature'])) {
156
+ errors.push('envelope.signature: expected string')
157
+ }
158
+ if ('signature_key_id' in x && x['signature_key_id'] !== undefined && !isString(x['signature_key_id'])) {
159
+ errors.push('envelope.signature_key_id: expected string')
160
+ }
161
+ if ('supersedes' in x && x['supersedes'] !== undefined && !isString(x['supersedes'])) {
162
+ errors.push('envelope.supersedes: expected string')
163
+ }
164
+ return errors.length === 0 ? ok() : fail(...errors)
165
+ }
166
+
167
+ // -- Reusable inner-shape validators -----------------------------------------
168
+
169
+ const ACTOR_TYPES: readonly GapActorType[] = [
170
+ 'skill', 'service', 'device', 'agent', 'mcp_server', 'gateway_subsystem', 'human_user',
171
+ ]
172
+ const isActorType = isOneOf(ACTOR_TYPES)
173
+
174
+ function validatePredicate(parent: string, p: unknown): ValidationResult {
175
+ if (!isObject(p)) return fail(`${parent}: expected object`)
176
+ return merge(
177
+ requireField(parent, p, 'kind', isString, 'string'),
178
+ requireField(parent, p, 'args', isObject, 'object'),
179
+ )
180
+ }
181
+
182
+ function validatePredicateArray(parent: string, arr: unknown): ValidationResult {
183
+ if (!isArray(arr)) return fail(`${parent}: expected array`)
184
+ const merged: ValidationResult[] = []
185
+ arr.forEach((p, i) => merged.push(validatePredicate(`${parent}[${i}]`, p)))
186
+ return merge(...merged)
187
+ }
188
+
189
+ function validateCapability(parent: string, c: unknown): ValidationResult {
190
+ if (!isObject(c)) return fail(`${parent}: expected object`)
191
+ const errors: ValidationResult[] = [
192
+ requireField(parent, c, 'capability', isString, 'string'),
193
+ ]
194
+ if (c['scope'] !== undefined) errors.push(requireField(parent, c, 'scope', isObject, 'object'))
195
+ if (c['preconditions'] !== undefined) errors.push(validatePredicateArray(`${parent}.preconditions`, c['preconditions']))
196
+ if (c['safety_class'] !== undefined) {
197
+ errors.push(requireField(parent, c, 'safety_class', isOneOf(['A', 'B', 'C'] as const), '"A" | "B" | "C"'))
198
+ }
199
+ if (c['physical_safety'] !== undefined) errors.push(requireField(parent, c, 'physical_safety', isBoolean, 'boolean'))
200
+ if (c['require_signed_receipt'] !== undefined) errors.push(requireField(parent, c, 'require_signed_receipt', isBoolean, 'boolean'))
201
+ if (c['pii_args'] !== undefined) {
202
+ if (!isArray(c['pii_args']) || !c['pii_args'].every(isString)) {
203
+ errors.push(fail(`${parent}.pii_args: expected string[]`))
204
+ }
205
+ }
206
+ if (c['privilege_protected'] !== undefined) {
207
+ errors.push(requireField(parent, c, 'privilege_protected', isBoolean, 'boolean'))
208
+ }
209
+ return merge(...errors)
210
+ }
211
+
212
+ function validateScope(parent: string, s: unknown): ValidationResult {
213
+ if (!isObject(s)) return fail(`${parent}: expected object`)
214
+ const errors: ValidationResult[] = [
215
+ requireField(parent, s, 'capability', isString, 'string'),
216
+ ]
217
+ if (s['capability_declaration_oid'] !== undefined) {
218
+ errors.push(requireField(parent, s, 'capability_declaration_oid', isString, 'string'))
219
+ }
220
+ if (s['scope_narrowing'] !== undefined) {
221
+ errors.push(requireField(parent, s, 'scope_narrowing', isObject, 'object'))
222
+ }
223
+ if (s['additional_preconditions'] !== undefined) {
224
+ errors.push(validatePredicateArray(`${parent}.additional_preconditions`, s['additional_preconditions']))
225
+ }
226
+ if (s['require_signed_receipt'] !== undefined) {
227
+ errors.push(requireField(parent, s, 'require_signed_receipt', isBoolean, 'boolean'))
228
+ }
229
+ return merge(...errors)
230
+ }
231
+
232
+ // -- Item 1: Delegation step validator ---------------------------------------
233
+
234
+ function validateDelegationStep(parent: string, s: unknown): ValidationResult {
235
+ if (!isObject(s)) return fail(`${parent}: expected object`)
236
+ return merge(
237
+ requireField(parent, s, 'step_index', (v) => isNumber(v) && Number.isInteger(v) && (v as number) >= 0, 'non-negative integer'),
238
+ requireField(parent, s, 'delegator_actor_oid', isString, 'string'),
239
+ requireField(parent, s, 'delegatee_actor_oid', isString, 'string'),
240
+ requireField(parent, s, 'grant_oid', isString, 'string'),
241
+ requireField(parent, s, 'delegated_at_ms', isInteger, 'integer'),
242
+ requireField(parent, s, 'step_signature', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
243
+ requireField(parent, s, 'step_signature_alg', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
244
+ optionalField(parent, s, 'prior_receipt_oid', isString, 'string'),
245
+ )
246
+ }
247
+
248
+ /**
249
+ * [DESIGN] Validates a gap:orchestration_chain body. Returns error
250
+ * 'delegation_depth_exceeded' when steps.length > 10.
251
+ */
252
+ export function validateOrchestrationChainBody(x: unknown): ValidationResult {
253
+ if (!isObject(x)) return fail('body: expected object')
254
+ const errors: ValidationResult[] = [
255
+ requireField('body', x, 'root_actor_oid', isString, 'string'),
256
+ requireField('body', x, 'capability_name', isString, 'string'),
257
+ requireField('body', x, 'final_invocation_oid', isString, 'string'),
258
+ ]
259
+ if (!isArray(x['steps'])) {
260
+ errors.push(fail('body.steps: expected array'))
261
+ } else {
262
+ if ((x['steps'] as unknown[]).length > 10) {
263
+ errors.push(fail('delegation_depth_exceeded: steps array exceeds maximum of 10 hops'))
264
+ }
265
+ ;(x['steps'] as unknown[]).forEach((s, i) =>
266
+ errors.push(validateDelegationStep(`body.steps[${i}]`, s))
267
+ )
268
+ }
269
+ return merge(...errors)
270
+ }
271
+
272
+ // -- Item 2: MCP tool-call context validator ---------------------------------
273
+
274
+ function validateMcpToolCallContext(parent: string, m: unknown): ValidationResult {
275
+ if (!isObject(m)) return fail(`${parent}: expected object`)
276
+ const errors: ValidationResult[] = [
277
+ requireField(parent, m, 'server_id', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
278
+ requireField(parent, m, 'tool_name', isString, 'string'),
279
+ ]
280
+ if (isString(m['tool_name']) && (m['tool_name'] as string).startsWith('gap:')) {
281
+ errors.push(fail(`${parent}.tool_name: MCP tool names MUST NOT start with 'gap:' (namespace reserved)`))
282
+ }
283
+ if (m['tool_schema_hash'] !== undefined) {
284
+ errors.push(optionalField(parent, m, 'tool_schema_hash', isString, 'string'))
285
+ }
286
+ return merge(...errors)
287
+ }
288
+
289
+ // -- Item 3: Token consumption validator -------------------------------------
290
+
291
+ /**
292
+ * [DESIGN] Validates a TokenConsumption object from a receipt body.
293
+ */
294
+ export function validateTokenConsumption(x: unknown): ValidationResult {
295
+ if (!isObject(x)) return fail('token_consumption: expected object')
296
+ const isNonNegInt = (v: unknown): v is number =>
297
+ isNumber(v) && Number.isInteger(v) && (v as number) >= 0
298
+ return merge(
299
+ requireField('token_consumption', x, 'input_tokens', isNonNegInt, 'non-negative integer'),
300
+ requireField('token_consumption', x, 'output_tokens', isNonNegInt, 'non-negative integer'),
301
+ requireField('token_consumption', x, 'model', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
302
+ requireField('token_consumption', x, 'settled_at_ms', isInteger, 'integer'),
303
+ optionalField('token_consumption', x, 'cost_usd', isNumber, 'number'),
304
+ )
305
+ }
306
+
307
+ // -- Item 4: Consent record validator ----------------------------------------
308
+
309
+ /**
310
+ * [DESIGN] Validates a gap:consent_record body. consented MUST be boolean;
311
+ * actor_oid and context are required.
312
+ */
313
+ export function validateConsentRecordBody(x: unknown): ValidationResult {
314
+ if (!isObject(x)) return fail('body: expected object')
315
+ return merge(
316
+ requireField('body', x, 'actor_oid', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
317
+ requireField('body', x, 'tenant_id', isString, 'string'),
318
+ requireField('body', x, 'context', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
319
+ requireField('body', x, 'consented', isBoolean, 'boolean'),
320
+ requireField('body', x, 'consented_at_ms', isInteger, 'integer'),
321
+ optionalField('body', x, 'prior_consent_oid', isString, 'string'),
322
+ optionalField('body', x, 'expires_at_ms', isInteger, 'integer'),
323
+ optionalField('body', x, 'consent_text_hash', isString, 'string'),
324
+ )
325
+ }
326
+
327
+ // -- Item 5: Identity binding validator --------------------------------------
328
+
329
+ const CREDENTIAL_KINDS: readonly CredentialKind[] = [
330
+ 'piv_cac', 'x509', 'fido2', 'tpm_attestation',
331
+ 'oidc_sub', 'spiffe_svid', 'wallet_address', 'professional_license',
332
+ ]
333
+ const isCredentialKind = isOneOf(CREDENTIAL_KINDS)
334
+
335
+ function validateIdentityBinding(parent: string, b: unknown): ValidationResult {
336
+ if (!isObject(b)) return fail(`${parent}: expected object`)
337
+ return merge(
338
+ requireField(parent, b, 'credential_kind', isCredentialKind,
339
+ '"piv_cac" | "x509" | "fido2" | "tpm_attestation" | "oidc_sub" | "spiffe_svid" | "wallet_address" | "professional_license"'),
340
+ requireField(parent, b, 'credential_identifier', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
341
+ requireField(parent, b, 'binding_signature', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
342
+ requireField(parent, b, 'binding_alg', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
343
+ requireField(parent, b, 'bound_at_ms', isInteger, 'integer'),
344
+ optionalField(parent, b, 'issuer', isString, 'string'),
345
+ optionalField(parent, b, 'expires_at_ms', isInteger, 'integer'),
346
+ )
347
+ }
348
+
349
+ // -- Item 6: Compartment validator -------------------------------------------
350
+
351
+ /**
352
+ * Validates a compartment label. Accepted values: 'UNCLASS', 'CUI', or a
353
+ * reverse-domain label (starts with a letter, contains at least one dot, no
354
+ * spaces, no leading 'gap:').
355
+ */
356
+ function isValidCompartment(v: unknown): v is string {
357
+ if (!isString(v)) return false
358
+ const s = v as string
359
+ if (s === 'UNCLASS' || s === 'CUI') return true
360
+ // Reverse-domain: starts with letter, contains a dot, no spaces
361
+ return /^[a-zA-Z][a-zA-Z0-9_-]*(\.[a-zA-Z0-9_-]+)+$/.test(s)
362
+ }
363
+
364
+ // -- Item 7: PIP response validator ------------------------------------------
365
+
366
+ /**
367
+ * [DESIGN] Validates a gap:pip_response body.
368
+ */
369
+ export function validatePipResponseBody(x: unknown): ValidationResult {
370
+ if (!isObject(x)) return fail('body: expected object')
371
+ return merge(
372
+ requireField('body', x, 'pip_endpoint', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
373
+ requireField('body', x, 'request_args_hash', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
374
+ requireField('body', x, 'response_body_hash', (v) => isString(v) && (v as string).length > 0, 'non-empty string'),
375
+ requireField('body', x, 'evaluated_at_ms', isInteger, 'integer'),
376
+ requireField('body', x, 'cache_ttl_ms', (v) => isInteger(v) && (v as number) >= 0, 'non-negative integer'),
377
+ optionalField('body', x, 'response_summary', isString, 'string'),
378
+ optionalField('body', x, 'pip_signature', isString, 'string'),
379
+ optionalField('body', x, 'pip_signature_alg', isString, 'string'),
380
+ )
381
+ }
382
+
383
+ // -- Body validators ---------------------------------------------------------
384
+
385
+ export function validateCapabilityDeclarationBody(x: unknown): ValidationResult {
386
+ if (!isObject(x)) return fail('body: expected object')
387
+ const errors: ValidationResult[] = [
388
+ requireField('body', x, 'actor_type', isActorType, 'GapActorType'),
389
+ requireField('body', x, 'actor_id', isString, 'string'),
390
+ requireField('body', x, 'actor_name', isString, 'string'),
391
+ requireField('body', x, 'actor_version', isString, 'string'),
392
+ ]
393
+ if (x['source_url'] !== undefined) errors.push(requireField('body', x, 'source_url', isString, 'string'))
394
+ if (x['parent_oid'] !== undefined) errors.push(requireField('body', x, 'parent_oid', isString, 'string'))
395
+ if (!isArray(x['capabilities'])) {
396
+ errors.push(fail('body.capabilities: expected array'))
397
+ } else {
398
+ x['capabilities'].forEach((c, i) => errors.push(validateCapability(`body.capabilities[${i}]`, c)))
399
+ }
400
+ if (x['human_summary'] !== undefined) errors.push(requireField('body', x, 'human_summary', isString, 'string'))
401
+ if (x['privacy_classification'] !== undefined) {
402
+ errors.push(requireField('body', x, 'privacy_classification',
403
+ isOneOf(['public', 'restricted', 'sensitive', 'phi', 'pii', 'financial', 'privileged'] as const),
404
+ '"public" | "restricted" | "sensitive" | "phi" | "pii" | "financial" | "privileged"'))
405
+ }
406
+ if (x['declared_limits'] !== undefined) {
407
+ if (!isObject(x['declared_limits'])) errors.push(fail('body.declared_limits: expected object'))
408
+ }
409
+ // C15: ephemeral actor lifecycle fields
410
+ if (x['actor_lifecycle'] !== undefined) {
411
+ errors.push(requireField('body', x, 'actor_lifecycle',
412
+ isOneOf(['persistent', 'ephemeral'] as const),
413
+ '"persistent" | "ephemeral"'))
414
+ }
415
+ if (x['actor_instance_id'] !== undefined) {
416
+ errors.push(requireField('body', x, 'actor_instance_id', isString, 'string'))
417
+ }
418
+ if (x['session_expires_at_ms'] !== undefined) {
419
+ errors.push(requireField('body', x, 'session_expires_at_ms', isInteger, 'integer'))
420
+ }
421
+ // Item 5: identity_binding
422
+ if (x['identity_binding'] !== undefined) {
423
+ errors.push(validateIdentityBinding('body.identity_binding', x['identity_binding']))
424
+ }
425
+ // Item 6: compartment
426
+ if (x['compartment'] !== undefined) {
427
+ if (!isValidCompartment(x['compartment'])) {
428
+ errors.push(fail('body.compartment: expected "UNCLASS", "CUI", or a reverse-domain label (e.g. "com.acme.project-alpha")'))
429
+ }
430
+ }
431
+ return merge(...errors)
432
+ }
433
+
434
+ export function validateCapabilityGrantBody(x: unknown): ValidationResult {
435
+ if (!isObject(x)) return fail('body: expected object')
436
+ const errors: ValidationResult[] = []
437
+ // grantee
438
+ if (!isObject(x['grantee'])) {
439
+ errors.push(fail('body.grantee: expected object'))
440
+ } else {
441
+ const g = x['grantee']
442
+ errors.push(
443
+ requireField('body.grantee', g, 'actor_type', isActorType, 'GapActorType'),
444
+ requireField('body.grantee', g, 'actor_oid', isString, 'string'),
445
+ optionalField('body.grantee', g, 'actor_session_id', isString, 'string'),
446
+ )
447
+ }
448
+ // capability_scopes
449
+ if (!isArray(x['capability_scopes'])) {
450
+ errors.push(fail('body.capability_scopes: expected array'))
451
+ } else {
452
+ x['capability_scopes'].forEach((s, i) => errors.push(validateScope(`body.capability_scopes[${i}]`, s)))
453
+ }
454
+ errors.push(requireField('body', x, 'granted_at_ms', isInteger, 'integer'))
455
+ // expires_at_ms can be integer | null
456
+ if (!('expires_at_ms' in x)) {
457
+ errors.push(fail('body.expires_at_ms: missing required field'))
458
+ } else if (x['expires_at_ms'] !== null && !isInteger(x['expires_at_ms'])) {
459
+ errors.push(fail('body.expires_at_ms: expected integer | null'))
460
+ }
461
+ errors.push(requireField('body', x, 'granted_by', isString, 'string'))
462
+ if (x['reason'] !== undefined) errors.push(requireField('body', x, 'reason', isString, 'string'))
463
+ if (x['evidence_oids'] !== undefined) {
464
+ if (!isArray(x['evidence_oids']) || !x['evidence_oids'].every(isString)) {
465
+ errors.push(fail('body.evidence_oids: expected string[]'))
466
+ }
467
+ }
468
+ if (x['revocation_level_override'] !== undefined) {
469
+ if (x['revocation_level_override'] !== 1 && x['revocation_level_override'] !== 2 && x['revocation_level_override'] !== 3) {
470
+ errors.push(fail('body.revocation_level_override: expected 1 | 2 | 3'))
471
+ }
472
+ }
473
+ if (x['limits'] !== undefined) {
474
+ if (!isObject(x['limits'])) {
475
+ errors.push(fail('body.limits: expected object'))
476
+ } else {
477
+ const lim = x['limits']
478
+ if (lim['aggregate_limits'] !== undefined) {
479
+ if (!isArray(lim['aggregate_limits'])) {
480
+ errors.push(fail('body.limits.aggregate_limits: expected array'))
481
+ } else {
482
+ lim['aggregate_limits'].forEach((entry: unknown, i: number) => {
483
+ if (!isObject(entry)) {
484
+ errors.push(fail(`body.limits.aggregate_limits[${i}]: expected object`))
485
+ return
486
+ }
487
+ errors.push(
488
+ requireField(`body.limits.aggregate_limits[${i}]`, entry, 'key', isString, 'string'),
489
+ requireField(`body.limits.aggregate_limits[${i}]`, entry, 'max', (v) => isInteger(v) && (v as number) >= 0, 'integer >= 0'),
490
+ requireField(`body.limits.aggregate_limits[${i}]`, entry, 'window_seconds', (v) => isInteger(v) && (v as number) > 0, 'integer > 0'),
491
+ )
492
+ })
493
+ }
494
+ }
495
+ // C7: cross-grant aggregate limit group
496
+ if (lim['aggregate_limit_group'] !== undefined) {
497
+ errors.push(optionalField('body.limits', lim, 'aggregate_limit_group', isString, 'string'))
498
+ }
499
+ }
500
+ }
501
+ if (x['parent_grant_oid'] !== undefined) {
502
+ errors.push(requireField('body', x, 'parent_grant_oid', isString, 'string'))
503
+ }
504
+ if (x['max_delegation_depth'] !== undefined) {
505
+ if (!isNumber(x['max_delegation_depth']) || !Number.isInteger(x['max_delegation_depth']) || (x['max_delegation_depth'] as number) < 0) {
506
+ errors.push(fail('body.max_delegation_depth: expected non-negative integer'))
507
+ }
508
+ }
509
+ const isPositiveInteger = (v: unknown): v is number =>
510
+ isNumber(v) && Number.isInteger(v) && (v as number) > 0
511
+ if (x['timestamp_window_seconds'] !== undefined) {
512
+ if (!isPositiveInteger(x['timestamp_window_seconds'])) {
513
+ errors.push(fail('body.timestamp_window_seconds: expected positive integer'))
514
+ }
515
+ }
516
+ if (x['offline_grace_seconds'] !== undefined) {
517
+ if (!isNumber(x['offline_grace_seconds']) || !Number.isInteger(x['offline_grace_seconds']) || (x['offline_grace_seconds'] as number) < 0) {
518
+ errors.push(fail('body.offline_grace_seconds: expected non-negative integer'))
519
+ }
520
+ }
521
+ if (x['max_grant_offline_ttl_ms'] !== undefined) {
522
+ if (!isPositiveInteger(x['max_grant_offline_ttl_ms'])) {
523
+ errors.push(fail('body.max_grant_offline_ttl_ms: expected positive integer'))
524
+ }
525
+ }
526
+ if (x['max_revocation_bundle_age_ms'] !== undefined) {
527
+ if (!isPositiveInteger(x['max_revocation_bundle_age_ms'])) {
528
+ errors.push(fail('body.max_revocation_bundle_age_ms: expected positive integer'))
529
+ }
530
+ }
531
+ // -- Break-glass optional fields -------------------------------------------
532
+ if (x['break_glass'] !== undefined) {
533
+ errors.push(optionalField('body', x, 'break_glass', isBoolean, 'boolean'))
534
+ }
535
+ if (x['break_glass_ttl_ms'] !== undefined) {
536
+ if (!isPositiveInteger(x['break_glass_ttl_ms'])) {
537
+ errors.push(fail('body.break_glass_ttl_ms: expected positive integer'))
538
+ }
539
+ }
540
+ if (x['break_glass_max_invocations'] !== undefined) {
541
+ if (!isPositiveInteger(x['break_glass_max_invocations'])) {
542
+ errors.push(fail('body.break_glass_max_invocations: expected positive integer'))
543
+ }
544
+ }
545
+ if (x['break_glass_requires_reason'] !== undefined) {
546
+ errors.push(optionalField('body', x, 'break_glass_requires_reason', isBoolean, 'boolean'))
547
+ }
548
+ // Item 6: compartment
549
+ if (x['compartment'] !== undefined) {
550
+ if (!isValidCompartment(x['compartment'])) {
551
+ errors.push(fail('body.compartment: expected "UNCLASS", "CUI", or a reverse-domain label (e.g. "com.acme.project-alpha")'))
552
+ }
553
+ }
554
+ return merge(...errors)
555
+ }
556
+
557
+ export function validateCapabilityInvocationBody(x: unknown): ValidationResult {
558
+ if (!isObject(x)) return fail('body: expected object')
559
+ const errors: ValidationResult[] = []
560
+ if (!isObject(x['caller'])) {
561
+ errors.push(fail('body.caller: expected object'))
562
+ } else {
563
+ const c = x['caller']
564
+ errors.push(
565
+ requireField('body.caller', c, 'actor_type', isActorType, 'GapActorType'),
566
+ requireField('body.caller', c, 'actor_oid', isString, 'string'),
567
+ requireField('body.caller', c, 'grant_oid', isString, 'string'),
568
+ optionalField('body.caller', c, 'actor_session_id', isString, 'string'),
569
+ )
570
+ }
571
+ errors.push(
572
+ requireField('body', x, 'capability', isString, 'string'),
573
+ optionalField('body', x, 'capability_declaration_oid', isString, 'string'),
574
+ requireField('body', x, 'args', isObject, 'object'),
575
+ optionalField('body', x, 'invoked_at_ms', isInteger, 'integer'),
576
+ )
577
+ if (x['workflow_context'] !== undefined) {
578
+ if (!isObject(x['workflow_context'])) {
579
+ errors.push(fail('body.workflow_context: expected object'))
580
+ } else {
581
+ const wc = x['workflow_context']
582
+ errors.push(
583
+ requireField('body.workflow_context', wc, 'workflow_instance_oid', isString, 'string'),
584
+ requireField('body.workflow_context', wc, 'stage_id', isString, 'string'),
585
+ )
586
+ }
587
+ }
588
+ if (x['sla_hint'] !== undefined && !isObject(x['sla_hint'])) {
589
+ errors.push(fail('body.sla_hint: expected object'))
590
+ }
591
+ if (x['idempotency_key'] !== undefined) {
592
+ errors.push(requireField('body', x, 'idempotency_key', isString, 'string'))
593
+ }
594
+ if (x['client_event_ms'] !== undefined) {
595
+ errors.push(optionalField('body', x, 'client_event_ms', isInteger, 'integer'))
596
+ }
597
+ if (x['queued_at_ms'] !== undefined) {
598
+ errors.push(optionalField('body', x, 'queued_at_ms', isInteger, 'integer'))
599
+ }
600
+ // Item 1: delegation_chain -- max 10 steps
601
+ if (x['delegation_chain'] !== undefined) {
602
+ if (!isArray(x['delegation_chain'])) {
603
+ errors.push(fail('body.delegation_chain: expected array'))
604
+ } else {
605
+ if ((x['delegation_chain'] as unknown[]).length > 10) {
606
+ errors.push(fail('delegation_depth_exceeded: delegation_chain exceeds maximum of 10 hops'))
607
+ }
608
+ ;(x['delegation_chain'] as unknown[]).forEach((s, i) =>
609
+ errors.push(validateDelegationStep(`body.delegation_chain[${i}]`, s))
610
+ )
611
+ }
612
+ }
613
+ // Item 2: mcp_tool_call
614
+ if (x['mcp_tool_call'] !== undefined) {
615
+ errors.push(validateMcpToolCallContext('body.mcp_tool_call', x['mcp_tool_call']))
616
+ }
617
+ // Item 6: compartment
618
+ if (x['compartment'] !== undefined) {
619
+ if (!isValidCompartment(x['compartment'])) {
620
+ errors.push(fail('body.compartment: expected "UNCLASS", "CUI", or a reverse-domain label (e.g. "com.acme.project-alpha")'))
621
+ }
622
+ }
623
+ return merge(...errors)
624
+ }
625
+
626
+ export function validateWorkflowDefinitionBody(x: unknown): ValidationResult {
627
+ if (!isObject(x)) return fail('body: expected object')
628
+ const errors: ValidationResult[] = [
629
+ requireField('body', x, 'workflow_id', isString, 'string'),
630
+ requireField('body', x, 'workflow_name', isString, 'string'),
631
+ requireField('body', x, 'workflow_version', isString, 'string'),
632
+ requireField('body', x, 'initial_stage_id', isString, 'string'),
633
+ requireField('body', x, 'max_total_duration_seconds', isInteger, 'integer'),
634
+ ]
635
+ if (!isObject(x['trigger'])) {
636
+ errors.push(fail('body.trigger: expected object'))
637
+ } else {
638
+ errors.push(requireField('body.trigger', x['trigger'], 'kind',
639
+ isOneOf(['risk_policy', 'capability_invocation', 'explicit', 'schedule'] as const),
640
+ 'WorkflowTriggerKind'))
641
+ }
642
+ if (!isArray(x['stages'])) {
643
+ errors.push(fail('body.stages: expected array'))
644
+ } else {
645
+ x['stages'].forEach((s, i) => {
646
+ if (!isObject(s)) {
647
+ errors.push(fail(`body.stages[${i}]: expected object`))
648
+ return
649
+ }
650
+ errors.push(requireField(`body.stages[${i}]`, s, 'stage_id', isString, 'string'))
651
+ if (s['authorized_approvers'] !== undefined) {
652
+ if (!isArray(s['authorized_approvers']) || !s['authorized_approvers'].every(isString)) {
653
+ errors.push(fail(`body.stages[${i}].authorized_approvers: expected string[]`))
654
+ }
655
+ }
656
+ })
657
+ }
658
+ if (!isArray(x['required_channels']) || !x['required_channels'].every(isString)) {
659
+ errors.push(fail('body.required_channels: expected string[]'))
660
+ }
661
+ if (x['optional_channels'] !== undefined) {
662
+ if (!isArray(x['optional_channels']) || !x['optional_channels'].every(isString)) {
663
+ errors.push(fail('body.optional_channels: expected string[]'))
664
+ }
665
+ }
666
+ if (x['description'] !== undefined) errors.push(requireField('body', x, 'description', isString, 'string'))
667
+ if (x['cleanup_stage_id'] !== undefined) errors.push(requireField('body', x, 'cleanup_stage_id', isString, 'string'))
668
+ return merge(...errors)
669
+ }
670
+
671
+ export function validateWorkflowInstanceBody(x: unknown): ValidationResult {
672
+ if (!isObject(x)) return fail('body: expected object')
673
+ const errors: ValidationResult[] = [
674
+ requireField('body', x, 'workflow_definition_oid', isString, 'string'),
675
+ requireField('body', x, 'workflow_id', isString, 'string'),
676
+ requireField('body', x, 'current_stage_id', isString, 'string'),
677
+ requireField('body', x, 'scope_variables', isObject, 'object'),
678
+ requireField('body', x, 'started_at_ms', isInteger, 'integer'),
679
+ requireField('body', x, 'last_transition_at_ms', isInteger, 'integer'),
680
+ ]
681
+ if (!('terminated_at_ms' in x)) {
682
+ errors.push(fail('body.terminated_at_ms: missing required field'))
683
+ } else if (x['terminated_at_ms'] !== null && !isInteger(x['terminated_at_ms'])) {
684
+ errors.push(fail('body.terminated_at_ms: expected integer | null'))
685
+ }
686
+ if (!('terminal_outcome' in x)) {
687
+ errors.push(fail('body.terminal_outcome: missing required field'))
688
+ } else if (x['terminal_outcome'] !== null) {
689
+ if (!isString(x['terminal_outcome'])
690
+ || !['approved', 'denied', 'timed_out', 'withdrawn', 'error'].includes(x['terminal_outcome'])) {
691
+ errors.push(fail('body.terminal_outcome: expected null | "approved" | "denied" | "timed_out" | "withdrawn" | "error"'))
692
+ }
693
+ }
694
+ if (!isObject(x['trigger_event'])) {
695
+ errors.push(fail('body.trigger_event: expected object'))
696
+ } else {
697
+ errors.push(
698
+ requireField('body.trigger_event', x['trigger_event'], 'kind',
699
+ isOneOf(['risk_policy', 'capability_invocation', 'explicit', 'schedule'] as const),
700
+ 'WorkflowTriggerKind'),
701
+ requireField('body.trigger_event', x['trigger_event'], 'source_actor_oid', isString, 'string'),
702
+ )
703
+ }
704
+ if (!isArray(x['active_channel_listeners'])) {
705
+ errors.push(fail('body.active_channel_listeners: expected array'))
706
+ }
707
+ if (!isArray(x['transition_oids']) || !x['transition_oids'].every(isString)) {
708
+ errors.push(fail('body.transition_oids: expected string[]'))
709
+ }
710
+ return merge(...errors)
711
+ }
712
+
713
+ export function validateStageTransitionBody(x: unknown): ValidationResult {
714
+ if (!isObject(x)) return fail('body: expected object')
715
+ const errors: ValidationResult[] = [
716
+ requireField('body', x, 'workflow_instance_oid', isString, 'string'),
717
+ requireField('body', x, 'from_stage_id', isString, 'string'),
718
+ requireField('body', x, 'to_stage_id', isString, 'string'),
719
+ requireField('body', x, 'trigger_reason',
720
+ isOneOf([
721
+ 'listen_matched', 'timeout', 'action_completed', 'action_failed',
722
+ 'precondition_passed', 'precondition_failed',
723
+ 'invocation_succeeded', 'invocation_failed',
724
+ 'external_signal', 'cleanup',
725
+ ] as const),
726
+ 'StageTransitionReason'),
727
+ requireField('body', x, 'bind_outputs', isObject, 'object'),
728
+ requireField('body', x, 'transitioned_at_ms', isInteger, 'integer'),
729
+ ]
730
+ if (!('previous_transition_oid' in x)) {
731
+ errors.push(fail('body.previous_transition_oid: missing required field'))
732
+ } else if (x['previous_transition_oid'] !== null && !isString(x['previous_transition_oid'])) {
733
+ errors.push(fail('body.previous_transition_oid: expected string | null'))
734
+ }
735
+ if (x['triggering_event_oid'] !== undefined) {
736
+ errors.push(requireField('body', x, 'triggering_event_oid', isString, 'string'))
737
+ }
738
+ if (x['triggering_invocation_oid'] !== undefined) {
739
+ errors.push(requireField('body', x, 'triggering_invocation_oid', isString, 'string'))
740
+ }
741
+ return merge(...errors)
742
+ }
743
+
744
+ export function validateChannelEventBody(x: unknown): ValidationResult {
745
+ if (!isObject(x)) return fail('body: expected object')
746
+ const errors: ValidationResult[] = [
747
+ requireField('body', x, 'channel', isString, 'string'),
748
+ requireField('body', x, 'event_kind', isString, 'string'),
749
+ requireField('body', x, 'payload', isObject, 'object'),
750
+ requireField('body', x, 'observed_at_ms', isInteger, 'integer'),
751
+ ]
752
+ if (x['workflow_instance_oid'] !== undefined) {
753
+ errors.push(requireField('body', x, 'workflow_instance_oid', isString, 'string'))
754
+ }
755
+ if (x['stage_id'] !== undefined) {
756
+ errors.push(requireField('body', x, 'stage_id', isString, 'string'))
757
+ }
758
+ return merge(...errors)
759
+ }
760
+
761
+ export function validateGapDecisionReceiptBody(x: unknown): ValidationResult {
762
+ if (!isObject(x)) return fail('body: expected object')
763
+ const errors: ValidationResult[] = [
764
+ requireField('body', x, 'subject_kind',
765
+ isOneOf([
766
+ 'capability_invocation', 'stage_transition', 'grant_issued', 'grant_revoked',
767
+ 'workflow_started', 'workflow_terminated', 'revocation_initiated',
768
+ 'revocation_effective', 'federation_handshake', /* reserved for GAP 1.1 */ 'provisional_block',
769
+ ] as const),
770
+ 'DecisionSubjectKind'),
771
+ requireField('body', x, 'subject_oid', isString, 'string'),
772
+ requireField('body', x, 'status',
773
+ isOneOf(['ok', 'denied', 'failed', 'deferred', 'timed_out', 'pending'] as const),
774
+ 'DecisionStatus'),
775
+ requireField('body', x, 'initiated_at_ms', isInteger, 'integer'),
776
+ requireField('body', x, 'resolved_at_ms', isInteger, 'integer'),
777
+ ]
778
+ if (!isObject(x['initiator'])) {
779
+ errors.push(fail('body.initiator: expected object'))
780
+ } else {
781
+ errors.push(
782
+ requireField('body.initiator', x['initiator'], 'actor_oid', isString, 'string'),
783
+ requireField('body.initiator', x['initiator'], 'actor_type', isActorType, 'GapActorType'),
784
+ )
785
+ }
786
+ if (x['detail'] !== undefined) errors.push(requireField('body', x, 'detail', isString, 'string'))
787
+ if (x['capability_grant_oids'] !== undefined) {
788
+ if (!isArray(x['capability_grant_oids']) || !x['capability_grant_oids'].every(isString)) {
789
+ errors.push(fail('body.capability_grant_oids: expected string[]'))
790
+ }
791
+ }
792
+ if (x['channel_event_oids'] !== undefined) {
793
+ if (!isArray(x['channel_event_oids']) || !x['channel_event_oids'].every(isString)) {
794
+ errors.push(fail('body.channel_event_oids: expected string[]'))
795
+ }
796
+ }
797
+ if (x['compliance_tags'] !== undefined) {
798
+ if (!isArray(x['compliance_tags']) || !x['compliance_tags'].every(isString)) {
799
+ errors.push(fail('body.compliance_tags: expected string[]'))
800
+ }
801
+ }
802
+ if (x['signer_identity'] !== undefined) {
803
+ if (!isObject(x['signer_identity'])) {
804
+ errors.push(fail('body.signer_identity: expected object'))
805
+ } else {
806
+ const si = x['signer_identity']
807
+ errors.push(requireField('body.signer_identity', si, 'display_name', isString, 'string'))
808
+ errors.push(optionalField('body.signer_identity', si, 'role', isString, 'string'))
809
+ errors.push(optionalField('body.signer_identity', si, 'credential_id', isString, 'string'))
810
+ }
811
+ }
812
+ // C8: sub-millisecond sequence fields
813
+ if (x['sequence_number'] !== undefined) {
814
+ errors.push(optionalField('body', x, 'sequence_number',
815
+ (v): v is number => isNumber(v) && Number.isInteger(v) && (v as number) >= 0,
816
+ 'non-negative integer'))
817
+ }
818
+ if (x['decided_at_ns'] !== undefined) {
819
+ errors.push(optionalField('body', x, 'decided_at_ns', isInteger, 'integer'))
820
+ }
821
+ // Item 3: token_consumption
822
+ if (x['token_consumption'] !== undefined) {
823
+ errors.push(validateTokenConsumption(x['token_consumption']))
824
+ }
825
+ return merge(...errors)
826
+ }
827
+
828
+ export function validateRevocationEventBody(x: unknown): ValidationResult {
829
+ if (!isObject(x)) return fail('body: expected object')
830
+ const errors: ValidationResult[] = [
831
+ requireField('body', x, 'target_kind',
832
+ isOneOf([
833
+ 'capability_declaration', 'capability_grant',
834
+ 'workflow_definition', 'workflow_instance', 'skill',
835
+ ] as const),
836
+ 'RevocationTargetKind'),
837
+ requireField('body', x, 'target_oid', isString, 'string'),
838
+ requireField('body', x, 'reason', isString, 'string'),
839
+ requireField('body', x, 'provisional', isBoolean, 'boolean'),
840
+ ]
841
+ if (x['required_level'] !== 1 && x['required_level'] !== 2 && x['required_level'] !== 3) {
842
+ errors.push(fail('body.required_level: expected 1 | 2 | 3'))
843
+ }
844
+ if (!isArray(x['approvers'])) {
845
+ errors.push(fail('body.approvers: expected array'))
846
+ } else {
847
+ x['approvers'].forEach((a, i) => {
848
+ if (!isObject(a)) {
849
+ errors.push(fail(`body.approvers[${i}]: expected object`))
850
+ return
851
+ }
852
+ errors.push(
853
+ requireField(`body.approvers[${i}]`, a, 'actor_oid', isString, 'string'),
854
+ requireField(`body.approvers[${i}]`, a, 'approved_at_ms', isInteger, 'integer'),
855
+ requireField(`body.approvers[${i}]`, a, 'cooling_off_satisfied', isBoolean, 'boolean'),
856
+ )
857
+ })
858
+ }
859
+ if (!('effective_at_ms' in x)) {
860
+ errors.push(fail('body.effective_at_ms: missing required field'))
861
+ } else if (x['effective_at_ms'] !== null && !isNumber(x['effective_at_ms'])) {
862
+ errors.push(fail('body.effective_at_ms: expected number | null'))
863
+ }
864
+ if (x['evidence_oids'] !== undefined) {
865
+ if (!isArray(x['evidence_oids']) || !x['evidence_oids'].every(isString)) {
866
+ errors.push(fail('body.evidence_oids: expected string[]'))
867
+ }
868
+ }
869
+ if (x['provisional_block_policy'] !== undefined) {
870
+ if (!isObject(x['provisional_block_policy'])) {
871
+ errors.push(fail('body.provisional_block_policy: expected object'))
872
+ } else {
873
+ const pbp = x['provisional_block_policy']
874
+ if (pbp['on_expiry_without_quorum'] !== 'renew' && pbp['on_expiry_without_quorum'] !== 'revert') {
875
+ errors.push(fail('body.provisional_block_policy.on_expiry_without_quorum: expected "renew" | "revert"'))
876
+ }
877
+ // M-5: optional TTL override; minimum 1 hour
878
+ if (pbp['provisional_block_ttl_ms'] !== undefined) {
879
+ const minTtl = 3_600_000
880
+ if (!isInteger(pbp['provisional_block_ttl_ms']) || (pbp['provisional_block_ttl_ms'] as number) < minTtl) {
881
+ errors.push(fail('body.provisional_block_policy.provisional_block_ttl_ms: expected integer >= 3600000 (1 hour)'))
882
+ }
883
+ }
884
+ }
885
+ }
886
+ if (x['min_approvers'] !== undefined) {
887
+ const isPositiveInteger = (v: unknown): v is number =>
888
+ isNumber(v) && Number.isInteger(v) && (v as number) > 0
889
+ if (!isPositiveInteger(x['min_approvers'])) {
890
+ errors.push(fail('body.min_approvers: expected positive integer'))
891
+ }
892
+ }
893
+ return merge(...errors)
894
+ }
895
+
896
+ // -- Full-envelope validators (envelope + body) ------------------------------
897
+
898
+ export function validateCapabilityDeclaration(x: unknown): ValidationResult {
899
+ const env = validateEnvelopeShape(x, 'gap:capability_declaration')
900
+ if (!env.ok) return env
901
+ return merge(env, validateCapabilityDeclarationBody((x as GapCdroEnvelope<unknown>).body))
902
+ }
903
+
904
+ export function validateCapabilityGrant(x: unknown): ValidationResult {
905
+ const env = validateEnvelopeShape(x, 'gap:capability_grant')
906
+ if (!env.ok) return env
907
+ return merge(env, validateCapabilityGrantBody((x as GapCdroEnvelope<unknown>).body))
908
+ }
909
+
910
+ export function validateCapabilityInvocation(x: unknown): ValidationResult {
911
+ const env = validateEnvelopeShape(x, 'gap:capability_invocation')
912
+ if (!env.ok) return env
913
+ return merge(env, validateCapabilityInvocationBody((x as GapCdroEnvelope<unknown>).body))
914
+ }
915
+
916
+ export function validateWorkflowDefinition(x: unknown): ValidationResult {
917
+ const env = validateEnvelopeShape(x, 'gap:workflow_definition')
918
+ if (!env.ok) return env
919
+ return merge(env, validateWorkflowDefinitionBody((x as GapCdroEnvelope<unknown>).body))
920
+ }
921
+
922
+ export function validateWorkflowInstance(x: unknown): ValidationResult {
923
+ const env = validateEnvelopeShape(x, 'gap:workflow_instance')
924
+ if (!env.ok) return env
925
+ return merge(env, validateWorkflowInstanceBody((x as GapCdroEnvelope<unknown>).body))
926
+ }
927
+
928
+ export function validateStageTransition(x: unknown): ValidationResult {
929
+ const env = validateEnvelopeShape(x, 'gap:stage_transition')
930
+ if (!env.ok) return env
931
+ return merge(env, validateStageTransitionBody((x as GapCdroEnvelope<unknown>).body))
932
+ }
933
+
934
+ export function validateChannelEvent(x: unknown): ValidationResult {
935
+ const env = validateEnvelopeShape(x, 'gap:channel_event')
936
+ if (!env.ok) return env
937
+ return merge(env, validateChannelEventBody((x as GapCdroEnvelope<unknown>).body))
938
+ }
939
+
940
+ export function validateGapDecisionReceipt(x: unknown): ValidationResult {
941
+ const env = validateEnvelopeShape(x, 'gap:decision_receipt')
942
+ if (!env.ok) return env
943
+ return merge(env, validateGapDecisionReceiptBody((x as GapCdroEnvelope<unknown>).body))
944
+ }
945
+
946
+ export function validateRevocationEvent(x: unknown): ValidationResult {
947
+ const env = validateEnvelopeShape(x, 'gap:revocation_event')
948
+ if (!env.ok) return env
949
+ return merge(env, validateRevocationEventBody((x as GapCdroEnvelope<unknown>).body))
950
+ }
951
+
952
+ // -- Full-envelope validators for new CDRO types (Items 1, 4, 7) -------------
953
+
954
+ export function validateOrchestrationChain(x: unknown): ValidationResult {
955
+ const env = validateEnvelopeShape(x, 'gap:orchestration_chain')
956
+ if (!env.ok) return env
957
+ return merge(env, validateOrchestrationChainBody((x as GapCdroEnvelope<unknown>).body))
958
+ }
959
+
960
+ export function validateConsentRecord(x: unknown): ValidationResult {
961
+ const env = validateEnvelopeShape(x, 'gap:consent_record')
962
+ if (!env.ok) return env
963
+ return merge(env, validateConsentRecordBody((x as GapCdroEnvelope<unknown>).body))
964
+ }
965
+
966
+ export function validatePipResponse(x: unknown): ValidationResult {
967
+ const env = validateEnvelopeShape(x, 'gap:pip_response')
968
+ if (!env.ok) return env
969
+ return merge(env, validatePipResponseBody((x as GapCdroEnvelope<unknown>).body))
970
+ }
971
+
972
+ // -- Note on imports ---------------------------------------------------------
973
+ // The CapabilityDeclaration / CapabilityGrant / ... type imports above are
974
+ // used only at the .d.ts level (validators take `unknown`); the underscore
975
+ // guards below silence "unused" lint without changing emit.
976
+ type _Unused =
977
+ | CapabilityDeclaration
978
+ | CapabilityDeclarationBody
979
+ | CapabilityGrant
980
+ | CapabilityGrantBody
981
+ | CapabilityInvocation
982
+ | CapabilityInvocationBody
983
+ | WorkflowDefinition
984
+ | WorkflowDefinitionBody
985
+ | WorkflowInstance
986
+ | WorkflowInstanceBody
987
+ | StageTransition
988
+ | StageTransitionBody
989
+ | ChannelEvent
990
+ | ChannelEventBody
991
+ | GapDecisionReceipt
992
+ | GapDecisionReceiptBody
993
+ | RevocationEvent
994
+ | RevocationEventBody
995
+ | Capability
996
+ | CapabilityPredicate
997
+ | GrantedCapabilityScope
998
+ // New types from Items 1-7
999
+ | ConsentRecordBody
1000
+ | CredentialKind
1001
+ | DelegationStep
1002
+ | IdentityBinding
1003
+ | McpToolCallContext
1004
+ | OrchestrationChainBody
1005
+ | PipResponseBody
1006
+ | TokenBudgetArgs
1007
+ | ExternalPipArgs
1008
+ | TokenConsumption