@brainai/satp-client 2.0.6 → 2.0.8

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.
@@ -8,8 +8,20 @@ const DECISIONS = Object.freeze({
8
8
  });
9
9
 
10
10
  const REASON_CODES = Object.freeze({
11
+ ACTOR_EVIDENCE_ACTION_UNBOUND: 'ACTOR_EVIDENCE_ACTION_UNBOUND',
12
+ ACTOR_EVIDENCE_FRESH: 'ACTOR_EVIDENCE_FRESH',
13
+ ACTOR_EVIDENCE_MISSING: 'ACTOR_EVIDENCE_MISSING',
14
+ ACTOR_EVIDENCE_REVOKED: 'ACTOR_EVIDENCE_REVOKED',
15
+ ACTOR_EVIDENCE_STALE: 'ACTOR_EVIDENCE_STALE',
16
+ ACTOR_EVIDENCE_UNVERIFIED: 'ACTOR_EVIDENCE_UNVERIFIED',
17
+ ACTOR_EVIDENCE_VERIFIED: 'ACTOR_EVIDENCE_VERIFIED',
18
+ ACTOR_ID_MISSING: 'ACTOR_ID_MISSING',
19
+ ACTOR_SUBJECT_MISMATCH: 'ACTOR_SUBJECT_MISMATCH',
11
20
  ACTION_PAYMENT_NEEDS_APPROVAL: 'ACTION_PAYMENT_NEEDS_APPROVAL',
12
21
  ACTION_PAYMENT_PREAPPROVED: 'ACTION_PAYMENT_PREAPPROVED',
22
+ ACTION_CONTEXT_MISMATCH: 'ACTION_CONTEXT_MISMATCH',
23
+ DELEGATION_CONTEXT_MISSING: 'DELEGATION_CONTEXT_MISSING',
24
+ DELEGATION_DEPTH_EXCEEDED: 'DELEGATION_DEPTH_EXCEEDED',
13
25
  EVIDENCE_FRESH: 'EVIDENCE_FRESH',
14
26
  EVIDENCE_STALE_OR_MISSING: 'EVIDENCE_STALE_OR_MISSING',
15
27
  IDENTITY_INACTIVE: 'IDENTITY_INACTIVE',
@@ -18,12 +30,18 @@ const REASON_CODES = Object.freeze({
18
30
  LOCAL_POLICY_ALLOW: 'LOCAL_POLICY_ALLOW',
19
31
  MISSING_CAPABILITY: 'MISSING_CAPABILITY',
20
32
  PROTECTED_TOOL_REQUIRES_APPROVAL: 'PROTECTED_TOOL_REQUIRES_APPROVAL',
33
+ SUBJECT_ID_MISSING: 'SUBJECT_ID_MISSING',
21
34
  TRUST_SCORE_BELOW_DENY_FLOOR: 'TRUST_SCORE_BELOW_DENY_FLOOR',
22
35
  TRUST_SCORE_BELOW_MINIMUM: 'TRUST_SCORE_BELOW_MINIMUM',
23
36
  TRUST_SCORE_OK: 'TRUST_SCORE_OK',
24
37
  X402_LOOKUP_PAYMENT_PREAPPROVED: 'X402_LOOKUP_PAYMENT_PREAPPROVED',
25
38
  X402_LOOKUP_REQUIRES_APPROVAL: 'X402_LOOKUP_REQUIRES_APPROVAL',
39
+ X402_LOOKUP_SETTLEMENT_BOUND: 'X402_LOOKUP_SETTLEMENT_BOUND',
26
40
  X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION: 'X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION',
41
+ X402_SETTLEMENT_CONTEXT_MISMATCH: 'X402_SETTLEMENT_CONTEXT_MISMATCH',
42
+ X402_SETTLEMENT_IS_NOT_TASK_OUTCOME_PROOF: 'X402_SETTLEMENT_IS_NOT_TASK_OUTCOME_PROOF',
43
+ X402_SETTLEMENT_UNVERIFIED: 'X402_SETTLEMENT_UNVERIFIED',
44
+ X402_SETTLEMENT_VERIFIED: 'X402_SETTLEMENT_VERIFIED',
27
45
  });
28
46
 
29
47
  const DEFAULT_POLICY = Object.freeze({
@@ -32,6 +50,9 @@ const DEFAULT_POLICY = Object.freeze({
32
50
  maxAutoSpendUsd: 0,
33
51
  requireVerifiedIdentity: true,
34
52
  staleEvidenceAfterMs: 7 * 24 * 60 * 60 * 1000,
53
+ maxActorEvidenceAgeMs: 15 * 60 * 1000,
54
+ maxDelegationDepth: 1,
55
+ maxSettlementAgeMs: 15 * 60 * 1000,
35
56
  });
36
57
 
37
58
  const RUNTIME_POLICY_AUDIT_TRACE_SCHEMA_VERSION = 'satp.runtimePolicyAuditTrace.v1';
@@ -99,6 +120,11 @@ function evaluateRuntimePolicy(identityPayload, actionDescriptor, options = {})
99
120
  const reasonCodes = [];
100
121
  const checks = {};
101
122
 
123
+ if (identity.explicitActorContext) {
124
+ const actorDecision = evaluateActorEvidence(identity, action, policy, now, reasonCodes, checks);
125
+ if (actorDecision) return actorDecision;
126
+ }
127
+
102
128
  checks.identityActive = identity.active === true;
103
129
  if (!checks.identityActive) {
104
130
  reasonCodes.push(REASON_CODES.IDENTITY_INACTIVE);
@@ -136,7 +162,7 @@ function evaluateRuntimePolicy(identityPayload, actionDescriptor, options = {})
136
162
  checks.evidenceFresh = isEvidenceFresh(identity, policy, now);
137
163
  if (action.requiresFreshEvidence && !checks.evidenceFresh) {
138
164
  reasonCodes.push(REASON_CODES.EVIDENCE_STALE_OR_MISSING);
139
- return staleEvidenceDecision(action, options, reasonCodes, checks);
165
+ return staleEvidenceDecision(identity, action, options, policy, now, reasonCodes, checks);
140
166
  }
141
167
  if (action.requiresFreshEvidence) reasonCodes.push(REASON_CODES.EVIDENCE_FRESH);
142
168
 
@@ -148,11 +174,36 @@ function evaluateRuntimePolicy(identityPayload, actionDescriptor, options = {})
148
174
  }
149
175
 
150
176
  checks.maxAutoSpendUsd = policy.maxAutoSpendUsd;
151
- if (checks.actionCostUsd > checks.maxAutoSpendUsd && options.actionPaymentPreapproved !== true) {
177
+ const settlement = getX402Settlement(options);
178
+ let settlementAllowsPayment = false;
179
+ if (settlement && action.costUsd > 0) {
180
+ const settlementResult = validateX402Settlement({
181
+ settlement,
182
+ identity,
183
+ action,
184
+ purpose: 'action_payment',
185
+ expectedResource: action.resource,
186
+ expectedCostUsd: action.costUsd,
187
+ now,
188
+ policy,
189
+ });
190
+ checks.x402Settlement = settlementResult.check;
191
+ if (!settlementResult.ok) {
192
+ reasonCodes.push(settlementResult.reasonCode);
193
+ return decision(DECISIONS.DENY, reasonCodes, checks, settlementResult.message);
194
+ }
195
+ settlementAllowsPayment = true;
196
+ reasonCodes.push(REASON_CODES.X402_SETTLEMENT_VERIFIED);
197
+ reasonCodes.push(REASON_CODES.X402_SETTLEMENT_IS_NOT_TASK_OUTCOME_PROOF);
198
+ reasonCodes.push(REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION);
199
+ }
200
+ if (checks.actionCostUsd > checks.maxAutoSpendUsd
201
+ && options.actionPaymentPreapproved !== true
202
+ && !settlementAllowsPayment) {
152
203
  reasonCodes.push(REASON_CODES.ACTION_PAYMENT_NEEDS_APPROVAL);
153
204
  return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, 'Paid action exceeds local auto-spend policy.');
154
205
  }
155
- if (checks.actionCostUsd > 0) {
206
+ if (checks.actionCostUsd > 0 && !settlementAllowsPayment) {
156
207
  reasonCodes.push(REASON_CODES.ACTION_PAYMENT_PREAPPROVED);
157
208
  reasonCodes.push(REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION);
158
209
  }
@@ -180,6 +231,9 @@ function buildRuntimePolicyAuditTrace(identityPayload, actionDescriptor, options
180
231
  message: result.message,
181
232
  subject: {
182
233
  agentId: identity.agentId,
234
+ subjectId: identity.subjectId,
235
+ actorId: identity.actorId,
236
+ actorEvidencePresent: identity.actorEvidence !== null,
183
237
  active: identity.active,
184
238
  verified: identity.verified,
185
239
  trustScoreBand: trustScoreBand(identity.trustScore),
@@ -230,6 +284,7 @@ function buildRuntimePolicyActionDescriptor(input = {}, overrides = {}) {
230
284
  const type = action.type || action.surface || 'generic';
231
285
  const descriptor = {
232
286
  schemaVersion: RUNTIME_POLICY_HOST_ACTION_DESCRIPTOR_SCHEMA_VERSION,
287
+ actionId: firstDefined(action.action_id, action.actionId, null),
233
288
  type,
234
289
  resource: firstDefined(action.resource, defaultResourceForAction(type, action)),
235
290
  operation: firstDefined(action.operation, defaultOperationForAction(type)),
@@ -254,7 +309,210 @@ function buildRuntimePolicyActionDescriptor(input = {}, overrides = {}) {
254
309
  return descriptor;
255
310
  }
256
311
 
257
- function staleEvidenceDecision(action, options, reasonCodes, checks) {
312
+ function evaluateActorEvidence(identity, action, policy, now, reasonCodes, checks) {
313
+ checks.subjectIdPresent = isNonEmptyString(identity.subjectId);
314
+ if (!checks.subjectIdPresent) {
315
+ reasonCodes.push(REASON_CODES.SUBJECT_ID_MISSING);
316
+ return decision(DECISIONS.DENY, reasonCodes, checks, 'Explicit runtime context requires subject_id.');
317
+ }
318
+
319
+ checks.actorIdPresent = isNonEmptyString(identity.actorId);
320
+ if (!checks.actorIdPresent) {
321
+ reasonCodes.push(REASON_CODES.ACTOR_ID_MISSING);
322
+ return decision(DECISIONS.DENY, reasonCodes, checks, 'Explicit runtime context requires actor_id.');
323
+ }
324
+
325
+ const evidence = normalizeActorEvidence(identity.actorEvidence);
326
+ checks.actorEvidencePresent = evidence !== null;
327
+ if (!evidence) {
328
+ reasonCodes.push(REASON_CODES.ACTOR_EVIDENCE_MISSING);
329
+ return decision(
330
+ DECISIONS.NEEDS_APPROVAL,
331
+ reasonCodes,
332
+ checks,
333
+ 'Caller-supplied actor context is not authentication; verifier-produced actor_evidence is required.'
334
+ );
335
+ }
336
+
337
+ checks.actorEvidenceRevoked = evidence.revoked;
338
+ if (evidence.revoked) {
339
+ reasonCodes.push(REASON_CODES.ACTOR_EVIDENCE_REVOKED);
340
+ return decision(DECISIONS.DENY, reasonCodes, checks, 'Actor evidence has been revoked.');
341
+ }
342
+
343
+ checks.actorEvidenceVerified = evidence.verified && isNonEmptyString(evidence.verifierId);
344
+ if (!checks.actorEvidenceVerified) {
345
+ reasonCodes.push(REASON_CODES.ACTOR_EVIDENCE_UNVERIFIED);
346
+ return decision(DECISIONS.DENY, reasonCodes, checks, 'Actor evidence was not produced and verified by a named verifier.');
347
+ }
348
+ reasonCodes.push(REASON_CODES.ACTOR_EVIDENCE_VERIFIED);
349
+
350
+ if (evidence.actorId !== identity.actorId || evidence.subjectId !== identity.subjectId) {
351
+ reasonCodes.push(REASON_CODES.ACTOR_SUBJECT_MISMATCH);
352
+ return decision(DECISIONS.DENY, reasonCodes, checks, 'Actor evidence is not bound to the requested actor and subject.');
353
+ }
354
+
355
+ const evidenceFreshness = actorEvidenceFreshness(evidence, now, policy.maxActorEvidenceAgeMs);
356
+ checks.actorEvidenceFresh = evidenceFreshness.fresh;
357
+ if (!evidenceFreshness.fresh) {
358
+ reasonCodes.push(REASON_CODES.ACTOR_EVIDENCE_STALE);
359
+ return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, evidenceFreshness.message);
360
+ }
361
+ reasonCodes.push(REASON_CODES.ACTOR_EVIDENCE_FRESH);
362
+
363
+ if (!evidence.actionBinding) {
364
+ reasonCodes.push(REASON_CODES.ACTOR_EVIDENCE_ACTION_UNBOUND);
365
+ return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, 'Actor evidence must bind the action being evaluated.');
366
+ }
367
+ checks.actorEvidenceActionBound = actionBindingMatches(evidence.actionBinding, action);
368
+ if (!checks.actorEvidenceActionBound) {
369
+ reasonCodes.push(REASON_CODES.ACTION_CONTEXT_MISMATCH);
370
+ return decision(DECISIONS.DENY, reasonCodes, checks, 'Actor evidence action binding does not match the requested action.');
371
+ }
372
+
373
+ const delegationDepth = evidence.delegationDepth;
374
+ checks.delegationDepth = delegationDepth;
375
+ checks.maxDelegationDepth = policy.maxDelegationDepth;
376
+ if (identity.actorId !== identity.subjectId && delegationDepth === null) {
377
+ reasonCodes.push(REASON_CODES.DELEGATION_CONTEXT_MISSING);
378
+ return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, 'Delegated actor evidence must include delegation_depth.');
379
+ }
380
+ if (delegationDepth !== null
381
+ && (!Number.isInteger(delegationDepth) || delegationDepth < 0 || delegationDepth > policy.maxDelegationDepth)) {
382
+ reasonCodes.push(REASON_CODES.DELEGATION_DEPTH_EXCEEDED);
383
+ return decision(DECISIONS.DENY, reasonCodes, checks, 'Actor evidence exceeds the local delegation-depth policy.');
384
+ }
385
+
386
+ return null;
387
+ }
388
+
389
+ function normalizeActorEvidence(value) {
390
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
391
+ const delegation = value.delegation && typeof value.delegation === 'object' ? value.delegation : {};
392
+ return {
393
+ verifierId: firstDefined(value.verifier_id, value.verifierId, null),
394
+ verified: value.verified === true,
395
+ revoked: value.revoked === true,
396
+ actorId: firstDefined(value.actor_id, value.actorId, null),
397
+ subjectId: firstDefined(value.subject_id, value.subjectId, null),
398
+ issuedAt: firstDefined(value.issued_at, value.issuedAt, null),
399
+ expiresAt: firstDefined(value.expires_at, value.expiresAt, null),
400
+ actionBinding: firstDefined(value.action_binding, value.actionBinding, null),
401
+ delegationDepth: firstDefined(value.delegation_depth, value.delegationDepth, delegation.depth, null),
402
+ };
403
+ }
404
+
405
+ function actorEvidenceFreshness(evidence, now, maxAgeMs) {
406
+ if (!(now instanceof Date) || Number.isNaN(now.getTime())) {
407
+ return { fresh: false, message: 'Runtime policy evaluation time is invalid.' };
408
+ }
409
+ if (!evidence.issuedAt) return { fresh: false, message: 'Actor evidence is missing issued_at.' };
410
+ const issuedAt = new Date(evidence.issuedAt);
411
+ const expiresAt = evidence.expiresAt ? new Date(evidence.expiresAt) : null;
412
+ if (Number.isNaN(issuedAt.getTime())) return { fresh: false, message: 'Actor evidence issued_at is invalid.' };
413
+ if (expiresAt && Number.isNaN(expiresAt.getTime())) return { fresh: false, message: 'Actor evidence expires_at is invalid.' };
414
+ const ageMs = now.getTime() - issuedAt.getTime();
415
+ if (ageMs < 0 || ageMs > maxAgeMs || (expiresAt && expiresAt.getTime() <= now.getTime())) {
416
+ return { fresh: false, message: 'Actor evidence is stale, expired, or future-dated.' };
417
+ }
418
+ return { fresh: true, message: null };
419
+ }
420
+
421
+ function actionBindingMatches(binding, action) {
422
+ if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return false;
423
+ const boundActionId = firstDefined(binding.action_id, binding.actionId, null);
424
+ return isNonEmptyString(action.actionId)
425
+ && boundActionId === action.actionId
426
+ && binding.type === action.type
427
+ && binding.operation === action.operation
428
+ && binding.resource === action.resource;
429
+ }
430
+
431
+ function getX402Settlement(options) {
432
+ return firstDefined(options.x402Settlement, options.x402_settlement, null);
433
+ }
434
+
435
+ function validateX402Settlement({
436
+ settlement,
437
+ identity,
438
+ action,
439
+ purpose,
440
+ expectedResource,
441
+ expectedCostUsd,
442
+ maximumCostUsd = null,
443
+ now,
444
+ policy,
445
+ }) {
446
+ const check = {
447
+ present: true,
448
+ verified: settlement && settlement.verified === true,
449
+ verifierPresent: Boolean(settlement && isNonEmptyString(firstDefined(settlement.verifier_id, settlement.verifierId, null))),
450
+ settlementIdPresent: Boolean(settlement && isNonEmptyString(firstDefined(settlement.settlement_id, settlement.settlementId, null))),
451
+ purpose: settlement ? firstDefined(settlement.purpose, null) : null,
452
+ status: settlement ? firstDefined(settlement.status, null) : null,
453
+ };
454
+ if (!settlement || typeof settlement !== 'object' || Array.isArray(settlement)
455
+ || settlement.verified !== true
456
+ || !check.verifierPresent
457
+ || !check.settlementIdPresent
458
+ || settlement.status !== 'settled') {
459
+ return {
460
+ ok: false,
461
+ check,
462
+ reasonCode: REASON_CODES.X402_SETTLEMENT_UNVERIFIED,
463
+ message: 'x402 settlement context is not verifier-confirmed as settled.',
464
+ };
465
+ }
466
+
467
+ const actorId = firstDefined(settlement.actor_id, settlement.actorId, null);
468
+ const subjectId = firstDefined(settlement.subject_id, settlement.subjectId, null);
469
+ const actionId = firstDefined(settlement.action_id, settlement.actionId, null);
470
+ const resource = firstDefined(settlement.resource, null);
471
+ const amountUsd = firstDefined(settlement.amount_usd, settlement.amountUsd, null);
472
+ const settledAt = firstDefined(settlement.settled_at, settlement.settledAt, null);
473
+ const settledDate = settledAt ? new Date(settledAt) : null;
474
+ const ageMs = settledDate ? now.getTime() - settledDate.getTime() : Number.POSITIVE_INFINITY;
475
+ const actionIdMatches = action.actionId === null ? actionId === null : actionId === action.actionId;
476
+ const amountValid = typeof amountUsd === 'number'
477
+ && Number.isFinite(amountUsd)
478
+ && amountUsd >= expectedCostUsd
479
+ && (maximumCostUsd === null || amountUsd <= maximumCostUsd);
480
+ const timeValid = settledDate && !Number.isNaN(settledDate.getTime())
481
+ && ageMs >= 0 && ageMs <= policy.maxSettlementAgeMs;
482
+ const expectedActorId = identity.explicitActorContext ? identity.actorId : identity.subjectId;
483
+
484
+ Object.assign(check, {
485
+ actorMatches: isNonEmptyString(expectedActorId)
486
+ && isNonEmptyString(actorId)
487
+ && actorId === expectedActorId,
488
+ subjectMatches: isNonEmptyString(identity.subjectId)
489
+ && isNonEmptyString(subjectId)
490
+ && subjectId === identity.subjectId,
491
+ actionMatches: actionIdMatches,
492
+ resourceMatches: resource === expectedResource,
493
+ amountCoversExpectedCost: amountValid,
494
+ fresh: timeValid,
495
+ });
496
+
497
+ if (settlement.purpose !== purpose
498
+ || !check.actorMatches
499
+ || !check.subjectMatches
500
+ || !check.actionMatches
501
+ || !check.resourceMatches
502
+ || !amountValid
503
+ || !timeValid) {
504
+ return {
505
+ ok: false,
506
+ check,
507
+ reasonCode: REASON_CODES.X402_SETTLEMENT_CONTEXT_MISMATCH,
508
+ message: 'x402 settlement is not bound to the actor, subject, action, resource, purpose, amount, and time being evaluated.',
509
+ };
510
+ }
511
+
512
+ return { ok: true, check, reasonCode: null, message: null };
513
+ }
514
+
515
+ function staleEvidenceDecision(identity, action, options, policy, now, reasonCodes, checks) {
258
516
  const lookup = action.evidenceLookup || null;
259
517
  if (!lookup || lookup.type !== 'x402') {
260
518
  return decision(DECISIONS.DEGRADE, reasonCodes, checks, 'Evidence is stale or missing; no paid lookup path is configured.');
@@ -266,6 +524,36 @@ function staleEvidenceDecision(action, options, reasonCodes, checks) {
266
524
  maxCostUsd: lookup.maxCostUsd ?? null,
267
525
  };
268
526
 
527
+ const settlement = getX402Settlement(options);
528
+ if (settlement) {
529
+ const settlementResult = validateX402Settlement({
530
+ settlement,
531
+ identity,
532
+ action,
533
+ purpose: 'evidence_lookup',
534
+ expectedResource: lookup.endpoint || null,
535
+ expectedCostUsd: 0,
536
+ maximumCostUsd: lookup.maxCostUsd ?? null,
537
+ now,
538
+ policy,
539
+ });
540
+ checks.x402Settlement = settlementResult.check;
541
+ if (!settlementResult.ok) {
542
+ reasonCodes.push(settlementResult.reasonCode);
543
+ return decision(DECISIONS.DENY, reasonCodes, checks, settlementResult.message);
544
+ }
545
+ reasonCodes.push(REASON_CODES.X402_LOOKUP_SETTLEMENT_BOUND);
546
+ reasonCodes.push(REASON_CODES.X402_SETTLEMENT_VERIFIED);
547
+ reasonCodes.push(REASON_CODES.X402_SETTLEMENT_IS_NOT_TASK_OUTCOME_PROOF);
548
+ reasonCodes.push(REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION);
549
+ return decision(
550
+ DECISIONS.DEGRADE,
551
+ reasonCodes,
552
+ checks,
553
+ 'x402 settlement covers the lookup only; refreshed evidence must still be verified before the action.'
554
+ );
555
+ }
556
+
269
557
  if (options.evidenceLookupPaymentPreapproved !== true) {
270
558
  reasonCodes.push(REASON_CODES.X402_LOOKUP_REQUIRES_APPROVAL);
271
559
  reasonCodes.push(REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION);
@@ -287,8 +575,21 @@ function decision(value, reasonCodes, checks, message) {
287
575
  }
288
576
 
289
577
  function normalizeIdentity(identity = {}) {
578
+ const explicitActorContext = hasOwn(identity, 'subject_id')
579
+ || hasOwn(identity, 'subjectId')
580
+ || hasOwn(identity, 'actor_id')
581
+ || hasOwn(identity, 'actorId')
582
+ || hasOwn(identity, 'actor_evidence')
583
+ || hasOwn(identity, 'actorEvidence');
584
+ const subjectId = firstDefined(identity.subject_id, identity.subjectId, identity.agentId, identity.profileId, null);
585
+ const actorId = firstDefined(identity.actor_id, identity.actorId, null);
586
+
290
587
  return {
291
- agentId: identity.agentId || identity.profileId || null,
588
+ agentId: subjectId,
589
+ subjectId,
590
+ actorId,
591
+ actorEvidence: firstDefined(identity.actor_evidence, identity.actorEvidence, null),
592
+ explicitActorContext,
292
593
  active: identity.active !== false,
293
594
  verified: identity.verified === true || identity.satpVerified === true,
294
595
  trustScore: clampScore(identity.trustScore ?? identity.agentFolioTrustScore ?? 0),
@@ -301,6 +602,7 @@ function normalizeAction(action = {}) {
301
602
  const parsedCost = parseActionCostUsd(action);
302
603
 
303
604
  return {
605
+ actionId: firstDefined(action.action_id, action.actionId, null),
304
606
  type: action.type || 'generic',
305
607
  resource: action.resource || null,
306
608
  resourceLabel: action.resourceLabel || null,
@@ -360,6 +662,16 @@ function firstDefined(...values) {
360
662
  return values.find((value) => value !== undefined);
361
663
  }
362
664
 
665
+ function hasOwn(value, property) {
666
+ return value !== null
667
+ && typeof value === 'object'
668
+ && Object.prototype.hasOwnProperty.call(value, property);
669
+ }
670
+
671
+ function isNonEmptyString(value) {
672
+ return typeof value === 'string' && value.trim().length > 0;
673
+ }
674
+
363
675
  function defaultResourceForAction(type, action) {
364
676
  if (type === 'mcp_protected_tool') return 'mcp://protected/tool';
365
677
  if (TRUST_SCORE_GATE_TYPES.has(type)) {
@@ -446,8 +758,20 @@ function explainRuntimePolicyResult(result = {}) {
446
758
  }
447
759
 
448
760
  const RUNTIME_POLICY_EXPLANATIONS = Object.freeze({
761
+ [REASON_CODES.ACTOR_EVIDENCE_ACTION_UNBOUND]: 'Verifier-produced actor evidence is not bound to an action.',
762
+ [REASON_CODES.ACTOR_EVIDENCE_FRESH]: 'Verifier-produced actor evidence is within the local freshness window.',
763
+ [REASON_CODES.ACTOR_EVIDENCE_MISSING]: 'Caller-supplied actor context is not authentication; actor evidence is required.',
764
+ [REASON_CODES.ACTOR_EVIDENCE_REVOKED]: 'Verifier-produced actor evidence has been revoked.',
765
+ [REASON_CODES.ACTOR_EVIDENCE_STALE]: 'Verifier-produced actor evidence is stale, expired, missing a timestamp, or future-dated.',
766
+ [REASON_CODES.ACTOR_EVIDENCE_UNVERIFIED]: 'Actor evidence is not verified by a named verifier.',
767
+ [REASON_CODES.ACTOR_EVIDENCE_VERIFIED]: 'A named verifier authenticated the actor evidence.',
768
+ [REASON_CODES.ACTOR_ID_MISSING]: 'Explicit runtime context requires actor_id.',
769
+ [REASON_CODES.ACTOR_SUBJECT_MISMATCH]: 'Actor evidence is bound to a different actor or subject.',
449
770
  [REASON_CODES.ACTION_PAYMENT_NEEDS_APPROVAL]: 'The action cost exceeds the host auto-spend policy and needs approval.',
450
771
  [REASON_CODES.ACTION_PAYMENT_PREAPPROVED]: 'The host preapproved payment for this action.',
772
+ [REASON_CODES.ACTION_CONTEXT_MISMATCH]: 'Actor evidence is bound to a different action.',
773
+ [REASON_CODES.DELEGATION_CONTEXT_MISSING]: 'Delegated actor evidence is missing its delegation depth.',
774
+ [REASON_CODES.DELEGATION_DEPTH_EXCEEDED]: 'The actor evidence exceeds the local delegation-depth limit.',
451
775
  [REASON_CODES.EVIDENCE_FRESH]: 'The identity evidence is fresh enough for this action.',
452
776
  [REASON_CODES.EVIDENCE_STALE_OR_MISSING]: 'The identity evidence is stale or missing.',
453
777
  [REASON_CODES.IDENTITY_INACTIVE]: 'The identity is inactive.',
@@ -456,12 +780,18 @@ const RUNTIME_POLICY_EXPLANATIONS = Object.freeze({
456
780
  [REASON_CODES.LOCAL_POLICY_ALLOW]: 'The local host policy allows the action.',
457
781
  [REASON_CODES.MISSING_CAPABILITY]: 'The identity is missing the capability required by this action.',
458
782
  [REASON_CODES.PROTECTED_TOOL_REQUIRES_APPROVAL]: 'The protected tool requires operator approval.',
783
+ [REASON_CODES.SUBJECT_ID_MISSING]: 'Explicit runtime context requires subject_id.',
459
784
  [REASON_CODES.TRUST_SCORE_BELOW_DENY_FLOOR]: 'The trust score is below the host deny floor.',
460
785
  [REASON_CODES.TRUST_SCORE_BELOW_MINIMUM]: 'The trust score is below the minimum for this action.',
461
786
  [REASON_CODES.TRUST_SCORE_OK]: 'The trust score satisfies the local policy.',
462
787
  [REASON_CODES.X402_LOOKUP_PAYMENT_PREAPPROVED]: 'The host preapproved payment for the x402 evidence lookup.',
463
788
  [REASON_CODES.X402_LOOKUP_REQUIRES_APPROVAL]: 'The x402 evidence lookup requires payment approval.',
789
+ [REASON_CODES.X402_LOOKUP_SETTLEMENT_BOUND]: 'A verified x402 settlement is bound to the optional evidence lookup.',
464
790
  [REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION]: 'x402 payment does not authorize the agent action.',
791
+ [REASON_CODES.X402_SETTLEMENT_CONTEXT_MISMATCH]: 'The x402 settlement does not match the evaluated actor, subject, action, resource, purpose, amount, or time.',
792
+ [REASON_CODES.X402_SETTLEMENT_IS_NOT_TASK_OUTCOME_PROOF]: 'x402 settlement proves payment only, not successful task execution.',
793
+ [REASON_CODES.X402_SETTLEMENT_UNVERIFIED]: 'The x402 settlement is not verifier-confirmed as settled.',
794
+ [REASON_CODES.X402_SETTLEMENT_VERIFIED]: 'The x402 settlement is verifier-confirmed and context-bound.',
465
795
  });
466
796
 
467
797
  function safeIsoDate(value) {
@@ -1,173 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const {
5
- buildRuntimePolicyActionDescriptorFromX402Discovery,
6
- buildSatpTrustPacket,
7
- createRuntimePolicyAdapter,
8
- getV3ProgramIds,
9
- validateSatpTrustPacket,
10
- } = require('../src');
11
-
12
- const METADATA_HASH = '93d122f8879fe87c186c10a00db8fbc80a73cecd2ede44b9ffa6410be3c2b805';
13
-
14
- async function mcpToolServerQuickstart() {
15
- function createSatpMcpTools() {
16
- return {
17
- 'satp.getPrograms': async ({ network = 'devnet' } = {}) => ({
18
- network,
19
- programs: getV3ProgramIds(network),
20
- guardrails: {
21
- readOnly: true,
22
- writesSolanaState: false,
23
- usesKeypairs: false,
24
- },
25
- }),
26
-
27
- 'satp.prepareAttestationRequest': async ({
28
- subjectWallet,
29
- agentId,
30
- claimType = 'identity',
31
- metadataHash,
32
- network = 'devnet',
33
- }) => {
34
- const packet = buildSatpTrustPacket({
35
- subjectWallet,
36
- agentId,
37
- claimType,
38
- metadataHash,
39
- network,
40
- });
41
- const validation = validateSatpTrustPacket(packet);
42
- if (!validation.ok) throw new Error(validation.errors.join('; '));
43
-
44
- return {
45
- packet,
46
- guardrails: {
47
- readOnly: true,
48
- livePaymentRequired: false,
49
- transaction: null,
50
- },
51
- };
52
- },
53
- };
54
- }
55
-
56
- const tools = createSatpMcpTools();
57
- const prepared = await tools['satp.prepareAttestationRequest']({
58
- subjectWallet: '11111111111111111111111111111111',
59
- agentId: 'example-mcp-agent',
60
- metadataHash: METADATA_HASH,
61
- });
62
- return {
63
- audience: 'mcp-tool-server-builders',
64
- mode: prepared.packet.mode,
65
- guardrails: prepared.guardrails,
66
- };
67
- }
68
-
69
- function agentRuntimeQuickstart() {
70
- const now = new Date().toISOString();
71
- const adapter = createRuntimePolicyAdapter({
72
- policy: {
73
- minimumTrustScore: 70,
74
- denyTrustScoreBelow: 25,
75
- requireVerifiedIdentity: true,
76
- maxAutoSpendUsd: 0,
77
- },
78
- });
79
- const identity = {
80
- agentId: 'partner-runtime-agent',
81
- active: true,
82
- satpVerified: true,
83
- trustScore: 82,
84
- capabilities: ['a2a:task.delegate'],
85
- evidenceUpdatedAt: now,
86
- };
87
- const action = adapter.action({
88
- type: 'a2a_agent_runtime',
89
- resource: 'a2a://task/delegate',
90
- operation: 'delegate',
91
- capability: 'a2a:task.delegate',
92
- requiresFreshEvidence: true,
93
- operatorApprovalRequired: false,
94
- });
95
- const result = adapter.evaluate(identity, action, { now });
96
- const auditTrace = adapter.auditTrace(identity, action, { result, now });
97
-
98
- return {
99
- audience: 'a2a-agent-runtime-builders',
100
- decision: result.decision,
101
- reasonCodes: result.reasonCodes,
102
- localDecisionOnly: auditTrace.guardrails.localDecisionOnly,
103
- };
104
- }
105
-
106
- function x402PaidEndpointQuickstart() {
107
- const discovery = {
108
- endpoint: 'https://api.example.test/satp/reputation',
109
- action: 'lookup',
110
- accepts: [{
111
- scheme: 'exact',
112
- network: 'solana-mainnet',
113
- asset: 'USDC',
114
- maxAmountRequired: '10000',
115
- resource: 'https://api.example.test/satp/reputation',
116
- description: 'SATP-backed reputation evidence lookup',
117
- }],
118
- };
119
- const lookupAction = buildRuntimePolicyActionDescriptorFromX402Discovery(
120
- discovery,
121
- { maxCostUsd: 0.01 }
122
- );
123
- const adapter = createRuntimePolicyAdapter({
124
- policy: { maxAutoSpendUsd: 0, minimumTrustScore: 70 },
125
- });
126
- const identity = {
127
- agentId: 'x402-consumer-agent',
128
- active: true,
129
- satpVerified: true,
130
- trustScore: 90,
131
- capabilities: ['satp:evidence.lookup'],
132
- evidenceUpdatedAt: '2026-05-21T00:00:00Z',
133
- };
134
- const result = adapter.evaluate(identity, lookupAction, {
135
- evidenceLookupPaymentPreapproved: false,
136
- now: '2026-07-31T00:00:00Z',
137
- });
138
-
139
- return {
140
- audience: 'x402-paid-endpoint-builders',
141
- decision: result.decision,
142
- reasonCodes: result.reasonCodes,
143
- guardrail: lookupAction.evidenceLookup.guardrail,
144
- paymentBoundary: {
145
- satpPackageUseIsFreeOpen: true,
146
- x402OnlyForHostedReputationEvidenceLookup: true,
147
- paymentIsNotActionAuthorization: true,
148
- livePaymentRequired: false,
149
- },
150
- };
151
- }
152
-
153
- async function run() {
154
- const examples = [
155
- await mcpToolServerQuickstart(),
156
- agentRuntimeQuickstart(),
157
- x402PaidEndpointQuickstart(),
158
- ];
159
- console.log(JSON.stringify({ examples }, null, 2));
160
- }
161
-
162
- if (require.main === module) {
163
- run().catch((err) => {
164
- console.error(err);
165
- process.exitCode = 1;
166
- });
167
- }
168
-
169
- module.exports = {
170
- agentRuntimeQuickstart,
171
- mcpToolServerQuickstart,
172
- x402PaidEndpointQuickstart,
173
- };