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
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
import { createEd25519Keypair, sha256Hex } from "../../src/core/crypto.js";
|
|
8
|
+
|
|
9
|
+
export const X402_HITL_SMOKE_SCHEMA_VERSION = "X402HitlEscalationSmoke.v1";
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
return [
|
|
13
|
+
"usage: node scripts/ops/run-x402-hitl-smoke.mjs [options]",
|
|
14
|
+
"",
|
|
15
|
+
"options:",
|
|
16
|
+
" --base-url <url> API base URL (default: $SETTLD_BASE_URL or http://127.0.0.1:3000)",
|
|
17
|
+
" --tenant-id <id> Tenant ID (default: $SETTLD_TENANT_ID or tenant_default)",
|
|
18
|
+
" --protocol <v> Protocol header value (default: $SETTLD_PROTOCOL or 1.0)",
|
|
19
|
+
" --api-key <key> API key token (keyId.secret). If omitted, script mints one via --ops-token.",
|
|
20
|
+
" --ops-token <tok> Ops token used to mint API key when --api-key is not provided",
|
|
21
|
+
" --out <file> Report output path (default: artifacts/ops/x402-hitl-smoke.json)",
|
|
22
|
+
" --help Show help"
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeOptionalString(value) {
|
|
27
|
+
if (typeof value !== "string") return null;
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed === "" ? null : trimmed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseArgs(argv, env = process.env) {
|
|
33
|
+
const out = {
|
|
34
|
+
baseUrl: normalizeOptionalString(env.SETTLD_BASE_URL) ?? "http://127.0.0.1:3000",
|
|
35
|
+
tenantId: normalizeOptionalString(env.SETTLD_TENANT_ID) ?? "tenant_default",
|
|
36
|
+
protocol: normalizeOptionalString(env.SETTLD_PROTOCOL) ?? "1.0",
|
|
37
|
+
apiKey: normalizeOptionalString(env.SETTLD_API_KEY),
|
|
38
|
+
opsToken: normalizeOptionalString(env.PROXY_OPS_TOKEN) ?? normalizeOptionalString(env.SETTLD_OPS_TOKEN),
|
|
39
|
+
outPath: "artifacts/ops/x402-hitl-smoke.json",
|
|
40
|
+
help: false
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
44
|
+
const arg = String(argv[i] ?? "").trim();
|
|
45
|
+
if (!arg) continue;
|
|
46
|
+
if (arg === "--help" || arg === "-h") {
|
|
47
|
+
out.help = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg === "--base-url") {
|
|
51
|
+
out.baseUrl = normalizeOptionalString(argv[i + 1]) ?? "";
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === "--tenant-id") {
|
|
56
|
+
out.tenantId = normalizeOptionalString(argv[i + 1]) ?? "";
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "--protocol") {
|
|
61
|
+
out.protocol = normalizeOptionalString(argv[i + 1]) ?? "";
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--api-key") {
|
|
66
|
+
out.apiKey = normalizeOptionalString(argv[i + 1]);
|
|
67
|
+
i += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--ops-token") {
|
|
71
|
+
out.opsToken = normalizeOptionalString(argv[i + 1]);
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--out") {
|
|
76
|
+
out.outPath = normalizeOptionalString(argv[i + 1]) ?? "";
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!out.help) {
|
|
84
|
+
if (!out.baseUrl) throw new Error("--base-url is required");
|
|
85
|
+
if (!out.tenantId) throw new Error("--tenant-id is required");
|
|
86
|
+
if (!out.protocol) throw new Error("--protocol is required");
|
|
87
|
+
if (!out.outPath) throw new Error("--out is required");
|
|
88
|
+
try {
|
|
89
|
+
new URL(out.baseUrl);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error("--base-url must be a valid URL");
|
|
92
|
+
}
|
|
93
|
+
if (!out.apiKey && !out.opsToken) {
|
|
94
|
+
throw new Error("provide --api-key or --ops-token");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function safeJsonParse(raw) {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(String(raw ?? ""));
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function ensureDirForFile(filePath) {
|
|
110
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function writeJson(filePath, value) {
|
|
114
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
115
|
+
ensureDirForFile(resolved);
|
|
116
|
+
fs.writeFileSync(resolved, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
117
|
+
return resolved;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function truncateText(text, max = 700) {
|
|
121
|
+
const value = String(text ?? "");
|
|
122
|
+
if (value.length <= max) return value;
|
|
123
|
+
return `${value.slice(0, max)}...`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toErrorDetails(response) {
|
|
127
|
+
const json = response?.json && typeof response.json === "object" ? response.json : null;
|
|
128
|
+
if (json) return truncateText(JSON.stringify(json));
|
|
129
|
+
return truncateText(response?.text ?? "");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function expectStatus(response, expectedStatus, context) {
|
|
133
|
+
if (response.status === expectedStatus) return;
|
|
134
|
+
throw new Error(`${context} failed: expected HTTP ${expectedStatus}, got ${response.status} (${toErrorDetails(response)})`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function requestJson({ baseUrl, method, pathname, headers, body }) {
|
|
138
|
+
const url = new URL(pathname, baseUrl);
|
|
139
|
+
const res = await fetch(url, {
|
|
140
|
+
method,
|
|
141
|
+
headers,
|
|
142
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
143
|
+
});
|
|
144
|
+
const text = await res.text();
|
|
145
|
+
const json = safeJsonParse(text);
|
|
146
|
+
return {
|
|
147
|
+
status: res.status,
|
|
148
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
149
|
+
text,
|
|
150
|
+
json
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildApiHeaders({ tenantId, protocol, apiKey, idempotencyKey = null, withBody = false }) {
|
|
155
|
+
return {
|
|
156
|
+
"x-proxy-tenant-id": tenantId,
|
|
157
|
+
"x-settld-protocol": protocol,
|
|
158
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
159
|
+
...(idempotencyKey ? { "x-idempotency-key": idempotencyKey } : {}),
|
|
160
|
+
...(withBody ? { "content-type": "application/json; charset=utf-8" } : {})
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function apiRequest(ctx, { method, pathname, body = undefined, idempotencyKey = null }) {
|
|
165
|
+
return await requestJson({
|
|
166
|
+
baseUrl: ctx.baseUrl,
|
|
167
|
+
method,
|
|
168
|
+
pathname,
|
|
169
|
+
headers: buildApiHeaders({
|
|
170
|
+
tenantId: ctx.tenantId,
|
|
171
|
+
protocol: ctx.protocol,
|
|
172
|
+
apiKey: ctx.apiKey,
|
|
173
|
+
idempotencyKey,
|
|
174
|
+
withBody: body !== undefined
|
|
175
|
+
}),
|
|
176
|
+
body
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function mintApiKey({ baseUrl, tenantId, protocol, opsToken }) {
|
|
181
|
+
const response = await requestJson({
|
|
182
|
+
baseUrl,
|
|
183
|
+
method: "POST",
|
|
184
|
+
pathname: "/ops/api-keys",
|
|
185
|
+
headers: {
|
|
186
|
+
"x-proxy-tenant-id": tenantId,
|
|
187
|
+
"x-settld-protocol": protocol,
|
|
188
|
+
"x-proxy-ops-token": opsToken,
|
|
189
|
+
authorization: `Bearer ${opsToken}`,
|
|
190
|
+
"content-type": "application/json; charset=utf-8"
|
|
191
|
+
},
|
|
192
|
+
body: {
|
|
193
|
+
scopes: ["ops_read", "ops_write", "finance_read", "finance_write", "audit_read"],
|
|
194
|
+
description: "x402 hitl smoke script"
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
expectStatus(response, 201, "mint api key");
|
|
198
|
+
const keyId = normalizeOptionalString(response.json?.keyId);
|
|
199
|
+
const secret = normalizeOptionalString(response.json?.secret);
|
|
200
|
+
if (!keyId || !secret) {
|
|
201
|
+
throw new Error(`mint api key failed: missing keyId/secret in response (${toErrorDetails(response)})`);
|
|
202
|
+
}
|
|
203
|
+
return `${keyId}.${secret}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function registerAgent(ctx, { agentId, idem }) {
|
|
207
|
+
const { publicKeyPem } = createEd25519Keypair();
|
|
208
|
+
const response = await apiRequest(ctx, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
pathname: "/agents/register",
|
|
211
|
+
idempotencyKey: idem,
|
|
212
|
+
body: {
|
|
213
|
+
agentId,
|
|
214
|
+
displayName: `Agent ${agentId}`,
|
|
215
|
+
owner: { ownerType: "service", ownerId: "svc_ops_smoke" },
|
|
216
|
+
publicKeyPem
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
expectStatus(response, 201, `register agent ${agentId}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function creditWallet(ctx, { agentId, amountCents, idem }) {
|
|
223
|
+
const response = await apiRequest(ctx, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
pathname: `/agents/${encodeURIComponent(agentId)}/wallet/credit`,
|
|
226
|
+
idempotencyKey: idem,
|
|
227
|
+
body: {
|
|
228
|
+
amountCents,
|
|
229
|
+
currency: "USD"
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
expectStatus(response, 201, `credit wallet ${agentId}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function upsertWalletPolicy(ctx, { policy, idem }) {
|
|
236
|
+
const response = await apiRequest(ctx, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
pathname: "/ops/x402/wallet-policies",
|
|
239
|
+
idempotencyKey: idem,
|
|
240
|
+
body: { policy }
|
|
241
|
+
});
|
|
242
|
+
expectStatus(response, 201, "upsert wallet policy");
|
|
243
|
+
return response.json?.policy ?? null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function createGate(ctx, { gateId, payerAgentId, payeeAgentId, amountCents, walletPolicy }) {
|
|
247
|
+
const response = await apiRequest(ctx, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
pathname: "/x402/gate/create",
|
|
250
|
+
idempotencyKey: `x402_gate_create_${gateId}`,
|
|
251
|
+
body: {
|
|
252
|
+
gateId,
|
|
253
|
+
payerAgentId,
|
|
254
|
+
payeeAgentId,
|
|
255
|
+
toolId: "weather_read",
|
|
256
|
+
amountCents,
|
|
257
|
+
currency: "USD",
|
|
258
|
+
agentPassport: {
|
|
259
|
+
sponsorRef: walletPolicy.sponsorRef,
|
|
260
|
+
sponsorWalletRef: walletPolicy.sponsorWalletRef,
|
|
261
|
+
agentKeyId: `agent_key_${gateId}`,
|
|
262
|
+
delegationRef: `delegation_${gateId}`,
|
|
263
|
+
policyRef: walletPolicy.policyRef,
|
|
264
|
+
policyVersion: walletPolicy.policyVersion
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
expectStatus(response, 201, `create gate ${gateId}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function issueWalletDecision(ctx, { sponsorWalletRef, gateId, idem }) {
|
|
272
|
+
const response = await apiRequest(ctx, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
pathname: `/x402/wallets/${encodeURIComponent(sponsorWalletRef)}/authorize`,
|
|
275
|
+
idempotencyKey: idem,
|
|
276
|
+
body: { gateId }
|
|
277
|
+
});
|
|
278
|
+
expectStatus(response, 200, `issue wallet decision ${gateId}`);
|
|
279
|
+
const token = normalizeOptionalString(response.json?.walletAuthorizationDecisionToken);
|
|
280
|
+
if (!token) throw new Error(`issue wallet decision ${gateId} failed: token missing`);
|
|
281
|
+
return token;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function authorizeGate(ctx, { gateId, walletAuthorizationDecisionToken, escalationOverrideToken = null, idem }) {
|
|
285
|
+
return await apiRequest(ctx, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
pathname: "/x402/gate/authorize-payment",
|
|
288
|
+
idempotencyKey: idem,
|
|
289
|
+
body: {
|
|
290
|
+
gateId,
|
|
291
|
+
walletAuthorizationDecisionToken,
|
|
292
|
+
...(escalationOverrideToken ? { escalationOverrideToken } : {})
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function resolveEscalation(ctx, { escalationId, action, idem, reason = null }) {
|
|
298
|
+
const response = await apiRequest(ctx, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
pathname: `/x402/gate/escalations/${encodeURIComponent(escalationId)}/resolve`,
|
|
301
|
+
idempotencyKey: idem,
|
|
302
|
+
body: {
|
|
303
|
+
action,
|
|
304
|
+
...(reason ? { reason } : {})
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
expectStatus(response, 200, `resolve escalation ${escalationId} (${action})`);
|
|
308
|
+
return response;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function check(id, ok, details = null) {
|
|
312
|
+
return {
|
|
313
|
+
id,
|
|
314
|
+
ok: ok === true,
|
|
315
|
+
details: details && typeof details === "object" && !Array.isArray(details) ? details : details ?? null
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function expectEscalationRequired(response, context) {
|
|
320
|
+
expectStatus(response, 409, context);
|
|
321
|
+
const code = normalizeOptionalString(response.json?.code);
|
|
322
|
+
if (code !== "X402_AUTHORIZATION_ESCALATION_REQUIRED") {
|
|
323
|
+
throw new Error(`${context} failed: expected code X402_AUTHORIZATION_ESCALATION_REQUIRED, got ${code ?? "null"}`);
|
|
324
|
+
}
|
|
325
|
+
const escalation = response.json?.details?.escalation;
|
|
326
|
+
const escalationId = normalizeOptionalString(escalation?.escalationId);
|
|
327
|
+
if (!escalationId) throw new Error(`${context} failed: escalationId missing`);
|
|
328
|
+
return escalationId;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function verifyGateAndFetchReceipt(ctx, { gateId, idPrefix }) {
|
|
332
|
+
const requestSha256 = sha256Hex(`${idPrefix}_request`);
|
|
333
|
+
const responseSha256 = sha256Hex(`${idPrefix}_response`);
|
|
334
|
+
const verifyResponse = await apiRequest(ctx, {
|
|
335
|
+
method: "POST",
|
|
336
|
+
pathname: "/x402/gate/verify",
|
|
337
|
+
idempotencyKey: `${idPrefix}_verify`,
|
|
338
|
+
body: {
|
|
339
|
+
gateId,
|
|
340
|
+
verificationStatus: "green",
|
|
341
|
+
runStatus: "completed",
|
|
342
|
+
evidenceRefs: [`http:request_sha256:${requestSha256}`, `http:response_sha256:${responseSha256}`]
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
expectStatus(verifyResponse, 200, `verify gate ${gateId}`);
|
|
346
|
+
const receiptId = normalizeOptionalString(verifyResponse.json?.settlementReceipt?.receiptId);
|
|
347
|
+
if (!receiptId) throw new Error(`verify gate ${gateId} failed: settlementReceipt.receiptId missing`);
|
|
348
|
+
|
|
349
|
+
const receiptResponse = await apiRequest(ctx, {
|
|
350
|
+
method: "GET",
|
|
351
|
+
pathname: `/x402/receipts/${encodeURIComponent(receiptId)}`
|
|
352
|
+
});
|
|
353
|
+
expectStatus(receiptResponse, 200, `get receipt ${receiptId}`);
|
|
354
|
+
const resolvedReceiptId = normalizeOptionalString(receiptResponse.json?.receipt?.receiptId);
|
|
355
|
+
if (resolvedReceiptId !== receiptId) {
|
|
356
|
+
throw new Error(`receipt lookup mismatch: expected ${receiptId}, got ${resolvedReceiptId ?? "null"}`);
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
receiptId,
|
|
360
|
+
requestSha256,
|
|
361
|
+
responseSha256
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function evaluateChecks(checks) {
|
|
366
|
+
const rows = Array.isArray(checks) ? checks : [];
|
|
367
|
+
const passedChecks = rows.filter((row) => row?.ok === true).length;
|
|
368
|
+
return {
|
|
369
|
+
ok: rows.length > 0 && rows.length === passedChecks,
|
|
370
|
+
passedChecks,
|
|
371
|
+
requiredChecks: rows.length
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function runX402HitlSmoke(args) {
|
|
376
|
+
const startedAt = new Date().toISOString();
|
|
377
|
+
const runToken = randomUUID().replaceAll("-", "").slice(0, 16);
|
|
378
|
+
const report = {
|
|
379
|
+
schemaVersion: X402_HITL_SMOKE_SCHEMA_VERSION,
|
|
380
|
+
ok: false,
|
|
381
|
+
startedAt,
|
|
382
|
+
completedAt: null,
|
|
383
|
+
config: {
|
|
384
|
+
baseUrl: args.baseUrl,
|
|
385
|
+
tenantId: args.tenantId,
|
|
386
|
+
protocol: args.protocol,
|
|
387
|
+
usedMintedApiKey: false
|
|
388
|
+
},
|
|
389
|
+
approveFlow: {},
|
|
390
|
+
denyFlow: {},
|
|
391
|
+
checks: [],
|
|
392
|
+
errors: []
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const ctx = {
|
|
396
|
+
baseUrl: args.baseUrl,
|
|
397
|
+
tenantId: args.tenantId,
|
|
398
|
+
protocol: args.protocol,
|
|
399
|
+
apiKey: args.apiKey
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
if (!ctx.apiKey) {
|
|
404
|
+
ctx.apiKey = await mintApiKey({
|
|
405
|
+
baseUrl: args.baseUrl,
|
|
406
|
+
tenantId: args.tenantId,
|
|
407
|
+
protocol: args.protocol,
|
|
408
|
+
opsToken: args.opsToken
|
|
409
|
+
});
|
|
410
|
+
report.config.usedMintedApiKey = true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const payerAgentId = `agt_hitl_payer_${runToken}`;
|
|
414
|
+
const payeeAgentId = `agt_hitl_payee_${runToken}`;
|
|
415
|
+
await registerAgent(ctx, { agentId: payerAgentId, idem: `agent_register_payer_${runToken}` });
|
|
416
|
+
await registerAgent(ctx, { agentId: payeeAgentId, idem: `agent_register_payee_${runToken}` });
|
|
417
|
+
await creditWallet(ctx, { agentId: payerAgentId, amountCents: 20_000, idem: `wallet_credit_${runToken}` });
|
|
418
|
+
|
|
419
|
+
const walletPolicyDraft = {
|
|
420
|
+
schemaVersion: "X402WalletPolicy.v1",
|
|
421
|
+
sponsorRef: `sponsor_hitl_${runToken}`,
|
|
422
|
+
sponsorWalletRef: `wallet_hitl_${runToken}`,
|
|
423
|
+
policyRef: `policy_hitl_${runToken}`,
|
|
424
|
+
policyVersion: 1,
|
|
425
|
+
status: "active",
|
|
426
|
+
maxAmountCents: 1000,
|
|
427
|
+
maxDailyAuthorizationCents: 300,
|
|
428
|
+
allowedProviderIds: [payeeAgentId],
|
|
429
|
+
allowedToolIds: ["weather_read"],
|
|
430
|
+
allowedCurrencies: ["USD"],
|
|
431
|
+
allowedReversalActions: ["request_refund", "resolve_refund", "void_authorization"],
|
|
432
|
+
requireQuote: false,
|
|
433
|
+
requireStrictRequestBinding: false,
|
|
434
|
+
requireAgentKeyMatch: false
|
|
435
|
+
};
|
|
436
|
+
const walletPolicy = await upsertWalletPolicy(ctx, {
|
|
437
|
+
policy: walletPolicyDraft,
|
|
438
|
+
idem: `wallet_policy_upsert_${runToken}`
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const gateApprove = `gate_hitl_approve_${runToken}`;
|
|
442
|
+
const gateBaseline = `gate_hitl_baseline_${runToken}`;
|
|
443
|
+
const gateDeny = `gate_hitl_deny_${runToken}`;
|
|
444
|
+
await createGate(ctx, { gateId: gateApprove, payerAgentId, payeeAgentId, amountCents: 300, walletPolicy });
|
|
445
|
+
await createGate(ctx, { gateId: gateBaseline, payerAgentId, payeeAgentId, amountCents: 200, walletPolicy });
|
|
446
|
+
await createGate(ctx, { gateId: gateDeny, payerAgentId, payeeAgentId, amountCents: 150, walletPolicy });
|
|
447
|
+
|
|
448
|
+
const decisionApprove = await issueWalletDecision(ctx, {
|
|
449
|
+
sponsorWalletRef: walletPolicy.sponsorWalletRef,
|
|
450
|
+
gateId: gateApprove,
|
|
451
|
+
idem: `wallet_decision_approve_${runToken}`
|
|
452
|
+
});
|
|
453
|
+
const decisionBaseline = await issueWalletDecision(ctx, {
|
|
454
|
+
sponsorWalletRef: walletPolicy.sponsorWalletRef,
|
|
455
|
+
gateId: gateBaseline,
|
|
456
|
+
idem: `wallet_decision_baseline_${runToken}`
|
|
457
|
+
});
|
|
458
|
+
const decisionDeny = await issueWalletDecision(ctx, {
|
|
459
|
+
sponsorWalletRef: walletPolicy.sponsorWalletRef,
|
|
460
|
+
gateId: gateDeny,
|
|
461
|
+
idem: `wallet_decision_deny_${runToken}`
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const baselineAuth = await authorizeGate(ctx, {
|
|
465
|
+
gateId: gateBaseline,
|
|
466
|
+
walletAuthorizationDecisionToken: decisionBaseline,
|
|
467
|
+
idem: `authorize_baseline_${runToken}`
|
|
468
|
+
});
|
|
469
|
+
expectStatus(baselineAuth, 200, "authorize baseline gate");
|
|
470
|
+
|
|
471
|
+
const blockedApprove = await authorizeGate(ctx, {
|
|
472
|
+
gateId: gateApprove,
|
|
473
|
+
walletAuthorizationDecisionToken: decisionApprove,
|
|
474
|
+
idem: `authorize_approve_blocked_${runToken}`
|
|
475
|
+
});
|
|
476
|
+
const approveEscalationId = expectEscalationRequired(blockedApprove, "approve flow blocked authorization");
|
|
477
|
+
|
|
478
|
+
const approveResolution = await resolveEscalation(ctx, {
|
|
479
|
+
escalationId: approveEscalationId,
|
|
480
|
+
action: "approve",
|
|
481
|
+
idem: `resolve_approve_${runToken}`,
|
|
482
|
+
reason: "operator emergency approval for smoke validation"
|
|
483
|
+
});
|
|
484
|
+
const approveOverrideToken = normalizeOptionalString(approveResolution.json?.escalationOverrideToken);
|
|
485
|
+
const approveDecisionToken = normalizeOptionalString(approveResolution.json?.walletAuthorizationDecisionToken);
|
|
486
|
+
if (!approveOverrideToken || !approveDecisionToken) {
|
|
487
|
+
throw new Error("approve escalation did not return required override/decision tokens");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const resumedApprove = await authorizeGate(ctx, {
|
|
491
|
+
gateId: gateApprove,
|
|
492
|
+
walletAuthorizationDecisionToken: approveDecisionToken,
|
|
493
|
+
escalationOverrideToken: approveOverrideToken,
|
|
494
|
+
idem: `authorize_approve_resumed_${runToken}`
|
|
495
|
+
});
|
|
496
|
+
expectStatus(resumedApprove, 200, "resume approved escalation authorization");
|
|
497
|
+
const receipt = await verifyGateAndFetchReceipt(ctx, {
|
|
498
|
+
gateId: gateApprove,
|
|
499
|
+
idPrefix: `hitl_${runToken}`
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
report.approveFlow = {
|
|
503
|
+
gateId: gateApprove,
|
|
504
|
+
escalationId: approveEscalationId,
|
|
505
|
+
receiptId: receipt.receiptId,
|
|
506
|
+
requestSha256: receipt.requestSha256,
|
|
507
|
+
responseSha256: receipt.responseSha256
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const blockedDeny = await authorizeGate(ctx, {
|
|
511
|
+
gateId: gateDeny,
|
|
512
|
+
walletAuthorizationDecisionToken: decisionDeny,
|
|
513
|
+
idem: `authorize_deny_blocked_${runToken}`
|
|
514
|
+
});
|
|
515
|
+
const denyEscalationId = expectEscalationRequired(blockedDeny, "deny flow blocked authorization");
|
|
516
|
+
|
|
517
|
+
const denyResolution = await resolveEscalation(ctx, {
|
|
518
|
+
escalationId: denyEscalationId,
|
|
519
|
+
action: "deny",
|
|
520
|
+
idem: `resolve_deny_${runToken}`,
|
|
521
|
+
reason: "operator denied escalation for smoke validation"
|
|
522
|
+
});
|
|
523
|
+
const denyStatus = normalizeOptionalString(denyResolution.json?.escalation?.status);
|
|
524
|
+
if (denyStatus !== "denied") {
|
|
525
|
+
throw new Error(`deny escalation failed: expected status denied, got ${denyStatus ?? "null"}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const retryAfterDeny = await authorizeGate(ctx, {
|
|
529
|
+
gateId: gateDeny,
|
|
530
|
+
walletAuthorizationDecisionToken: decisionDeny,
|
|
531
|
+
idem: `authorize_deny_retry_${runToken}`
|
|
532
|
+
});
|
|
533
|
+
const retryBlocked = retryAfterDeny.status === 409;
|
|
534
|
+
if (!retryBlocked) {
|
|
535
|
+
throw new Error(`deny retry expected HTTP 409, got ${retryAfterDeny.status} (${toErrorDetails(retryAfterDeny)})`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
report.denyFlow = {
|
|
539
|
+
gateId: gateDeny,
|
|
540
|
+
escalationId: denyEscalationId,
|
|
541
|
+
postDenyAuthorizeStatus: retryAfterDeny.status,
|
|
542
|
+
postDenyAuthorizeCode: normalizeOptionalString(retryAfterDeny.json?.code)
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
report.checks = [
|
|
546
|
+
check("baseline_authorization_ok", baselineAuth.status === 200, { gateId: gateBaseline }),
|
|
547
|
+
check("approve_flow_escalation_created", Boolean(approveEscalationId), { escalationId: approveEscalationId }),
|
|
548
|
+
check("approve_flow_resumed_authorization_ok", resumedApprove.status === 200, { gateId: gateApprove }),
|
|
549
|
+
check("approve_flow_receipt_fetched", Boolean(report.approveFlow.receiptId), { receiptId: report.approveFlow.receiptId }),
|
|
550
|
+
check("deny_flow_escalation_created", Boolean(denyEscalationId), { escalationId: denyEscalationId }),
|
|
551
|
+
check("deny_flow_resolution_denied", denyStatus === "denied", { escalationId: denyEscalationId }),
|
|
552
|
+
check("deny_flow_retry_remains_blocked", retryBlocked, {
|
|
553
|
+
status: retryAfterDeny.status,
|
|
554
|
+
code: normalizeOptionalString(retryAfterDeny.json?.code)
|
|
555
|
+
})
|
|
556
|
+
];
|
|
557
|
+
} catch (err) {
|
|
558
|
+
report.errors.push({
|
|
559
|
+
message: err?.message ?? String(err ?? "")
|
|
560
|
+
});
|
|
561
|
+
} finally {
|
|
562
|
+
report.completedAt = new Date().toISOString();
|
|
563
|
+
report.verdict = evaluateChecks(report.checks);
|
|
564
|
+
report.ok = report.errors.length === 0 && report.verdict.ok === true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const outPath = writeJson(args.outPath, report);
|
|
568
|
+
return { report, outPath };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function main() {
|
|
572
|
+
const args = parseArgs(process.argv.slice(2));
|
|
573
|
+
if (args.help) {
|
|
574
|
+
process.stdout.write(`${usage()}\n`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const { report, outPath } = await runX402HitlSmoke(args);
|
|
578
|
+
process.stdout.write(
|
|
579
|
+
`${JSON.stringify(
|
|
580
|
+
{
|
|
581
|
+
ok: report.ok,
|
|
582
|
+
outPath,
|
|
583
|
+
approveFlow: report.approveFlow,
|
|
584
|
+
denyFlow: report.denyFlow,
|
|
585
|
+
verdict: report.verdict
|
|
586
|
+
},
|
|
587
|
+
null,
|
|
588
|
+
2
|
|
589
|
+
)}\n`
|
|
590
|
+
);
|
|
591
|
+
if (!report.ok) process.exitCode = 1;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const isDirectExecution = (() => {
|
|
595
|
+
try {
|
|
596
|
+
return import.meta.url === new URL(`file://${process.argv[1]}`).href;
|
|
597
|
+
} catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
})();
|
|
601
|
+
|
|
602
|
+
if (isDirectExecution) {
|
|
603
|
+
main().catch((err) => {
|
|
604
|
+
process.stderr.write(`${err?.stack ?? err?.message ?? String(err ?? "")}\n`);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
});
|
|
607
|
+
}
|