@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.
- package/LICENSE +201 -0
- package/README.md +63 -35
- package/idls/v3/attestations_v3.json +439 -0
- package/idls/v3/escrow_v3.json +972 -0
- package/idls/v3/identity_v3.json +1344 -0
- package/idls/v3/reputation_v3.json +140 -0
- package/idls/v3/reviews_v3.json +300 -0
- package/idls/v3/validation_v3.json +146 -0
- package/package.json +12 -4
- package/src/attestation-request.js +176 -0
- package/src/index.d.ts +221 -0
- package/src/index.js +12 -1
- package/src/runtime-authorization-evidence.js +350 -0
- package/src/runtime-policy-adapter.js +335 -5
- package/examples/adoption-quickstarts.js +0 -173
- package/examples/integration-patterns.js +0 -495
- package/examples/runtime-policy-adapter.js +0 -107
- package/examples/x402-discovery-evidence-lookup.js +0 -66
- package/examples/x402-reputation-evidence-lookup-client.js +0 -182
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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
|
-
};
|