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,600 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { canonicalJsonStringify, normalizeForCanonicalJson } from "../../src/core/canonical-json.js";
|
|
6
|
+
import { sha256Hex } from "../../src/core/crypto.js";
|
|
7
|
+
import { createStarterPolicyPack, listPolicyPackTemplates, POLICY_PACK_SCHEMA_VERSION } from "../../src/core/policy-packs.js";
|
|
8
|
+
|
|
9
|
+
const VALIDATION_REPORT_SCHEMA_VERSION = "SettldPolicyPackValidationReport.v1";
|
|
10
|
+
const SIMULATION_REPORT_SCHEMA_VERSION = "SettldPolicySimulationReport.v1";
|
|
11
|
+
const PUBLISH_REPORT_SCHEMA_VERSION = "SettldPolicyPublishReport.v1";
|
|
12
|
+
const PUBLICATION_ARTIFACT_SCHEMA_VERSION = "SettldPolicyPublication.v1";
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
const lines = [
|
|
16
|
+
"usage:",
|
|
17
|
+
" settld policy init <pack-id> [--out <path>] [--force] [--format json|text] [--json-out <path>]",
|
|
18
|
+
" settld policy simulate <policy-pack.json|-> [--scenario <scenario.json|->|--scenario-json <json>] [--format json|text] [--json-out <path>]",
|
|
19
|
+
" settld policy publish <policy-pack.json|-> [--out <path>] [--force] [--channel <name>] [--owner <id>] [--format json|text] [--json-out <path>]"
|
|
20
|
+
];
|
|
21
|
+
process.stderr.write(`${lines.join("\n")}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fail(message) {
|
|
25
|
+
throw new Error(String(message));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const out = {
|
|
30
|
+
command: String(argv[0] ?? "").trim() || null,
|
|
31
|
+
packId: null,
|
|
32
|
+
inputPath: null,
|
|
33
|
+
scenarioPath: null,
|
|
34
|
+
scenarioJson: null,
|
|
35
|
+
outPath: null,
|
|
36
|
+
jsonOut: null,
|
|
37
|
+
format: "text",
|
|
38
|
+
force: false,
|
|
39
|
+
help: false,
|
|
40
|
+
channel: null,
|
|
41
|
+
owner: null
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
45
|
+
const arg = String(argv[i] ?? "").trim();
|
|
46
|
+
if (!arg) continue;
|
|
47
|
+
|
|
48
|
+
if (arg === "--help" || arg === "-h") {
|
|
49
|
+
out.help = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg === "--format") {
|
|
53
|
+
out.format = String(argv[i + 1] ?? "").trim().toLowerCase();
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (arg === "--json-out") {
|
|
58
|
+
out.jsonOut = String(argv[i + 1] ?? "").trim();
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === "--out") {
|
|
63
|
+
out.outPath = String(argv[i + 1] ?? "").trim();
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg === "--force") {
|
|
68
|
+
out.force = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg === "--in") {
|
|
72
|
+
out.inputPath = String(argv[i + 1] ?? "").trim();
|
|
73
|
+
i += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--scenario") {
|
|
77
|
+
out.scenarioPath = String(argv[i + 1] ?? "").trim();
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === "--scenario-json") {
|
|
82
|
+
out.scenarioJson = String(argv[i + 1] ?? "").trim();
|
|
83
|
+
i += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--channel") {
|
|
87
|
+
out.channel = String(argv[i + 1] ?? "").trim();
|
|
88
|
+
i += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === "--owner") {
|
|
92
|
+
out.owner = String(argv[i + 1] ?? "").trim();
|
|
93
|
+
i += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (arg !== "-" && arg.startsWith("-")) fail(`unknown argument: ${arg}`);
|
|
98
|
+
|
|
99
|
+
if (out.command === "init" && !out.packId) {
|
|
100
|
+
out.packId = arg;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if ((out.command === "simulate" || out.command === "publish") && !out.inputPath) {
|
|
104
|
+
out.inputPath = arg;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
fail(`unexpected positional argument: ${arg}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!out.command || out.command === "--help" || out.command === "-h") {
|
|
111
|
+
out.help = true;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
if (!["init", "simulate", "publish"].includes(out.command)) fail(`unsupported command: ${out.command}`);
|
|
115
|
+
if (out.format !== "json" && out.format !== "text") fail("--format must be json or text");
|
|
116
|
+
if (out.command === "init" && !out.packId) fail("pack id is required for init");
|
|
117
|
+
if ((out.command === "simulate" || out.command === "publish") && !out.inputPath) fail("policy pack input is required");
|
|
118
|
+
if (out.command !== "simulate" && (out.scenarioPath || out.scenarioJson)) fail("--scenario/--scenario-json only apply to simulate");
|
|
119
|
+
if (out.scenarioPath && out.scenarioJson) fail("choose one of --scenario or --scenario-json");
|
|
120
|
+
const publishOptionsUsed = out.channel !== null || out.owner !== null;
|
|
121
|
+
if (out.command !== "publish" && publishOptionsUsed) fail("--channel/--owner only apply to publish");
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isPlainObject(value) {
|
|
126
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
127
|
+
const proto = Object.getPrototypeOf(value);
|
|
128
|
+
return proto === Object.prototype || proto === null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function addValidationError(report, { code, path: errorPath, message }) {
|
|
132
|
+
report.errors.push({ code: String(code), path: String(errorPath), message: String(message) });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateString(report, value, fieldPath) {
|
|
136
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
137
|
+
addValidationError(report, { code: "invalid_string", path: fieldPath, message: "must be a non-empty string" });
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validateSafeInt(report, value, fieldPath, { min = 0 } = {}) {
|
|
144
|
+
if (!Number.isSafeInteger(value) || value < min) {
|
|
145
|
+
addValidationError(report, { code: "invalid_integer", path: fieldPath, message: `must be a safe integer >= ${min}` });
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateStringArray(report, value, fieldPath) {
|
|
152
|
+
if (!Array.isArray(value)) {
|
|
153
|
+
addValidationError(report, { code: "invalid_array", path: fieldPath, message: "must be an array of strings" });
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
let ok = true;
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
159
|
+
const item = value[i];
|
|
160
|
+
const itemPath = `${fieldPath}[${i}]`;
|
|
161
|
+
if (!validateString(report, item, itemPath)) {
|
|
162
|
+
ok = false;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (seen.has(item)) {
|
|
166
|
+
addValidationError(report, { code: "duplicate_value", path: itemPath, message: "must not contain duplicates" });
|
|
167
|
+
ok = false;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
seen.add(item);
|
|
171
|
+
}
|
|
172
|
+
return ok;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validatePolicyPackDocument(policyPack) {
|
|
176
|
+
const report = {
|
|
177
|
+
schemaVersion: VALIDATION_REPORT_SCHEMA_VERSION,
|
|
178
|
+
ok: false,
|
|
179
|
+
packId: typeof policyPack?.packId === "string" ? policyPack.packId : null,
|
|
180
|
+
errors: [],
|
|
181
|
+
warnings: []
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (!isPlainObject(policyPack)) {
|
|
185
|
+
addValidationError(report, { code: "invalid_policy_pack", path: "$", message: "policy pack must be an object" });
|
|
186
|
+
return report;
|
|
187
|
+
}
|
|
188
|
+
if (policyPack.schemaVersion !== POLICY_PACK_SCHEMA_VERSION) {
|
|
189
|
+
addValidationError(report, {
|
|
190
|
+
code: "unsupported_schema_version",
|
|
191
|
+
path: "$.schemaVersion",
|
|
192
|
+
message: `must be ${POLICY_PACK_SCHEMA_VERSION}`
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
validateString(report, policyPack.packId, "$.packId");
|
|
197
|
+
if (!isPlainObject(policyPack.metadata)) {
|
|
198
|
+
addValidationError(report, { code: "invalid_metadata", path: "$.metadata", message: "metadata must be an object" });
|
|
199
|
+
} else {
|
|
200
|
+
validateString(report, policyPack.metadata.name, "$.metadata.name");
|
|
201
|
+
validateString(report, policyPack.metadata.vertical, "$.metadata.vertical");
|
|
202
|
+
validateString(report, policyPack.metadata.description, "$.metadata.description");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!isPlainObject(policyPack.policy)) {
|
|
206
|
+
addValidationError(report, { code: "invalid_policy", path: "$.policy", message: "policy must be an object" });
|
|
207
|
+
return report;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
validateString(report, policyPack.policy.currency, "$.policy.currency");
|
|
211
|
+
if (!isPlainObject(policyPack.policy.limits)) {
|
|
212
|
+
addValidationError(report, { code: "invalid_limits", path: "$.policy.limits", message: "limits must be an object" });
|
|
213
|
+
} else {
|
|
214
|
+
validateSafeInt(report, policyPack.policy.limits.perRequestUsdCents, "$.policy.limits.perRequestUsdCents", { min: 1 });
|
|
215
|
+
validateSafeInt(report, policyPack.policy.limits.monthlyUsdCents, "$.policy.limits.monthlyUsdCents", { min: 1 });
|
|
216
|
+
if (
|
|
217
|
+
Number.isSafeInteger(policyPack.policy.limits.perRequestUsdCents) &&
|
|
218
|
+
Number.isSafeInteger(policyPack.policy.limits.monthlyUsdCents) &&
|
|
219
|
+
policyPack.policy.limits.perRequestUsdCents > policyPack.policy.limits.monthlyUsdCents
|
|
220
|
+
) {
|
|
221
|
+
addValidationError(report, {
|
|
222
|
+
code: "limits_inconsistent",
|
|
223
|
+
path: "$.policy.limits",
|
|
224
|
+
message: "perRequestUsdCents must be <= monthlyUsdCents"
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!isPlainObject(policyPack.policy.allowlists)) {
|
|
230
|
+
addValidationError(report, { code: "invalid_allowlists", path: "$.policy.allowlists", message: "allowlists must be an object" });
|
|
231
|
+
} else {
|
|
232
|
+
validateStringArray(report, policyPack.policy.allowlists.providers, "$.policy.allowlists.providers");
|
|
233
|
+
validateStringArray(report, policyPack.policy.allowlists.tools, "$.policy.allowlists.tools");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!Array.isArray(policyPack.policy.approvals) || policyPack.policy.approvals.length === 0) {
|
|
237
|
+
addValidationError(report, { code: "invalid_approvals", path: "$.policy.approvals", message: "approvals must be a non-empty array" });
|
|
238
|
+
} else {
|
|
239
|
+
let previousMax = -1;
|
|
240
|
+
for (let i = 0; i < policyPack.policy.approvals.length; i += 1) {
|
|
241
|
+
const tier = policyPack.policy.approvals[i];
|
|
242
|
+
const tierPath = `$.policy.approvals[${i}]`;
|
|
243
|
+
if (!isPlainObject(tier)) {
|
|
244
|
+
addValidationError(report, { code: "invalid_approval_tier", path: tierPath, message: "tier must be an object" });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
validateString(report, tier.tierId, `${tierPath}.tierId`);
|
|
248
|
+
validateSafeInt(report, tier.maxAmountUsdCents, `${tierPath}.maxAmountUsdCents`, { min: 0 });
|
|
249
|
+
validateSafeInt(report, tier.requiredApprovers, `${tierPath}.requiredApprovers`, { min: 0 });
|
|
250
|
+
validateString(report, tier.approverRole, `${tierPath}.approverRole`);
|
|
251
|
+
if (Number.isSafeInteger(tier.maxAmountUsdCents) && tier.maxAmountUsdCents <= previousMax) {
|
|
252
|
+
addValidationError(report, {
|
|
253
|
+
code: "tier_order_invalid",
|
|
254
|
+
path: `${tierPath}.maxAmountUsdCents`,
|
|
255
|
+
message: "maxAmountUsdCents must increase monotonically"
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (Number.isSafeInteger(tier.maxAmountUsdCents)) previousMax = tier.maxAmountUsdCents;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!isPlainObject(policyPack.policy.enforcement)) {
|
|
263
|
+
addValidationError(report, { code: "invalid_enforcement", path: "$.policy.enforcement", message: "enforcement must be an object" });
|
|
264
|
+
} else {
|
|
265
|
+
const boolKeys = ["enforceProviderAllowlist", "requireReceiptSignature", "requireToolManifestHash", "allowUnknownToolVersion"];
|
|
266
|
+
for (const key of boolKeys) {
|
|
267
|
+
if (typeof policyPack.policy.enforcement[key] !== "boolean") {
|
|
268
|
+
addValidationError(report, { code: "invalid_boolean", path: `$.policy.enforcement.${key}`, message: "must be a boolean" });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!isPlainObject(policyPack.policy.disputeDefaults)) {
|
|
274
|
+
addValidationError(report, {
|
|
275
|
+
code: "invalid_dispute_defaults",
|
|
276
|
+
path: "$.policy.disputeDefaults",
|
|
277
|
+
message: "disputeDefaults must be an object"
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
validateSafeInt(report, policyPack.policy.disputeDefaults.responseWindowHours, "$.policy.disputeDefaults.responseWindowHours", { min: 1 });
|
|
281
|
+
if (typeof policyPack.policy.disputeDefaults.autoOpenIfReceiptMissing !== "boolean") {
|
|
282
|
+
addValidationError(report, {
|
|
283
|
+
code: "invalid_boolean",
|
|
284
|
+
path: "$.policy.disputeDefaults.autoOpenIfReceiptMissing",
|
|
285
|
+
message: "must be a boolean"
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
validateStringArray(report, policyPack.policy.disputeDefaults.evidenceChecklist, "$.policy.disputeDefaults.evidenceChecklist");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
report.ok = report.errors.length === 0;
|
|
292
|
+
return report;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function buildDefaultScenario(policyPack) {
|
|
296
|
+
const providers = Array.isArray(policyPack?.policy?.allowlists?.providers) ? policyPack.policy.allowlists.providers : [];
|
|
297
|
+
const tools = Array.isArray(policyPack?.policy?.allowlists?.tools) ? policyPack.policy.allowlists.tools : [];
|
|
298
|
+
return {
|
|
299
|
+
providerId: providers[0] ?? "",
|
|
300
|
+
toolId: tools[0] ?? "",
|
|
301
|
+
amountUsdCents: 0,
|
|
302
|
+
monthToDateSpendUsdCents: 0,
|
|
303
|
+
approvalsProvided: 0,
|
|
304
|
+
receiptSigned: true,
|
|
305
|
+
toolManifestHashPresent: true,
|
|
306
|
+
toolVersionKnown: true
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeScenario(raw, { policyPack }) {
|
|
311
|
+
if (!isPlainObject(raw)) fail("scenario must be a JSON object");
|
|
312
|
+
const fallback = buildDefaultScenario(policyPack);
|
|
313
|
+
const out = {
|
|
314
|
+
providerId: String(raw.providerId ?? fallback.providerId).trim(),
|
|
315
|
+
toolId: String(raw.toolId ?? fallback.toolId).trim(),
|
|
316
|
+
amountUsdCents: Number(raw.amountUsdCents ?? fallback.amountUsdCents),
|
|
317
|
+
monthToDateSpendUsdCents: Number(raw.monthToDateSpendUsdCents ?? fallback.monthToDateSpendUsdCents),
|
|
318
|
+
approvalsProvided: Number(raw.approvalsProvided ?? fallback.approvalsProvided),
|
|
319
|
+
receiptSigned: raw.receiptSigned === undefined ? fallback.receiptSigned : Boolean(raw.receiptSigned),
|
|
320
|
+
toolManifestHashPresent: raw.toolManifestHashPresent === undefined ? fallback.toolManifestHashPresent : Boolean(raw.toolManifestHashPresent),
|
|
321
|
+
toolVersionKnown: raw.toolVersionKnown === undefined ? fallback.toolVersionKnown : Boolean(raw.toolVersionKnown)
|
|
322
|
+
};
|
|
323
|
+
if (!out.providerId) fail("scenario.providerId is required");
|
|
324
|
+
if (!out.toolId) fail("scenario.toolId is required");
|
|
325
|
+
if (!Number.isSafeInteger(out.amountUsdCents) || out.amountUsdCents < 0) fail("scenario.amountUsdCents must be a safe integer >= 0");
|
|
326
|
+
if (!Number.isSafeInteger(out.monthToDateSpendUsdCents) || out.monthToDateSpendUsdCents < 0) {
|
|
327
|
+
fail("scenario.monthToDateSpendUsdCents must be a safe integer >= 0");
|
|
328
|
+
}
|
|
329
|
+
if (!Number.isSafeInteger(out.approvalsProvided) || out.approvalsProvided < 0) fail("scenario.approvalsProvided must be a safe integer >= 0");
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function findApprovalTier(policyPack, amountUsdCents) {
|
|
334
|
+
const tiers = policyPack.policy.approvals;
|
|
335
|
+
for (const tier of tiers) {
|
|
336
|
+
if (amountUsdCents <= tier.maxAmountUsdCents) return tier;
|
|
337
|
+
}
|
|
338
|
+
return tiers[tiers.length - 1];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function simulatePolicyPack({ policyPack, scenario }) {
|
|
342
|
+
const allowlists = policyPack.policy.allowlists;
|
|
343
|
+
const limits = policyPack.policy.limits;
|
|
344
|
+
const enforcement = policyPack.policy.enforcement;
|
|
345
|
+
const tier = findApprovalTier(policyPack, scenario.amountUsdCents);
|
|
346
|
+
|
|
347
|
+
const checks = [
|
|
348
|
+
{ id: "provider_allowlisted", ok: enforcement.enforceProviderAllowlist ? allowlists.providers.includes(scenario.providerId) : true },
|
|
349
|
+
{ id: "tool_allowlisted", ok: allowlists.tools.includes(scenario.toolId) },
|
|
350
|
+
{ id: "per_request_limit", ok: scenario.amountUsdCents <= limits.perRequestUsdCents },
|
|
351
|
+
{ id: "monthly_limit", ok: scenario.amountUsdCents + scenario.monthToDateSpendUsdCents <= limits.monthlyUsdCents },
|
|
352
|
+
{ id: "receipt_signature", ok: enforcement.requireReceiptSignature ? scenario.receiptSigned : true },
|
|
353
|
+
{ id: "tool_manifest_hash", ok: enforcement.requireToolManifestHash ? scenario.toolManifestHashPresent : true },
|
|
354
|
+
{ id: "tool_version_known", ok: enforcement.allowUnknownToolVersion ? true : scenario.toolVersionKnown }
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
const checksOk = checks.every((item) => item.ok === true);
|
|
358
|
+
const requiredApprovers = tier.requiredApprovers;
|
|
359
|
+
const approvalsSatisfied = scenario.approvalsProvided >= requiredApprovers;
|
|
360
|
+
const reasons = [];
|
|
361
|
+
for (const check of checks) {
|
|
362
|
+
if (check.ok !== true) reasons.push(check.id);
|
|
363
|
+
}
|
|
364
|
+
if (checksOk && !approvalsSatisfied) reasons.push("approval_required");
|
|
365
|
+
const decision = !checksOk ? "deny" : approvalsSatisfied ? "allow" : "challenge";
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
schemaVersion: SIMULATION_REPORT_SCHEMA_VERSION,
|
|
369
|
+
ok: true,
|
|
370
|
+
packId: policyPack.packId,
|
|
371
|
+
decision,
|
|
372
|
+
requiredApprovers,
|
|
373
|
+
approvalsProvided: scenario.approvalsProvided,
|
|
374
|
+
selectedApprovalTier: tier.tierId,
|
|
375
|
+
reasons,
|
|
376
|
+
checks,
|
|
377
|
+
scenario
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function toTrimmedOrNull(value) {
|
|
382
|
+
if (value === null || value === undefined) return null;
|
|
383
|
+
const normalized = String(value).trim();
|
|
384
|
+
return normalized || null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildPublicationArtifact(policyPack, { channel, owner }) {
|
|
388
|
+
const normalizedPack = normalizeForCanonicalJson(policyPack, { path: "$" });
|
|
389
|
+
const packCanonical = canonicalJsonStringify(normalizedPack);
|
|
390
|
+
const policyFingerprint = sha256Hex(packCanonical);
|
|
391
|
+
const publicationRef = `${channel}:${policyPack.packId}:${policyFingerprint.slice(0, 16)}`;
|
|
392
|
+
const artifact = {
|
|
393
|
+
schemaVersion: PUBLICATION_ARTIFACT_SCHEMA_VERSION,
|
|
394
|
+
publicationRef,
|
|
395
|
+
channel,
|
|
396
|
+
owner,
|
|
397
|
+
packId: policyPack.packId,
|
|
398
|
+
policySchemaVersion: policyPack.schemaVersion,
|
|
399
|
+
policyFingerprint,
|
|
400
|
+
metadata: policyPack.metadata,
|
|
401
|
+
policy: policyPack.policy,
|
|
402
|
+
checksums: {
|
|
403
|
+
policyPackCanonicalSha256: policyFingerprint
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
return { policyFingerprint, publicationRef, artifact };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function exists(filePath) {
|
|
410
|
+
try {
|
|
411
|
+
await fs.stat(filePath);
|
|
412
|
+
return true;
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function readJsonPath(pathLike) {
|
|
419
|
+
if (pathLike === "-") {
|
|
420
|
+
const chunks = [];
|
|
421
|
+
for await (const chunk of process.stdin) {
|
|
422
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"));
|
|
423
|
+
}
|
|
424
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
425
|
+
}
|
|
426
|
+
return JSON.parse(await fs.readFile(pathLike, "utf8"));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function writeOutput({ format, payload, text, jsonOut }) {
|
|
430
|
+
const jsonBody = `${JSON.stringify(payload, null, 2)}\n`;
|
|
431
|
+
if (jsonOut) {
|
|
432
|
+
const target = path.resolve(process.cwd(), jsonOut);
|
|
433
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
434
|
+
await fs.writeFile(target, jsonBody, "utf8");
|
|
435
|
+
}
|
|
436
|
+
if (format === "json") {
|
|
437
|
+
process.stdout.write(jsonBody);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
process.stdout.write(text);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderValidationText(report) {
|
|
444
|
+
if (report.ok) return "ok\n";
|
|
445
|
+
return `${report.errors.map((row) => `${row.code}\t${row.path}\t${row.message}`).join("\n")}\n`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function renderSimulationText(report) {
|
|
449
|
+
const reasonText = report.reasons.length ? report.reasons.join(",") : "none";
|
|
450
|
+
return [
|
|
451
|
+
`decision: ${report.decision}`,
|
|
452
|
+
`requiredApprovers: ${report.requiredApprovers}`,
|
|
453
|
+
`approvalsProvided: ${report.approvalsProvided}`,
|
|
454
|
+
`reasons: ${reasonText}`
|
|
455
|
+
].join("\n") + "\n";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function renderPublishText(report) {
|
|
459
|
+
return [
|
|
460
|
+
`ok: ${report.ok ? "true" : "false"}`,
|
|
461
|
+
`packId: ${report.packId}`,
|
|
462
|
+
`publicationRef: ${report.publicationRef}`,
|
|
463
|
+
`policyFingerprint: ${report.policyFingerprint}`,
|
|
464
|
+
`artifactPath: ${report.artifactPath}`,
|
|
465
|
+
`artifactSha256: ${report.artifactSha256}`
|
|
466
|
+
].join("\n") + "\n";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function formatKnownPackIds() {
|
|
470
|
+
return listPolicyPackTemplates()
|
|
471
|
+
.map((pack) => pack.packId)
|
|
472
|
+
.sort()
|
|
473
|
+
.join(", ");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function handleInit(parsed) {
|
|
477
|
+
const policyPack = createStarterPolicyPack({ packId: parsed.packId });
|
|
478
|
+
if (!policyPack) fail(`unknown policy pack: ${parsed.packId} (known: ${formatKnownPackIds()})`);
|
|
479
|
+
const targetPath = path.resolve(process.cwd(), parsed.outPath || `${parsed.packId}.policy-pack.json`);
|
|
480
|
+
if (!parsed.force && (await exists(targetPath))) fail(`output path exists: ${targetPath}`);
|
|
481
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
482
|
+
await fs.writeFile(targetPath, `${JSON.stringify(policyPack, null, 2)}\n`, "utf8");
|
|
483
|
+
|
|
484
|
+
const payload = {
|
|
485
|
+
ok: true,
|
|
486
|
+
command: "init",
|
|
487
|
+
packId: parsed.packId,
|
|
488
|
+
outPath: targetPath
|
|
489
|
+
};
|
|
490
|
+
await writeOutput({
|
|
491
|
+
format: parsed.format,
|
|
492
|
+
payload,
|
|
493
|
+
text: `ok\t${parsed.packId}\t${targetPath}\n`,
|
|
494
|
+
jsonOut: parsed.jsonOut
|
|
495
|
+
});
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function loadScenario(parsed, policyPack) {
|
|
500
|
+
if (parsed.scenarioJson) return normalizeScenario(JSON.parse(parsed.scenarioJson), { policyPack });
|
|
501
|
+
if (parsed.scenarioPath) return normalizeScenario(await readJsonPath(parsed.scenarioPath), { policyPack });
|
|
502
|
+
return normalizeScenario(buildDefaultScenario(policyPack), { policyPack });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function handleSimulate(parsed) {
|
|
506
|
+
const policyPack = await readJsonPath(parsed.inputPath);
|
|
507
|
+
const validation = validatePolicyPackDocument(policyPack);
|
|
508
|
+
if (!validation.ok) {
|
|
509
|
+
await writeOutput({
|
|
510
|
+
format: parsed.format,
|
|
511
|
+
payload: validation,
|
|
512
|
+
text: renderValidationText(validation),
|
|
513
|
+
jsonOut: parsed.jsonOut
|
|
514
|
+
});
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const scenario = await loadScenario(parsed, policyPack);
|
|
519
|
+
const report = simulatePolicyPack({ policyPack, scenario });
|
|
520
|
+
await writeOutput({
|
|
521
|
+
format: parsed.format,
|
|
522
|
+
payload: report,
|
|
523
|
+
text: renderSimulationText(report),
|
|
524
|
+
jsonOut: parsed.jsonOut
|
|
525
|
+
});
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function handlePublish(parsed) {
|
|
530
|
+
const policyPack = await readJsonPath(parsed.inputPath);
|
|
531
|
+
const validation = validatePolicyPackDocument(policyPack);
|
|
532
|
+
if (!validation.ok) {
|
|
533
|
+
await writeOutput({
|
|
534
|
+
format: parsed.format,
|
|
535
|
+
payload: validation,
|
|
536
|
+
text: renderValidationText(validation),
|
|
537
|
+
jsonOut: parsed.jsonOut
|
|
538
|
+
});
|
|
539
|
+
return 1;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const channel = toTrimmedOrNull(parsed.channel) ?? "local";
|
|
543
|
+
const owner = toTrimmedOrNull(parsed.owner) ?? "local-operator";
|
|
544
|
+
const { policyFingerprint, publicationRef, artifact } = buildPublicationArtifact(policyPack, { channel, owner });
|
|
545
|
+
const artifactPath = path.resolve(process.cwd(), parsed.outPath || `${policyPack.packId}.publish.${policyFingerprint.slice(0, 12)}.json`);
|
|
546
|
+
if (!parsed.force && (await exists(artifactPath))) fail(`output path exists: ${artifactPath}`);
|
|
547
|
+
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
548
|
+
await fs.writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
549
|
+
|
|
550
|
+
const artifactSha256 = sha256Hex(canonicalJsonStringify(normalizeForCanonicalJson(artifact, { path: "$" })));
|
|
551
|
+
const report = {
|
|
552
|
+
schemaVersion: PUBLISH_REPORT_SCHEMA_VERSION,
|
|
553
|
+
ok: true,
|
|
554
|
+
packId: policyPack.packId,
|
|
555
|
+
publicationRef,
|
|
556
|
+
channel,
|
|
557
|
+
owner,
|
|
558
|
+
policyFingerprint,
|
|
559
|
+
artifactPath,
|
|
560
|
+
artifactSha256
|
|
561
|
+
};
|
|
562
|
+
await writeOutput({
|
|
563
|
+
format: parsed.format,
|
|
564
|
+
payload: report,
|
|
565
|
+
text: renderPublishText(report),
|
|
566
|
+
jsonOut: parsed.jsonOut
|
|
567
|
+
});
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function main() {
|
|
572
|
+
let parsed;
|
|
573
|
+
try {
|
|
574
|
+
parsed = parseArgs(process.argv.slice(2));
|
|
575
|
+
} catch (err) {
|
|
576
|
+
usage();
|
|
577
|
+
process.stderr.write(`${err?.message ?? "invalid arguments"}\n`);
|
|
578
|
+
process.exit(2);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (parsed.help) {
|
|
583
|
+
usage();
|
|
584
|
+
process.exit(0);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
let code = 1;
|
|
590
|
+
if (parsed.command === "init") code = await handleInit(parsed);
|
|
591
|
+
if (parsed.command === "simulate") code = await handleSimulate(parsed);
|
|
592
|
+
if (parsed.command === "publish") code = await handlePublish(parsed);
|
|
593
|
+
process.exit(code);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
process.stderr.write(`${err?.message ?? "command failed"}\n`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
main();
|