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.
- package/README.md +32 -0
- package/SETTLD_VERSION +1 -1
- package/bin/settld.js +58 -0
- package/docs/CIRCLE_SANDBOX_E2E.md +12 -0
- package/docs/QUICKSTART_MCP.md +41 -1
- package/docs/QUICKSTART_MCP_HOSTS.md +156 -89
- package/docs/QUICKSTART_POLICY_PACKS.md +65 -0
- package/docs/QUICKSTART_PROFILES.md +198 -0
- package/docs/README.md +18 -0
- package/docs/RELEASE_CHECKLIST.md +26 -0
- package/docs/RELEASING.md +1 -0
- package/docs/SLO.md +62 -1
- package/docs/SUMMARY.md +1 -0
- package/docs/gitbook/README.md +13 -1
- package/docs/gitbook/quickstart.md +57 -58
- package/docs/integrations/README.md +1 -0
- package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +95 -0
- package/docs/ops/DISPUTE_FINANCE_RECONCILIATION_PACKET.md +56 -0
- package/docs/ops/KERNEL_V0_SHIP_GATE.md +3 -1
- package/docs/ops/MCP_COMPATIBILITY_MATRIX.md +8 -6
- package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +46 -9
- package/docs/ops/TRUST_CONFIG_WIZARD.md +37 -24
- package/docs/plans/2026-02-20-trust-os-v1-jira-backlog.md +348 -0
- package/docs/plans/2026-02-21-agent-economic-actor-operating-model.md +169 -0
- package/docs/plans/2026-02-21-trust-os-v1-strategy.md +241 -0
- package/docs/research/2026-02-21-agent-spend-host-landscape.md +57 -0
- package/docs/spec/ArbitrationOutcomeMapping.v1.md +62 -0
- package/docs/spec/DisputeCaseLifecycle.v1.md +51 -0
- package/docs/spec/OperatorAction.v1.md +90 -0
- package/docs/spec/PolicyDecision.v1.md +83 -0
- package/docs/spec/README.md +5 -0
- package/docs/spec/SettlementDecisionRecord.v2.md +2 -0
- package/docs/spec/schemas/OperatorAction.v1.schema.json +113 -0
- package/docs/spec/schemas/PolicyDecision.v1.schema.json +74 -0
- package/docs/spec/schemas/SettlementDecisionRecord.v2.schema.json +1 -0
- package/docs/spec/x402-error-codes.v1.txt +14 -0
- package/package.json +14 -1
- package/scripts/ci/build-launch-cutover-packet.mjs +177 -21
- package/scripts/ci/run-10x-throughput-drill.mjs +76 -4
- package/scripts/ci/run-10x-throughput-incident-rehearsal.mjs +49 -6
- package/scripts/ci/run-mcp-host-cert-matrix.mjs +201 -0
- package/scripts/ci/run-mcp-host-smoke.mjs +203 -5
- package/scripts/ci/run-offline-verification-parity-gate.mjs +762 -0
- package/scripts/ci/run-onboarding-host-success-gate.mjs +516 -0
- package/scripts/ci/run-onboarding-policy-slo-gate.mjs +537 -0
- package/scripts/ci/run-production-cutover-gate.mjs +540 -0
- package/scripts/ci/run-public-openclaw-npx-smoke.mjs +148 -0
- package/scripts/ci/run-release-promotion-guard.mjs +756 -0
- package/scripts/doctor/mcp-host.mjs +120 -0
- package/scripts/mcp/settld-mcp-server.mjs +330 -20
- package/scripts/ops/dispute-finance-reconciliation-packet.mjs +313 -0
- package/scripts/ops/hosted-baseline-evidence.mjs +286 -77
- package/scripts/ops/run-x402-hitl-smoke.mjs +607 -0
- package/scripts/policy/cli.mjs +600 -0
- package/scripts/profile/cli.mjs +1324 -0
- package/scripts/register-entity-secret.mjs +102 -0
- package/scripts/setup/circle-bootstrap.mjs +310 -0
- package/scripts/setup/host-config.mjs +617 -0
- package/scripts/setup/onboard.mjs +1337 -0
- package/scripts/setup/openclaw-onboard.mjs +423 -0
- package/scripts/setup/wizard.mjs +986 -0
- package/scripts/slo/check.mjs +123 -62
- package/scripts/spec/generate-protocol-vectors.mjs +88 -0
- package/scripts/test/run.sh +23 -9
- package/services/x402-gateway/src/server.js +147 -36
- package/src/api/app.js +2345 -267
- package/src/api/middleware/trust-kernel.js +114 -0
- package/src/api/openapi.js +598 -3
- package/src/api/persistence.js +184 -0
- package/src/api/store.js +277 -0
- package/src/core/agent-wallets.js +134 -0
- package/src/core/event-policy.js +21 -2
- package/src/core/operator-action.js +303 -0
- package/src/core/policy-decision.js +322 -0
- package/src/core/policy-packs.js +207 -0
- package/src/core/profile-fingerprint.js +27 -0
- package/src/core/profile-simulation-reasons.js +84 -0
- package/src/core/profile-templates.js +242 -0
- package/src/core/settlement-kernel.js +27 -1
- package/src/core/wallet-assignment-resolver.js +129 -0
- package/src/core/wallet-provider-bootstrap.js +365 -0
- package/src/db/store-pg.js +631 -0
package/scripts/slo/check.mjs
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
const
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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((
|
|
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
|
|
118
|
-
if (
|
|
119
|
-
if (!where(
|
|
120
|
-
const
|
|
121
|
-
if (!Number.isFinite(
|
|
122
|
-
sum +=
|
|
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
|
|
129
|
-
if (
|
|
130
|
-
if (!where(
|
|
131
|
-
return Number(
|
|
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
|
-
|
|
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 =
|
|
157
|
-
const deliveriesPending =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
console.log(JSON.stringify({ slo: summary }));
|
|
166
|
+
}
|
|
169
167
|
|
|
170
|
-
|
|
171
|
-
assert.ok(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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,
|
package/scripts/test/run.sh
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
906
|
-
|
|
907
|
-
outHeaders["x-settld-
|
|
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);
|