settld 0.1.5 → 0.2.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 (82) hide show
  1. package/README.md +32 -0
  2. package/SETTLD_VERSION +1 -1
  3. package/bin/settld.js +58 -0
  4. package/docs/CIRCLE_SANDBOX_E2E.md +12 -0
  5. package/docs/QUICKSTART_MCP.md +41 -1
  6. package/docs/QUICKSTART_MCP_HOSTS.md +156 -89
  7. package/docs/QUICKSTART_POLICY_PACKS.md +65 -0
  8. package/docs/QUICKSTART_PROFILES.md +198 -0
  9. package/docs/README.md +18 -0
  10. package/docs/RELEASE_CHECKLIST.md +26 -0
  11. package/docs/RELEASING.md +1 -0
  12. package/docs/SLO.md +62 -1
  13. package/docs/SUMMARY.md +1 -0
  14. package/docs/gitbook/README.md +13 -1
  15. package/docs/gitbook/quickstart.md +57 -58
  16. package/docs/integrations/README.md +1 -0
  17. package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +95 -0
  18. package/docs/ops/DISPUTE_FINANCE_RECONCILIATION_PACKET.md +56 -0
  19. package/docs/ops/KERNEL_V0_SHIP_GATE.md +3 -1
  20. package/docs/ops/MCP_COMPATIBILITY_MATRIX.md +8 -6
  21. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +46 -9
  22. package/docs/ops/TRUST_CONFIG_WIZARD.md +37 -24
  23. package/docs/plans/2026-02-20-trust-os-v1-jira-backlog.md +348 -0
  24. package/docs/plans/2026-02-21-agent-economic-actor-operating-model.md +169 -0
  25. package/docs/plans/2026-02-21-trust-os-v1-strategy.md +241 -0
  26. package/docs/research/2026-02-21-agent-spend-host-landscape.md +57 -0
  27. package/docs/spec/ArbitrationOutcomeMapping.v1.md +62 -0
  28. package/docs/spec/DisputeCaseLifecycle.v1.md +51 -0
  29. package/docs/spec/OperatorAction.v1.md +90 -0
  30. package/docs/spec/PolicyDecision.v1.md +83 -0
  31. package/docs/spec/README.md +5 -0
  32. package/docs/spec/SettlementDecisionRecord.v2.md +2 -0
  33. package/docs/spec/schemas/OperatorAction.v1.schema.json +113 -0
  34. package/docs/spec/schemas/PolicyDecision.v1.schema.json +74 -0
  35. package/docs/spec/schemas/SettlementDecisionRecord.v2.schema.json +1 -0
  36. package/docs/spec/x402-error-codes.v1.txt +14 -0
  37. package/package.json +14 -1
  38. package/scripts/ci/build-launch-cutover-packet.mjs +177 -21
  39. package/scripts/ci/run-10x-throughput-drill.mjs +76 -4
  40. package/scripts/ci/run-10x-throughput-incident-rehearsal.mjs +49 -6
  41. package/scripts/ci/run-mcp-host-cert-matrix.mjs +201 -0
  42. package/scripts/ci/run-mcp-host-smoke.mjs +203 -5
  43. package/scripts/ci/run-offline-verification-parity-gate.mjs +762 -0
  44. package/scripts/ci/run-onboarding-host-success-gate.mjs +516 -0
  45. package/scripts/ci/run-onboarding-policy-slo-gate.mjs +537 -0
  46. package/scripts/ci/run-production-cutover-gate.mjs +540 -0
  47. package/scripts/ci/run-public-openclaw-npx-smoke.mjs +148 -0
  48. package/scripts/ci/run-release-promotion-guard.mjs +756 -0
  49. package/scripts/doctor/mcp-host.mjs +120 -0
  50. package/scripts/mcp/settld-mcp-server.mjs +330 -20
  51. package/scripts/ops/dispute-finance-reconciliation-packet.mjs +313 -0
  52. package/scripts/ops/hosted-baseline-evidence.mjs +286 -77
  53. package/scripts/ops/run-x402-hitl-smoke.mjs +607 -0
  54. package/scripts/policy/cli.mjs +600 -0
  55. package/scripts/profile/cli.mjs +1324 -0
  56. package/scripts/register-entity-secret.mjs +102 -0
  57. package/scripts/setup/circle-bootstrap.mjs +310 -0
  58. package/scripts/setup/host-config.mjs +617 -0
  59. package/scripts/setup/onboard.mjs +1337 -0
  60. package/scripts/setup/openclaw-onboard.mjs +423 -0
  61. package/scripts/setup/wizard.mjs +986 -0
  62. package/scripts/slo/check.mjs +123 -62
  63. package/scripts/spec/generate-protocol-vectors.mjs +88 -0
  64. package/scripts/test/run.sh +23 -9
  65. package/services/x402-gateway/src/server.js +147 -36
  66. package/src/api/app.js +2345 -267
  67. package/src/api/middleware/trust-kernel.js +114 -0
  68. package/src/api/openapi.js +598 -3
  69. package/src/api/persistence.js +184 -0
  70. package/src/api/store.js +277 -0
  71. package/src/core/agent-wallets.js +134 -0
  72. package/src/core/event-policy.js +21 -2
  73. package/src/core/operator-action.js +303 -0
  74. package/src/core/policy-decision.js +322 -0
  75. package/src/core/policy-packs.js +207 -0
  76. package/src/core/profile-fingerprint.js +27 -0
  77. package/src/core/profile-simulation-reasons.js +84 -0
  78. package/src/core/profile-templates.js +242 -0
  79. package/src/core/settlement-kernel.js +27 -1
  80. package/src/core/wallet-assignment-resolver.js +129 -0
  81. package/src/core/wallet-provider-bootstrap.js +365 -0
  82. package/src/db/store-pg.js +631 -0
@@ -1,33 +1,43 @@
1
1
  import assert from "node:assert/strict";
2
2
  import fs from "node:fs/promises";
3
3
 
4
- const API_BASE_URL = process.env.SLO_API_BASE_URL ?? "http://127.0.0.1:3000";
5
- const METRICS_PATH = process.env.SLO_METRICS_PATH ?? "/metrics";
6
- const METRICS_FILE = process.env.SLO_METRICS_FILE ?? null;
4
+ const SLO_CHECK_SCHEMA_VERSION = "OperationalSloCheck.v1";
7
5
 
8
- const MAX_HTTP_5XX_TOTAL = Number(process.env.SLO_MAX_HTTP_5XX_TOTAL ?? "0");
9
- const MAX_OUTBOX_PENDING = Number(process.env.SLO_MAX_OUTBOX_PENDING ?? "200");
10
- const MAX_DELIVERY_DLQ = Number(process.env.SLO_MAX_DELIVERY_DLQ ?? "0");
11
- const MAX_DELIVERIES_PENDING = Number(process.env.SLO_MAX_DELIVERIES_PENDING ?? "0");
12
- const MAX_DELIVERIES_FAILED = Number(process.env.SLO_MAX_DELIVERIES_FAILED ?? "0");
6
+ function normalizeOptionalString(value) {
7
+ if (typeof value !== "string") return null;
8
+ const trimmed = value.trim();
9
+ return trimmed === "" ? null : trimmed;
10
+ }
13
11
 
14
- function assertFiniteNumber(n, name) {
12
+ export function assertFiniteNumber(n, name) {
15
13
  if (!Number.isFinite(n)) throw new TypeError(`${name} must be finite`);
16
14
  }
17
15
 
18
- for (const [k, v] of [
19
- ["SLO_MAX_HTTP_5XX_TOTAL", MAX_HTTP_5XX_TOTAL],
20
- ["SLO_MAX_OUTBOX_PENDING", MAX_OUTBOX_PENDING],
21
- ["SLO_MAX_DELIVERY_DLQ", MAX_DELIVERY_DLQ],
22
- ["SLO_MAX_DELIVERIES_PENDING", MAX_DELIVERIES_PENDING],
23
- ["SLO_MAX_DELIVERIES_FAILED", MAX_DELIVERIES_FAILED]
24
- ]) {
25
- assertFiniteNumber(v, k);
26
- if (v < 0) throw new TypeError(`${k} must be >= 0`);
16
+ export function parseSloThresholds(env = process.env) {
17
+ return {
18
+ maxHttp5xxTotal: Number(env.SLO_MAX_HTTP_5XX_TOTAL ?? "0"),
19
+ maxOutboxPending: Number(env.SLO_MAX_OUTBOX_PENDING ?? "200"),
20
+ maxDeliveryDlq: Number(env.SLO_MAX_DELIVERY_DLQ ?? "0"),
21
+ maxDeliveriesPending: Number(env.SLO_MAX_DELIVERIES_PENDING ?? "0"),
22
+ maxDeliveriesFailed: Number(env.SLO_MAX_DELIVERIES_FAILED ?? "0")
23
+ };
24
+ }
25
+
26
+ export function validateSloThresholds(thresholds) {
27
+ for (const [k, v] of [
28
+ ["SLO_MAX_HTTP_5XX_TOTAL", thresholds.maxHttp5xxTotal],
29
+ ["SLO_MAX_OUTBOX_PENDING", thresholds.maxOutboxPending],
30
+ ["SLO_MAX_DELIVERY_DLQ", thresholds.maxDeliveryDlq],
31
+ ["SLO_MAX_DELIVERIES_PENDING", thresholds.maxDeliveriesPending],
32
+ ["SLO_MAX_DELIVERIES_FAILED", thresholds.maxDeliveriesFailed]
33
+ ]) {
34
+ assertFiniteNumber(v, k);
35
+ if (v < 0) throw new TypeError(`${k} must be >= 0`);
36
+ }
27
37
  }
28
38
 
29
39
  function sleep(ms) {
30
- return new Promise((r) => setTimeout(r, ms));
40
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
41
  }
32
42
 
33
43
  async function fetchTextWithTimeout(url, timeoutMs = 5000) {
@@ -43,7 +53,6 @@ async function fetchTextWithTimeout(url, timeoutMs = 5000) {
43
53
  }
44
54
 
45
55
  function unescapeLabelValue(value) {
46
- // Prometheus exposition escaping.
47
56
  return String(value).replaceAll("\\\\", "\\").replaceAll("\\n", "\n").replaceAll('\\"', '"');
48
57
  }
49
58
 
@@ -94,13 +103,12 @@ function parseLabels(src) {
94
103
  return labels;
95
104
  }
96
105
 
97
- function parsePrometheusText(text) {
106
+ export function parsePrometheusText(text) {
98
107
  const series = [];
99
108
  const lines = String(text ?? "").split("\n");
100
109
  for (const lineRaw of lines) {
101
110
  const line = lineRaw.trim();
102
111
  if (!line || line.startsWith("#")) continue;
103
- // name{labels} value
104
112
  const m = /^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{[^}]*\})?\s+([-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?|NaN|Inf|-Inf)\s*$/.exec(line);
105
113
  if (!m) continue;
106
114
  const name = m[1];
@@ -112,67 +120,120 @@ function parsePrometheusText(text) {
112
120
  return series;
113
121
  }
114
122
 
115
- function sumWhere(series, { name, where = () => true } = {}) {
123
+ export function sumWhere(series, { name, where = () => true } = {}) {
116
124
  let sum = 0;
117
- for (const s of series) {
118
- if (s.name !== name) continue;
119
- if (!where(s.labels, s.value)) continue;
120
- const v = Number(s.value);
121
- if (!Number.isFinite(v)) continue;
122
- sum += v;
125
+ for (const sample of series) {
126
+ if (sample.name !== name) continue;
127
+ if (!where(sample.labels, sample.value)) continue;
128
+ const value = Number(sample.value);
129
+ if (!Number.isFinite(value)) continue;
130
+ sum += value;
123
131
  }
124
132
  return sum;
125
133
  }
126
134
 
127
- function getOne(series, { name, where = () => true } = {}) {
128
- for (const s of series) {
129
- if (s.name !== name) continue;
130
- if (!where(s.labels, s.value)) continue;
131
- return Number(s.value);
135
+ export function getOne(series, { name, where = () => true } = {}) {
136
+ for (const sample of series) {
137
+ if (sample.name !== name) continue;
138
+ if (!where(sample.labels, sample.value)) continue;
139
+ return Number(sample.value);
132
140
  }
133
141
  return null;
134
142
  }
135
143
 
136
- async function main() {
137
- let metricsText;
138
- if (METRICS_FILE) {
139
- metricsText = await fs.readFile(METRICS_FILE, "utf8");
140
- } else {
141
- // Give the server a moment to flush post-lifecycle gauges.
142
- await sleep(250);
143
- const r = await fetchTextWithTimeout(`${API_BASE_URL}${METRICS_PATH}`, 10_000);
144
- assert.equal(r.status, 200, `GET ${METRICS_PATH} failed: http ${r.status}`);
145
- metricsText = r.text;
146
- }
147
-
148
- const series = parsePrometheusText(metricsText);
149
-
144
+ export function collectOperationalSloSummary(series) {
150
145
  const http5xxTotal = sumWhere(series, {
151
146
  name: "http_requests_total",
152
147
  where: (labels) => typeof labels.status === "string" && labels.status.startsWith("5")
153
148
  });
154
-
155
149
  const outboxPending = sumWhere(series, { name: "outbox_pending_gauge" });
156
- const deliveryDlq = getOne(series, { name: "delivery_dlq_pending_total_gauge" }) ?? 0;
157
- const deliveriesPending = getOne(series, { name: "deliveries_pending_gauge", where: (l) => l.state === "pending" }) ?? 0;
158
- const deliveriesFailed = getOne(series, { name: "deliveries_pending_gauge", where: (l) => l.state === "failed" }) ?? 0;
159
-
160
- const summary = {
150
+ const deliveryDlq = sumWhere(series, { name: "delivery_dlq_pending_total_gauge" });
151
+ const deliveriesPending = sumWhere(series, {
152
+ name: "deliveries_pending_gauge",
153
+ where: (labels) => labels.state === "pending"
154
+ });
155
+ const deliveriesFailed = sumWhere(series, {
156
+ name: "deliveries_pending_gauge",
157
+ where: (labels) => labels.state === "failed"
158
+ });
159
+ return {
161
160
  http5xxTotal,
162
161
  outboxPending,
163
162
  deliveryDlq,
164
163
  deliveriesPending,
165
164
  deliveriesFailed
166
165
  };
167
- // Single-line JSON for CI logs.
168
- console.log(JSON.stringify({ slo: summary }));
166
+ }
169
167
 
170
- assert.ok(http5xxTotal <= MAX_HTTP_5XX_TOTAL, `SLO breach: http 5xx total ${http5xxTotal} > ${MAX_HTTP_5XX_TOTAL}`);
171
- assert.ok(outboxPending <= MAX_OUTBOX_PENDING, `SLO breach: outbox pending ${outboxPending} > ${MAX_OUTBOX_PENDING}`);
172
- assert.ok(deliveryDlq <= MAX_DELIVERY_DLQ, `SLO breach: delivery DLQ ${deliveryDlq} > ${MAX_DELIVERY_DLQ}`);
173
- assert.ok(deliveriesPending <= MAX_DELIVERIES_PENDING, `SLO breach: deliveries pending ${deliveriesPending} > ${MAX_DELIVERIES_PENDING}`);
174
- assert.ok(deliveriesFailed <= MAX_DELIVERIES_FAILED, `SLO breach: deliveries failed ${deliveriesFailed} > ${MAX_DELIVERIES_FAILED}`);
168
+ export function assertOperationalSlo(summary, thresholds) {
169
+ assert.ok(
170
+ summary.http5xxTotal <= thresholds.maxHttp5xxTotal,
171
+ `SLO breach: http 5xx total ${summary.http5xxTotal} > ${thresholds.maxHttp5xxTotal}`
172
+ );
173
+ assert.ok(
174
+ summary.outboxPending <= thresholds.maxOutboxPending,
175
+ `SLO breach: outbox pending ${summary.outboxPending} > ${thresholds.maxOutboxPending}`
176
+ );
177
+ assert.ok(
178
+ summary.deliveryDlq <= thresholds.maxDeliveryDlq,
179
+ `SLO breach: delivery DLQ ${summary.deliveryDlq} > ${thresholds.maxDeliveryDlq}`
180
+ );
181
+ assert.ok(
182
+ summary.deliveriesPending <= thresholds.maxDeliveriesPending,
183
+ `SLO breach: deliveries pending ${summary.deliveriesPending} > ${thresholds.maxDeliveriesPending}`
184
+ );
185
+ assert.ok(
186
+ summary.deliveriesFailed <= thresholds.maxDeliveriesFailed,
187
+ `SLO breach: deliveries failed ${summary.deliveriesFailed} > ${thresholds.maxDeliveriesFailed}`
188
+ );
175
189
  }
176
190
 
177
- await main();
191
+ export async function loadMetricsText({
192
+ metricsFile = null,
193
+ apiBaseUrl = "http://127.0.0.1:3000",
194
+ metricsPath = "/metrics",
195
+ flushDelayMs = 250
196
+ } = {}) {
197
+ if (metricsFile) {
198
+ return await fs.readFile(metricsFile, "utf8");
199
+ }
200
+ await sleep(flushDelayMs);
201
+ const response = await fetchTextWithTimeout(`${apiBaseUrl}${metricsPath}`, 10_000);
202
+ assert.equal(response.status, 200, `GET ${metricsPath} failed: http ${response.status}`);
203
+ return response.text;
204
+ }
205
+
206
+ export async function runSloCheck({ env = process.env } = {}) {
207
+ const thresholds = parseSloThresholds(env);
208
+ validateSloThresholds(thresholds);
209
+ const metricsFile = normalizeOptionalString(env.SLO_METRICS_FILE);
210
+ const metricsText = await loadMetricsText({
211
+ metricsFile,
212
+ apiBaseUrl: env.SLO_API_BASE_URL ?? "http://127.0.0.1:3000",
213
+ metricsPath: env.SLO_METRICS_PATH ?? "/metrics"
214
+ });
215
+ const series = parsePrometheusText(metricsText);
216
+ const summary = collectOperationalSloSummary(series);
217
+ assertOperationalSlo(summary, thresholds);
218
+ return summary;
219
+ }
220
+
221
+ async function main() {
222
+ const summary = await runSloCheck({ env: process.env });
223
+ console.log(JSON.stringify({ schemaVersion: SLO_CHECK_SCHEMA_VERSION, slo: summary }));
224
+ }
178
225
 
226
+ const isDirectExecution = (() => {
227
+ try {
228
+ return import.meta.url === new URL(`file://${process.argv[1]}`).href;
229
+ } catch {
230
+ return false;
231
+ }
232
+ })();
233
+
234
+ if (isDirectExecution) {
235
+ main().catch((err) => {
236
+ process.stderr.write(`${err?.stack ?? err?.message ?? String(err)}\n`);
237
+ process.exit(1);
238
+ });
239
+ }
@@ -22,6 +22,8 @@ import { buildDisputeOpenEnvelopeV1 } from "../../src/core/dispute-open-envelope
22
22
  import { buildAgreementDelegationV1 } from "../../src/core/agreement-delegation.js";
23
23
  import { buildToolCallAgreementV1 } from "../../src/core/tool-call-agreement.js";
24
24
  import { buildToolCallEvidenceV1 } from "../../src/core/tool-call-evidence.js";
25
+ import { buildPolicyDecisionV1 } from "../../src/core/policy-decision.js";
26
+ import { computeOperatorActionHashV1, signOperatorActionV1 } from "../../src/core/operator-action.js";
25
27
 
26
28
  function bytes(text) {
27
29
  return new TextEncoder().encode(text);
@@ -499,6 +501,7 @@ async function main() {
499
501
  decisionReason: null,
500
502
  verificationStatus: "green",
501
503
  policyHashUsed: "3".repeat(64),
504
+ profileHashUsed: "a".repeat(64),
502
505
  verificationMethodHashUsed: "4".repeat(64),
503
506
  policyRef: {
504
507
  policyHash: "3".repeat(64),
@@ -518,6 +521,70 @@ async function main() {
518
521
  });
519
522
  const settlementDecisionRecordV2Canonical = canonicalJsonStringify(settlementDecisionRecordV2);
520
523
 
524
+ const policyDecision = buildPolicyDecisionV1({
525
+ decisionId: "pdec_run_vectors_0001_auto",
526
+ tenantId,
527
+ runId: agentRun.runId,
528
+ settlementId: agentRunSettlement.settlementId,
529
+ gateId: "gate_vectors_0001",
530
+ policyInput: {
531
+ policyId: "policy_vectors_0001",
532
+ policyVersion: 1
533
+ },
534
+ policyHashUsed: "3".repeat(64),
535
+ verificationMethodHashUsed: "4".repeat(64),
536
+ policyDecision: {
537
+ decisionMode: "automatic",
538
+ verificationStatus: "green",
539
+ runStatus: "completed",
540
+ shouldAutoResolve: true,
541
+ settlementStatus: "released",
542
+ releaseRatePct: 100,
543
+ releaseAmountCents: 1250,
544
+ refundAmountCents: 0,
545
+ reasonCodes: []
546
+ },
547
+ createdAt: "2026-02-01T00:02:00.000Z",
548
+ signerKeyId: keyId,
549
+ signerPrivateKeyPem: privateKeyPem
550
+ });
551
+ const policyDecisionCanonical = canonicalJsonStringify(policyDecision);
552
+
553
+ const operatorActionCore = {
554
+ schemaVersion: "OperatorAction.v1",
555
+ actionId: "opact_vectors_0001",
556
+ caseRef: {
557
+ kind: "dispute",
558
+ caseId: "dsp_run_vectors_0001"
559
+ },
560
+ action: "OVERRIDE_ALLOW",
561
+ justificationCode: "OPS_OVERRIDE_APPROVED",
562
+ justification: "vector override approved",
563
+ actor: {
564
+ operatorId: "op_vectors_0001",
565
+ role: "incident_commander",
566
+ tenantId,
567
+ sessionId: "ops_session_vectors_01",
568
+ metadata: {
569
+ source: "ops-console",
570
+ ticketId: "INC-VECTORS-1"
571
+ }
572
+ },
573
+ actedAt: "2026-02-01T00:02:10.000Z",
574
+ metadata: {
575
+ severity: "critical",
576
+ checklist: ["evidence-reviewed", "lead-approved"]
577
+ }
578
+ };
579
+ const operatorAction = signOperatorActionV1({
580
+ action: operatorActionCore,
581
+ signedAt: "2026-02-01T00:02:11.000Z",
582
+ publicKeyPem,
583
+ privateKeyPem
584
+ });
585
+ const operatorActionCanonical = canonicalJsonStringify(operatorAction);
586
+ const operatorActionCoreCanonical = canonicalJsonStringify(operatorActionCore);
587
+
521
588
  const settlementReceipt = buildSettlementReceipt({
522
589
  receiptId: "rcpt_run_vectors_0001_auto",
523
590
  tenantId,
@@ -843,6 +910,27 @@ async function main() {
843
910
  canonicalJson: settlementDecisionRecordV2Canonical,
844
911
  sha256: sha256Hex(settlementDecisionRecordV2Canonical)
845
912
  },
913
+ policyDecision: {
914
+ schemaVersion: policyDecision.schemaVersion,
915
+ decisionId: policyDecision.decisionId,
916
+ policyDecisionHash: policyDecision.policyDecisionHash,
917
+ evaluationHash: policyDecision.evaluationHash,
918
+ canonicalJson: policyDecisionCanonical,
919
+ sha256: sha256Hex(policyDecisionCanonical),
920
+ signatureKeyId: policyDecision.signature?.signerKeyId ?? null,
921
+ signature: policyDecision.signature?.signature ?? null
922
+ },
923
+ operatorAction: {
924
+ schemaVersion: operatorAction.schemaVersion,
925
+ actionId: operatorAction.actionId ?? null,
926
+ actionHash: operatorAction.signature?.actionHash ?? computeOperatorActionHashV1({ action: operatorActionCore }),
927
+ canonicalJson: operatorActionCanonical,
928
+ sha256: sha256Hex(operatorActionCanonical),
929
+ coreCanonicalJson: operatorActionCoreCanonical,
930
+ coreSha256: sha256Hex(operatorActionCoreCanonical),
931
+ signatureKeyId: operatorAction.signature?.keyId ?? null,
932
+ signature: operatorAction.signature?.signatureBase64 ?? null
933
+ },
846
934
  settlementReceipt: {
847
935
  schemaVersion: settlementReceipt.schemaVersion,
848
936
  receiptId: settlementReceipt.receiptId,
@@ -4,6 +4,7 @@ set -euo pipefail
4
4
  cd "$(dirname "$0")/../.."
5
5
 
6
6
  PROBLEM_TESTS=(
7
+ "test/api-e2e-x402-authorize-payment.test.js"
7
8
  "test/api-python-sdk-first-paid-task-smoke.test.js"
8
9
  "test/api-python-sdk-first-verified-run-smoke.test.js"
9
10
  "test/magic-link-onboarding-live-contract.test.js"
@@ -15,19 +16,32 @@ PROBLEM_TESTS=(
15
16
  "test/trust-config-wizard-cli.test.js"
16
17
  )
17
18
 
18
- declare -A PROBLEM_SET=()
19
- for fp in "${PROBLEM_TESTS[@]}"; do
20
- PROBLEM_SET["$fp"]=1
21
- done
19
+ NOO_REGRESSION_TEST_FILE="test/api-e2e-x402-authorize-payment.test.js"
20
+ REQUIRED_NOO_REGRESSION_TESTS=(
21
+ "API e2e: x402 authorize-payment and verify fail closed on missing or revoked passport when required"
22
+ "API e2e: x402 authorize-payment requires valid execution intent when enabled"
23
+ "API e2e: verify enforces strict request binding evidence for quote-bound authorization"
24
+ )
22
25
 
23
- mapfile -t ALL_TESTS < <(ls test/*.test.js | sort)
26
+ for test_name in "${REQUIRED_NOO_REGRESSION_TESTS[@]}"; do
27
+ if ! grep -F "test(\"${test_name}\"" "$NOO_REGRESSION_TEST_FILE" >/dev/null; then
28
+ echo "missing required NOO regression test: ${test_name}" >&2
29
+ exit 1
30
+ fi
31
+ done
24
32
 
25
33
  SAFE_TESTS=()
26
- for fp in "${ALL_TESTS[@]}"; do
27
- if [[ -n "${PROBLEM_SET[$fp]+x}" ]]; then
28
- continue
34
+ for fp in $(ls test/*.test.js | sort); do
35
+ is_problem_test=0
36
+ for problem in "${PROBLEM_TESTS[@]}"; do
37
+ if [[ "$fp" == "$problem" ]]; then
38
+ is_problem_test=1
39
+ break
40
+ fi
41
+ done
42
+ if [[ "$is_problem_test" -eq 0 ]]; then
43
+ SAFE_TESTS+=("$fp")
29
44
  fi
30
- SAFE_TESTS+=("$fp")
31
45
  done
32
46
 
33
47
  # Phase 1: bulk suite (fast).
@@ -6,6 +6,7 @@ import { Readable } from "node:stream";
6
6
  import { parseX402PaymentRequired } from "../../../src/core/x402-gate.js";
7
7
  import { canonicalJsonStringify } from "../../../src/core/canonical-json.js";
8
8
  import { keyIdFromPublicKeyPem } from "../../../src/core/crypto.js";
9
+ import { normalizeReasonCodes as normalizePolicyDecisionReasonCodes } from "../../../src/core/policy-decision.js";
9
10
  import { buildToolProviderQuotePayloadV1, verifyToolProviderQuoteSignatureV1 } from "../../../src/core/provider-quote-signature.js";
10
11
  import { computeSettldPayRequestBindingSha256V1 } from "../../../src/core/settld-pay-token.js";
11
12
  import { computeToolProviderSignaturePayloadHashV1, verifyToolProviderSignatureV1 } from "../../../src/core/tool-provider-signature.js";
@@ -225,6 +226,31 @@ function parseProviderQuoteHeaders(headers) {
225
226
  };
226
227
  }
227
228
 
229
+ function parseAgentPassportHeader(headers) {
230
+ const rawHeader = headers?.["x-settld-agent-passport"] ?? headers?.["X-Settld-Agent-Passport"] ?? null;
231
+ const raw = typeof rawHeader === "string" ? rawHeader.trim() : Array.isArray(rawHeader) ? String(rawHeader[0] ?? "").trim() : "";
232
+ if (!raw) return { ok: true, agentPassport: null };
233
+ let text = null;
234
+ try {
235
+ if (raw.startsWith("{")) {
236
+ text = raw;
237
+ } else {
238
+ text = Buffer.from(raw, "base64url").toString("utf8");
239
+ }
240
+ } catch {
241
+ return { ok: false, message: "x-settld-agent-passport must be base64url JSON or raw JSON object" };
242
+ }
243
+ try {
244
+ const parsed = JSON.parse(text);
245
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
246
+ return { ok: false, message: "x-settld-agent-passport must decode to a JSON object" };
247
+ }
248
+ return { ok: true, agentPassport: parsed };
249
+ } catch {
250
+ return { ok: false, message: "x-settld-agent-passport is not valid JSON" };
251
+ }
252
+ }
253
+
228
254
  async function verifyProviderQuoteChallenge({
229
255
  offerFields,
230
256
  amountCents,
@@ -435,6 +461,107 @@ async function settldJson(path, { tenantId, method, idempotencyKey = null, body
435
461
  return json;
436
462
  }
437
463
 
464
+ function readDecisionRecordFromVerify(gateVerify) {
465
+ if (!gateVerify || typeof gateVerify !== "object" || Array.isArray(gateVerify)) return null;
466
+ const direct =
467
+ gateVerify.decisionRecord && typeof gateVerify.decisionRecord === "object" && !Array.isArray(gateVerify.decisionRecord)
468
+ ? gateVerify.decisionRecord
469
+ : null;
470
+ if (direct) return direct;
471
+ const fromTrace =
472
+ gateVerify?.settlement?.decisionTrace?.decisionRecord &&
473
+ typeof gateVerify.settlement.decisionTrace.decisionRecord === "object" &&
474
+ !Array.isArray(gateVerify.settlement.decisionTrace.decisionRecord)
475
+ ? gateVerify.settlement.decisionTrace.decisionRecord
476
+ : null;
477
+ return fromTrace;
478
+ }
479
+
480
+ function collectReasonCodesFromVerify(gateVerify) {
481
+ const fromGateDecision = Array.isArray(gateVerify?.gate?.decision?.reasonCodes) ? gateVerify.gate.decision.reasonCodes : [];
482
+ const fromDecision = Array.isArray(gateVerify?.decision?.reasonCodes) ? gateVerify.decision.reasonCodes : [];
483
+ return normalizePolicyDecisionReasonCodes([...fromGateDecision, ...fromDecision], "gateVerify.reasonCodes");
484
+ }
485
+
486
+ function derivePolicyDecisionFromVerify(gateVerify) {
487
+ const settlementStatus = String(gateVerify?.settlement?.status ?? "").trim().toLowerCase();
488
+ if (settlementStatus === "released") return "allow";
489
+ if (settlementStatus === "refunded") return "deny";
490
+ if (gateVerify?.decision?.shouldAutoResolve === false) return "challenge";
491
+ if (settlementStatus) return "escalate";
492
+ return null;
493
+ }
494
+
495
+ function applySettldDecisionHeaders(outHeaders, { gateId, gateVerify } = {}) {
496
+ if (gateId) outHeaders["x-settld-gate-id"] = gateId;
497
+ if (!gateVerify || typeof gateVerify !== "object" || Array.isArray(gateVerify)) return;
498
+
499
+ outHeaders["x-settld-settlement-status"] = String(gateVerify?.settlement?.status ?? "");
500
+ outHeaders["x-settld-released-amount-cents"] = String(gateVerify?.settlement?.releasedAmountCents ?? "");
501
+ outHeaders["x-settld-refunded-amount-cents"] = String(gateVerify?.settlement?.refundedAmountCents ?? "");
502
+
503
+ const verificationStatus = String(gateVerify?.gate?.decision?.verificationStatus ?? "").trim();
504
+ if (verificationStatus) outHeaders["x-settld-verification-status"] = verificationStatus;
505
+
506
+ const policyDecision = derivePolicyDecisionFromVerify(gateVerify);
507
+ if (policyDecision) outHeaders["x-settld-policy-decision"] = policyDecision;
508
+
509
+ const reasonCodes = collectReasonCodesFromVerify(gateVerify);
510
+ if (reasonCodes.length > 0) {
511
+ outHeaders["x-settld-verification-codes"] = reasonCodes.join(",");
512
+ outHeaders["x-settld-reason-code"] = reasonCodes[0];
513
+ } else if (policyDecision) {
514
+ const fallbackReasonCode =
515
+ policyDecision === "allow"
516
+ ? "POLICY_ALLOW"
517
+ : policyDecision === "deny"
518
+ ? "POLICY_DENY"
519
+ : policyDecision === "challenge"
520
+ ? "POLICY_CHALLENGE"
521
+ : "POLICY_ESCALATE";
522
+ outHeaders["x-settld-reason-code"] = fallbackReasonCode;
523
+ }
524
+
525
+ const decisionRecord = readDecisionRecordFromVerify(gateVerify);
526
+ const decisionId = typeof decisionRecord?.decisionId === "string" ? decisionRecord.decisionId.trim() : "";
527
+ if (decisionId) outHeaders["x-settld-decision-id"] = decisionId;
528
+
529
+ const policyHashRaw =
530
+ typeof decisionRecord?.policyHashUsed === "string" && decisionRecord.policyHashUsed.trim() !== ""
531
+ ? decisionRecord.policyHashUsed
532
+ : typeof decisionRecord?.policyRef?.policyHash === "string" && decisionRecord.policyRef.policyHash.trim() !== ""
533
+ ? decisionRecord.policyRef.policyHash
534
+ : null;
535
+ const policyHash = typeof policyHashRaw === "string" ? policyHashRaw.trim().toLowerCase() : "";
536
+ if (/^[0-9a-f]{64}$/.test(policyHash)) outHeaders["x-settld-policy-hash"] = policyHash;
537
+
538
+ const policyVersion = Number(decisionRecord?.bindings?.policyDecisionFingerprint?.policyVersion ?? Number.NaN);
539
+ if (Number.isSafeInteger(policyVersion) && policyVersion > 0) {
540
+ outHeaders["x-settld-policy-version"] = String(policyVersion);
541
+ }
542
+ const policyDecisionFingerprint =
543
+ decisionRecord?.bindings?.policyDecisionFingerprint &&
544
+ typeof decisionRecord.bindings.policyDecisionFingerprint === "object" &&
545
+ !Array.isArray(decisionRecord.bindings.policyDecisionFingerprint)
546
+ ? decisionRecord.bindings.policyDecisionFingerprint
547
+ : null;
548
+ const policyVerificationMethodHash =
549
+ typeof policyDecisionFingerprint?.verificationMethodHash === "string"
550
+ ? policyDecisionFingerprint.verificationMethodHash.trim().toLowerCase()
551
+ : "";
552
+ if (/^[0-9a-f]{64}$/.test(policyVerificationMethodHash)) {
553
+ outHeaders["x-settld-policy-verification-method-hash"] = policyVerificationMethodHash;
554
+ }
555
+ const policyEvaluationHash =
556
+ typeof policyDecisionFingerprint?.evaluationHash === "string" ? policyDecisionFingerprint.evaluationHash.trim().toLowerCase() : "";
557
+ if (/^[0-9a-f]{64}$/.test(policyEvaluationHash)) {
558
+ outHeaders["x-settld-policy-evaluation-hash"] = policyEvaluationHash;
559
+ }
560
+
561
+ if (gateVerify?.gate?.holdback?.status) outHeaders["x-settld-holdback-status"] = String(gateVerify.gate.holdback.status);
562
+ if (gateVerify?.gate?.holdback?.amountCents !== undefined) outHeaders["x-settld-holdback-amount-cents"] = String(gateVerify.gate.holdback.amountCents);
563
+ }
564
+
438
565
  async function handleProxy(req, res) {
439
566
  const url = new URL(req.url ?? "/", "http://localhost");
440
567
  if (req.method === "GET" && url.pathname === "/healthz") {
@@ -445,10 +572,24 @@ async function handleProxy(req, res) {
445
572
 
446
573
  const tenantId = tenantIdForRequest(req);
447
574
  const upstreamUrl = new URL(url.pathname + url.search, UPSTREAM_URL);
575
+ const parsedAgentPassportHeader = parseAgentPassportHeader(req.headers);
576
+ if (!parsedAgentPassportHeader.ok) {
577
+ res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
578
+ res.end(
579
+ JSON.stringify({
580
+ ok: false,
581
+ error: "invalid_agent_passport_header",
582
+ message: parsedAgentPassportHeader.message
583
+ })
584
+ );
585
+ return;
586
+ }
587
+ const requestAgentPassport = parsedAgentPassportHeader.agentPassport;
448
588
  const headers = new Headers();
449
589
  for (const [k, v] of Object.entries(req.headers)) {
450
590
  if (v === undefined) continue;
451
591
  if (k.toLowerCase() === "host") continue;
592
+ if (k.toLowerCase() === "x-settld-agent-passport") continue;
452
593
  if (Array.isArray(v)) headers.set(k, v.join(","));
453
594
  else headers.set(k, String(v));
454
595
  }
@@ -642,6 +783,7 @@ async function handleProxy(req, res) {
642
783
  holdbackBps: HOLDBACK_BPS,
643
784
  disputeWindowMs: DISPUTE_WINDOW_MS,
644
785
  ...(offeredToolId ? { toolId: offeredToolId } : {}),
786
+ ...(requestAgentPassport ? { agentPassport: requestAgentPassport } : {}),
645
787
  ...(X402_PROVIDER_PUBLIC_KEY_PEM ? { providerPublicKeyPem: X402_PROVIDER_PUBLIC_KEY_PEM } : {}),
646
788
  paymentRequiredHeader: { "x-payment-required": parsed.raw }
647
789
  }
@@ -696,18 +838,7 @@ async function handleProxy(req, res) {
696
838
  });
697
839
 
698
840
  const outHeaders = Object.fromEntries(upstreamRes.headers.entries());
699
- outHeaders["x-settld-gate-id"] = gateId;
700
- outHeaders["x-settld-settlement-status"] = String(gateVerify?.settlement?.status ?? "");
701
- outHeaders["x-settld-released-amount-cents"] = String(gateVerify?.settlement?.releasedAmountCents ?? "");
702
- outHeaders["x-settld-refunded-amount-cents"] = String(gateVerify?.settlement?.refundedAmountCents ?? "");
703
- if (gateVerify?.gate?.decision?.verificationStatus) {
704
- outHeaders["x-settld-verification-status"] = String(gateVerify.gate.decision.verificationStatus);
705
- }
706
- if (Array.isArray(gateVerify?.gate?.decision?.reasonCodes) && gateVerify.gate.decision.reasonCodes.length > 0) {
707
- outHeaders["x-settld-verification-codes"] = gateVerify.gate.decision.reasonCodes.join(",");
708
- }
709
- if (gateVerify?.gate?.holdback?.status) outHeaders["x-settld-holdback-status"] = String(gateVerify.gate.holdback.status);
710
- if (gateVerify?.gate?.holdback?.amountCents !== undefined) outHeaders["x-settld-holdback-amount-cents"] = String(gateVerify.gate.holdback.amountCents);
841
+ applySettldDecisionHeaders(outHeaders, { gateId, gateVerify });
711
842
 
712
843
  res.writeHead(502, outHeaders);
713
844
  res.end(`gateway: response too large to verify (>${2 * 1024 * 1024} bytes); refunded`);
@@ -853,19 +984,8 @@ async function handleProxy(req, res) {
853
984
  });
854
985
 
855
986
  const outHeaders = Object.fromEntries(upstreamRes.headers.entries());
856
- outHeaders["x-settld-gate-id"] = gateId;
857
987
  outHeaders["x-settld-response-sha256"] = respHash;
858
- outHeaders["x-settld-settlement-status"] = String(gateVerify?.settlement?.status ?? "");
859
- outHeaders["x-settld-released-amount-cents"] = String(gateVerify?.settlement?.releasedAmountCents ?? "");
860
- outHeaders["x-settld-refunded-amount-cents"] = String(gateVerify?.settlement?.refundedAmountCents ?? "");
861
- if (gateVerify?.gate?.decision?.verificationStatus) {
862
- outHeaders["x-settld-verification-status"] = String(gateVerify.gate.decision.verificationStatus);
863
- }
864
- if (Array.isArray(gateVerify?.gate?.decision?.reasonCodes) && gateVerify.gate.decision.reasonCodes.length > 0) {
865
- outHeaders["x-settld-verification-codes"] = gateVerify.gate.decision.reasonCodes.join(",");
866
- }
867
- if (gateVerify?.gate?.holdback?.status) outHeaders["x-settld-holdback-status"] = String(gateVerify.gate.holdback.status);
868
- if (gateVerify?.gate?.holdback?.amountCents !== undefined) outHeaders["x-settld-holdback-amount-cents"] = String(gateVerify.gate.holdback.amountCents);
988
+ applySettldDecisionHeaders(outHeaders, { gateId, gateVerify });
869
989
 
870
990
  res.writeHead(upstreamRes.status, outHeaders);
871
991
  res.end(capture.buf);
@@ -900,19 +1020,10 @@ async function handleProxy(req, res) {
900
1020
  } catch {}
901
1021
 
902
1022
  const outHeaders = Object.fromEntries(upstreamRes.headers.entries());
903
- outHeaders["x-settld-gate-id"] = gateId;
904
1023
  if (gateVerify) {
905
- outHeaders["x-settld-settlement-status"] = String(gateVerify?.settlement?.status ?? "");
906
- outHeaders["x-settld-released-amount-cents"] = String(gateVerify?.settlement?.releasedAmountCents ?? "");
907
- outHeaders["x-settld-refunded-amount-cents"] = String(gateVerify?.settlement?.refundedAmountCents ?? "");
908
- if (gateVerify?.gate?.decision?.verificationStatus) {
909
- outHeaders["x-settld-verification-status"] = String(gateVerify.gate.decision.verificationStatus);
910
- }
911
- if (Array.isArray(gateVerify?.gate?.decision?.reasonCodes) && gateVerify.gate.decision.reasonCodes.length > 0) {
912
- outHeaders["x-settld-verification-codes"] = gateVerify.gate.decision.reasonCodes.join(",");
913
- }
914
- if (gateVerify?.gate?.holdback?.status) outHeaders["x-settld-holdback-status"] = String(gateVerify.gate.holdback.status);
915
- if (gateVerify?.gate?.holdback?.amountCents !== undefined) outHeaders["x-settld-holdback-amount-cents"] = String(gateVerify.gate.holdback.amountCents);
1024
+ applySettldDecisionHeaders(outHeaders, { gateId, gateVerify });
1025
+ } else if (gateId) {
1026
+ outHeaders["x-settld-gate-id"] = gateId;
916
1027
  }
917
1028
 
918
1029
  res.writeHead(502, outHeaders);