settld 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +32 -0
  2. package/SETTLD_VERSION +1 -1
  3. package/bin/settld.js +58 -0
  4. package/docs/CIRCLE_SANDBOX_E2E.md +12 -0
  5. package/docs/QUICKSTART_MCP.md +41 -1
  6. package/docs/QUICKSTART_MCP_HOSTS.md +156 -89
  7. package/docs/QUICKSTART_POLICY_PACKS.md +65 -0
  8. package/docs/QUICKSTART_PROFILES.md +198 -0
  9. package/docs/README.md +18 -0
  10. package/docs/RELEASE_CHECKLIST.md +26 -0
  11. package/docs/RELEASING.md +1 -0
  12. package/docs/SLO.md +62 -1
  13. package/docs/SUMMARY.md +1 -0
  14. package/docs/gitbook/README.md +13 -1
  15. package/docs/gitbook/quickstart.md +57 -58
  16. package/docs/integrations/README.md +1 -0
  17. package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +95 -0
  18. package/docs/ops/DISPUTE_FINANCE_RECONCILIATION_PACKET.md +56 -0
  19. package/docs/ops/KERNEL_V0_SHIP_GATE.md +3 -1
  20. package/docs/ops/MCP_COMPATIBILITY_MATRIX.md +8 -6
  21. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +46 -9
  22. package/docs/ops/TRUST_CONFIG_WIZARD.md +37 -24
  23. package/docs/plans/2026-02-20-trust-os-v1-jira-backlog.md +348 -0
  24. package/docs/plans/2026-02-21-agent-economic-actor-operating-model.md +169 -0
  25. package/docs/plans/2026-02-21-trust-os-v1-strategy.md +241 -0
  26. package/docs/research/2026-02-21-agent-spend-host-landscape.md +57 -0
  27. package/docs/spec/ArbitrationOutcomeMapping.v1.md +62 -0
  28. package/docs/spec/DisputeCaseLifecycle.v1.md +51 -0
  29. package/docs/spec/OperatorAction.v1.md +90 -0
  30. package/docs/spec/PolicyDecision.v1.md +83 -0
  31. package/docs/spec/README.md +5 -0
  32. package/docs/spec/SettlementDecisionRecord.v2.md +2 -0
  33. package/docs/spec/schemas/OperatorAction.v1.schema.json +113 -0
  34. package/docs/spec/schemas/PolicyDecision.v1.schema.json +74 -0
  35. package/docs/spec/schemas/SettlementDecisionRecord.v2.schema.json +1 -0
  36. package/docs/spec/x402-error-codes.v1.txt +14 -0
  37. package/package.json +14 -1
  38. package/scripts/ci/build-launch-cutover-packet.mjs +177 -21
  39. package/scripts/ci/run-10x-throughput-drill.mjs +76 -4
  40. package/scripts/ci/run-10x-throughput-incident-rehearsal.mjs +49 -6
  41. package/scripts/ci/run-mcp-host-cert-matrix.mjs +201 -0
  42. package/scripts/ci/run-mcp-host-smoke.mjs +203 -5
  43. package/scripts/ci/run-offline-verification-parity-gate.mjs +762 -0
  44. package/scripts/ci/run-onboarding-host-success-gate.mjs +516 -0
  45. package/scripts/ci/run-onboarding-policy-slo-gate.mjs +537 -0
  46. package/scripts/ci/run-production-cutover-gate.mjs +540 -0
  47. package/scripts/ci/run-public-openclaw-npx-smoke.mjs +148 -0
  48. package/scripts/ci/run-release-promotion-guard.mjs +756 -0
  49. package/scripts/doctor/mcp-host.mjs +120 -0
  50. package/scripts/mcp/settld-mcp-server.mjs +330 -20
  51. package/scripts/ops/dispute-finance-reconciliation-packet.mjs +313 -0
  52. package/scripts/ops/hosted-baseline-evidence.mjs +286 -77
  53. package/scripts/ops/run-x402-hitl-smoke.mjs +607 -0
  54. package/scripts/policy/cli.mjs +600 -0
  55. package/scripts/profile/cli.mjs +1324 -0
  56. package/scripts/register-entity-secret.mjs +102 -0
  57. package/scripts/setup/circle-bootstrap.mjs +310 -0
  58. package/scripts/setup/host-config.mjs +617 -0
  59. package/scripts/setup/onboard.mjs +1337 -0
  60. package/scripts/setup/openclaw-onboard.mjs +423 -0
  61. package/scripts/setup/wizard.mjs +986 -0
  62. package/scripts/slo/check.mjs +123 -62
  63. package/scripts/spec/generate-protocol-vectors.mjs +88 -0
  64. package/scripts/test/run.sh +23 -9
  65. package/services/x402-gateway/src/server.js +147 -36
  66. package/src/api/app.js +2345 -267
  67. package/src/api/middleware/trust-kernel.js +114 -0
  68. package/src/api/openapi.js +598 -3
  69. package/src/api/persistence.js +184 -0
  70. package/src/api/store.js +277 -0
  71. package/src/core/agent-wallets.js +134 -0
  72. package/src/core/event-policy.js +21 -2
  73. package/src/core/operator-action.js +303 -0
  74. package/src/core/policy-decision.js +322 -0
  75. package/src/core/policy-packs.js +207 -0
  76. package/src/core/profile-fingerprint.js +27 -0
  77. package/src/core/profile-simulation-reasons.js +84 -0
  78. package/src/core/profile-templates.js +242 -0
  79. package/src/core/settlement-kernel.js +27 -1
  80. package/src/core/wallet-assignment-resolver.js +129 -0
  81. package/src/core/wallet-provider-bootstrap.js +365 -0
  82. package/src/db/store-pg.js +631 -0
@@ -0,0 +1,617 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ export const SUPPORTED_HOSTS = Object.freeze(["codex", "claude", "cursor", "openclaw"]);
8
+
9
+ const SUMMARY_SCHEMA_VERSION = "SettldHostConfigSetupResult.v1";
10
+ const X402_AGENT_PASSPORT_SCHEMA_VERSION = "X402AgentPassport.v1";
11
+ const REF_VALUE_PATTERN = /^[A-Za-z0-9:_-]+$/;
12
+
13
+ function isPlainObject(value) {
14
+ return Boolean(
15
+ value &&
16
+ typeof value === "object" &&
17
+ !Array.isArray(value) &&
18
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)
19
+ );
20
+ }
21
+
22
+ function cloneJson(value) {
23
+ return JSON.parse(JSON.stringify(value));
24
+ }
25
+
26
+ function deepEqual(a, b) {
27
+ if (a === b) return true;
28
+ if (typeof a !== typeof b) return false;
29
+ if (Array.isArray(a)) {
30
+ if (!Array.isArray(b) || a.length !== b.length) return false;
31
+ for (let i = 0; i < a.length; i += 1) {
32
+ if (!deepEqual(a[i], b[i])) return false;
33
+ }
34
+ return true;
35
+ }
36
+ if (isPlainObject(a)) {
37
+ if (!isPlainObject(b)) return false;
38
+ const keysA = Object.keys(a);
39
+ const keysB = Object.keys(b);
40
+ if (keysA.length !== keysB.length) return false;
41
+ for (const key of keysA) {
42
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
43
+ if (!deepEqual(a[key], b[key])) return false;
44
+ }
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function normalizeHost(value) {
51
+ const host = String(value ?? "").trim().toLowerCase();
52
+ if (!SUPPORTED_HOSTS.includes(host)) {
53
+ throw new Error(`unsupported host: ${value ?? ""} (expected one of: ${SUPPORTED_HOSTS.join(", ")})`);
54
+ }
55
+ return host;
56
+ }
57
+
58
+ function normalizeConfigPath(inputPath, { cwd = process.cwd() } = {}) {
59
+ const raw = String(inputPath ?? "").trim();
60
+ if (!raw) return null;
61
+ return path.normalize(path.isAbsolute(raw) ? raw : path.resolve(cwd, raw));
62
+ }
63
+
64
+ function uniquePathRows(rows) {
65
+ const out = [];
66
+ const seen = new Set();
67
+ for (const row of rows) {
68
+ if (!row || typeof row.path !== "string" || !row.path) continue;
69
+ const normalized = path.normalize(row.path);
70
+ if (seen.has(normalized)) continue;
71
+ seen.add(normalized);
72
+ out.push({ path: normalized, source: row.source ?? "unknown" });
73
+ }
74
+ return out;
75
+ }
76
+
77
+ function envPathCandidatesForHost(host, env) {
78
+ const rows = [];
79
+ const hostEnv = `SETTLD_${host.toUpperCase()}_MCP_CONFIG_PATH`;
80
+ const push = (source, value) => {
81
+ if (typeof value !== "string") return;
82
+ const trimmed = value.trim();
83
+ if (!trimmed) return;
84
+ rows.push({ source, rawPath: trimmed });
85
+ };
86
+
87
+ push("env:SETTLD_MCP_CONFIG_PATH", env.SETTLD_MCP_CONFIG_PATH);
88
+ push(`env:${hostEnv}`, env[hostEnv]);
89
+
90
+ if (host === "codex") {
91
+ push("env:CODEX_MCP_CONFIG_PATH", env.CODEX_MCP_CONFIG_PATH);
92
+ } else if (host === "claude") {
93
+ push("env:CLAUDE_DESKTOP_CONFIG_PATH", env.CLAUDE_DESKTOP_CONFIG_PATH);
94
+ push("env:CLAUDE_MCP_CONFIG_PATH", env.CLAUDE_MCP_CONFIG_PATH);
95
+ } else if (host === "cursor") {
96
+ push("env:CURSOR_MCP_CONFIG_PATH", env.CURSOR_MCP_CONFIG_PATH);
97
+ push("env:CURSOR_CONFIG_PATH", env.CURSOR_CONFIG_PATH);
98
+ } else if (host === "openclaw") {
99
+ push("env:OPENCLAW_MCP_CONFIG_PATH", env.OPENCLAW_MCP_CONFIG_PATH);
100
+ push("env:OPENCLAW_CONFIG_PATH", env.OPENCLAW_CONFIG_PATH);
101
+ }
102
+
103
+ return rows;
104
+ }
105
+
106
+ export function resolveHostConfigPathCandidatesDetailed({
107
+ host,
108
+ platform = process.platform,
109
+ env = process.env,
110
+ homeDir = os.homedir(),
111
+ cwd = process.cwd()
112
+ } = {}) {
113
+ const targetHost = normalizeHost(host);
114
+ const rows = [];
115
+ const xdgConfigHome =
116
+ typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() ? env.XDG_CONFIG_HOME.trim() : path.join(homeDir, ".config");
117
+ const appData = typeof env.APPDATA === "string" && env.APPDATA.trim() ? env.APPDATA.trim() : null;
118
+ const localAppData = typeof env.LOCALAPPDATA === "string" && env.LOCALAPPDATA.trim() ? env.LOCALAPPDATA.trim() : null;
119
+
120
+ for (const row of envPathCandidatesForHost(targetHost, env)) {
121
+ const normalized = normalizeConfigPath(row.rawPath, { cwd });
122
+ if (normalized) rows.push({ path: normalized, source: row.source });
123
+ }
124
+
125
+ if (targetHost === "codex") {
126
+ if (typeof env.CODEX_HOME === "string" && env.CODEX_HOME.trim()) {
127
+ rows.push({ path: path.join(env.CODEX_HOME.trim(), "config.json"), source: "env:CODEX_HOME" });
128
+ }
129
+ rows.push({ path: path.join(homeDir, ".codex", "config.json"), source: "default:home" });
130
+ rows.push({ path: path.join(xdgConfigHome, "codex", "config.json"), source: "default:xdg" });
131
+ }
132
+
133
+ if (targetHost === "claude") {
134
+ if (platform === "win32") {
135
+ if (appData) rows.push({ path: path.join(appData, "Claude", "claude_desktop_config.json"), source: "default:appdata" });
136
+ if (localAppData) rows.push({ path: path.join(localAppData, "Claude", "claude_desktop_config.json"), source: "default:localappdata" });
137
+ } else if (platform === "darwin") {
138
+ rows.push({
139
+ path: path.join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
140
+ source: "default:darwin"
141
+ });
142
+ } else {
143
+ rows.push({ path: path.join(xdgConfigHome, "Claude", "claude_desktop_config.json"), source: "default:xdg" });
144
+ rows.push({ path: path.join(homeDir, ".claude", "claude_desktop_config.json"), source: "default:home" });
145
+ }
146
+ }
147
+
148
+ if (targetHost === "cursor") {
149
+ if (typeof env.CURSOR_HOME === "string" && env.CURSOR_HOME.trim()) {
150
+ rows.push({ path: path.join(env.CURSOR_HOME.trim(), "mcp.json"), source: "env:CURSOR_HOME" });
151
+ }
152
+
153
+ if (platform === "win32") {
154
+ if (appData) rows.push({ path: path.join(appData, "Cursor", "User", "mcp.json"), source: "default:appdata" });
155
+ if (localAppData) rows.push({ path: path.join(localAppData, "Cursor", "User", "mcp.json"), source: "default:localappdata" });
156
+ } else if (platform === "darwin") {
157
+ rows.push({ path: path.join(homeDir, "Library", "Application Support", "Cursor", "User", "mcp.json"), source: "default:darwin" });
158
+ } else {
159
+ rows.push({ path: path.join(xdgConfigHome, "Cursor", "User", "mcp.json"), source: "default:xdg" });
160
+ }
161
+
162
+ rows.push({ path: path.join(homeDir, ".cursor", "mcp.json"), source: "default:home" });
163
+ }
164
+
165
+ if (targetHost === "openclaw") {
166
+ if (typeof env.OPENCLAW_HOME === "string" && env.OPENCLAW_HOME.trim()) {
167
+ rows.push({ path: path.join(env.OPENCLAW_HOME.trim(), "mcp.json"), source: "env:OPENCLAW_HOME" });
168
+ }
169
+
170
+ if (platform === "win32") {
171
+ if (appData) rows.push({ path: path.join(appData, "OpenClaw", "mcp.json"), source: "default:appdata" });
172
+ if (localAppData) rows.push({ path: path.join(localAppData, "OpenClaw", "mcp.json"), source: "default:localappdata" });
173
+ } else if (platform === "darwin") {
174
+ rows.push({ path: path.join(homeDir, "Library", "Application Support", "OpenClaw", "mcp.json"), source: "default:darwin" });
175
+ } else {
176
+ rows.push({ path: path.join(xdgConfigHome, "OpenClaw", "mcp.json"), source: "default:xdg" });
177
+ rows.push({ path: path.join(xdgConfigHome, "openclaw", "mcp.json"), source: "default:xdg-lower" });
178
+ }
179
+
180
+ rows.push({ path: path.join(homeDir, ".openclaw", "mcp.json"), source: "default:home" });
181
+ }
182
+
183
+ return uniquePathRows(rows);
184
+ }
185
+
186
+ export function resolveHostConfigPathCandidates(args = {}) {
187
+ return resolveHostConfigPathCandidatesDetailed(args).map((row) => row.path);
188
+ }
189
+
190
+ function normalizeHttpUrl(value, { stripRootTrailingSlash = true } = {}) {
191
+ const raw = String(value ?? "").trim();
192
+ if (!raw) return null;
193
+ let parsed;
194
+ try {
195
+ parsed = new URL(raw);
196
+ } catch {
197
+ return null;
198
+ }
199
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
200
+ let out = parsed.toString();
201
+ if (stripRootTrailingSlash && parsed.pathname === "/" && !parsed.search && !parsed.hash && out.endsWith("/")) {
202
+ out = out.slice(0, -1);
203
+ }
204
+ return out;
205
+ }
206
+
207
+ function normalizePassportRef(value, fieldPath, { required = false } = {}) {
208
+ if (value === null || value === undefined || String(value).trim() === "") {
209
+ if (required) throw new Error(`${fieldPath} must be a non-empty string`);
210
+ return null;
211
+ }
212
+ const normalized = String(value).trim();
213
+ if (!REF_VALUE_PATTERN.test(normalized)) {
214
+ throw new Error(`${fieldPath} must match ^[A-Za-z0-9:_-]+$`);
215
+ }
216
+ return normalized;
217
+ }
218
+
219
+ function normalizeOptionalPositiveSafeInteger(value, fieldPath, { allowNull = true } = {}) {
220
+ if (value === null || value === undefined || value === "") {
221
+ if (allowNull) return null;
222
+ throw new Error(`${fieldPath} must be a positive integer`);
223
+ }
224
+ const parsed = Number(value);
225
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
226
+ throw new Error(`${fieldPath} must be a positive integer`);
227
+ }
228
+ return parsed;
229
+ }
230
+
231
+ function normalizeOptionalNonNegativeSafeInteger(value, fieldPath, { allowNull = true } = {}) {
232
+ if (value === null || value === undefined || value === "") {
233
+ if (allowNull) return null;
234
+ throw new Error(`${fieldPath} must be a non-negative integer`);
235
+ }
236
+ const parsed = Number(value);
237
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
238
+ throw new Error(`${fieldPath} must be a non-negative integer`);
239
+ }
240
+ return parsed;
241
+ }
242
+
243
+ function parsePaidToolsAgentPassportFromEnv(env) {
244
+ const rawValue = env.SETTLD_PAID_TOOLS_AGENT_PASSPORT;
245
+ if (rawValue === null || rawValue === undefined) return null;
246
+ let parsed;
247
+ if (typeof rawValue === "string") {
248
+ const text = rawValue.trim();
249
+ if (!text) return null;
250
+ try {
251
+ parsed = JSON.parse(text);
252
+ } catch (err) {
253
+ throw new Error(`SETTLD_PAID_TOOLS_AGENT_PASSPORT must be valid JSON: ${err?.message ?? "parse failed"}`);
254
+ }
255
+ } else {
256
+ parsed = rawValue;
257
+ }
258
+ if (!isPlainObject(parsed)) {
259
+ throw new Error("SETTLD_PAID_TOOLS_AGENT_PASSPORT must decode to a JSON object");
260
+ }
261
+
262
+ const schemaVersionRaw =
263
+ typeof parsed.schemaVersion === "string" && parsed.schemaVersion.trim() ? parsed.schemaVersion.trim() : X402_AGENT_PASSPORT_SCHEMA_VERSION;
264
+ if (schemaVersionRaw !== X402_AGENT_PASSPORT_SCHEMA_VERSION) {
265
+ throw new Error(`SETTLD_PAID_TOOLS_AGENT_PASSPORT.schemaVersion must be ${X402_AGENT_PASSPORT_SCHEMA_VERSION}`);
266
+ }
267
+
268
+ const sponsorRef = normalizePassportRef(parsed.sponsorRef, "SETTLD_PAID_TOOLS_AGENT_PASSPORT.sponsorRef", { required: true });
269
+ const sponsorWalletRef = normalizePassportRef(parsed.sponsorWalletRef, "SETTLD_PAID_TOOLS_AGENT_PASSPORT.sponsorWalletRef", {
270
+ required: false
271
+ });
272
+ const agentKeyId = normalizePassportRef(parsed.agentKeyId, "SETTLD_PAID_TOOLS_AGENT_PASSPORT.agentKeyId", { required: true });
273
+ const policyRef = normalizePassportRef(parsed.policyRef, "SETTLD_PAID_TOOLS_AGENT_PASSPORT.policyRef", { required: false });
274
+ const policyVersion = normalizeOptionalPositiveSafeInteger(parsed.policyVersion, "SETTLD_PAID_TOOLS_AGENT_PASSPORT.policyVersion", {
275
+ allowNull: true
276
+ });
277
+ let delegationDepth = normalizeOptionalNonNegativeSafeInteger(
278
+ parsed.delegationDepth,
279
+ "SETTLD_PAID_TOOLS_AGENT_PASSPORT.delegationDepth",
280
+ { allowNull: true }
281
+ );
282
+
283
+ const hasPolicyTuple = sponsorWalletRef !== null || policyRef !== null || policyVersion !== null;
284
+ if (hasPolicyTuple && (sponsorWalletRef === null || policyRef === null || policyVersion === null)) {
285
+ throw new Error("SETTLD_PAID_TOOLS_AGENT_PASSPORT must include sponsorWalletRef + policyRef + policyVersion together");
286
+ }
287
+ if (hasPolicyTuple && delegationDepth === null) delegationDepth = 0;
288
+
289
+ return JSON.stringify({
290
+ schemaVersion: X402_AGENT_PASSPORT_SCHEMA_VERSION,
291
+ sponsorRef,
292
+ ...(sponsorWalletRef ? { sponsorWalletRef } : {}),
293
+ agentKeyId,
294
+ ...(policyRef ? { policyRef } : {}),
295
+ ...(policyVersion ? { policyVersion } : {}),
296
+ ...(delegationDepth !== null ? { delegationDepth } : {})
297
+ });
298
+ }
299
+
300
+ function parseMcpArgsFromEnv(env) {
301
+ const argsJson = typeof env.SETTLD_MCP_ARGS_JSON === "string" ? env.SETTLD_MCP_ARGS_JSON.trim() : "";
302
+ if (!argsJson) return ["-y", "settld-mcp"];
303
+ let parsed;
304
+ try {
305
+ parsed = JSON.parse(argsJson);
306
+ } catch (err) {
307
+ throw new Error(`SETTLD_MCP_ARGS_JSON must be valid JSON: ${err?.message ?? "parse failed"}`);
308
+ }
309
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string" || !item.trim())) {
310
+ throw new Error("SETTLD_MCP_ARGS_JSON must be an array of non-empty strings");
311
+ }
312
+ return parsed.map((item) => item.trim());
313
+ }
314
+
315
+ export function buildSettldMcpServerConfig({ env = process.env } = {}) {
316
+ const missing = [];
317
+
318
+ const baseUrlRaw = typeof env.SETTLD_BASE_URL === "string" ? env.SETTLD_BASE_URL.trim() : "";
319
+ if (!baseUrlRaw) missing.push("SETTLD_BASE_URL");
320
+ const tenantId = typeof env.SETTLD_TENANT_ID === "string" ? env.SETTLD_TENANT_ID.trim() : "";
321
+ if (!tenantId) missing.push("SETTLD_TENANT_ID");
322
+ const apiKey = typeof env.SETTLD_API_KEY === "string" ? env.SETTLD_API_KEY.trim() : "";
323
+ if (!apiKey) missing.push("SETTLD_API_KEY");
324
+
325
+ if (missing.length > 0) {
326
+ const err = new Error(`missing required env vars: ${missing.join(", ")}`);
327
+ err.code = "MISSING_ENV";
328
+ err.missingEnv = missing;
329
+ throw err;
330
+ }
331
+
332
+ const baseUrl = normalizeHttpUrl(baseUrlRaw, { stripRootTrailingSlash: true });
333
+ if (!baseUrl) {
334
+ const err = new Error("SETTLD_BASE_URL must be a valid http(s) URL");
335
+ err.code = "INVALID_ENV";
336
+ throw err;
337
+ }
338
+
339
+ const paidToolsRaw = typeof env.SETTLD_PAID_TOOLS_BASE_URL === "string" ? env.SETTLD_PAID_TOOLS_BASE_URL.trim() : "";
340
+ const paidToolsBaseUrl = paidToolsRaw ? normalizeHttpUrl(paidToolsRaw, { stripRootTrailingSlash: true }) : null;
341
+ if (paidToolsRaw && !paidToolsBaseUrl) {
342
+ const err = new Error("SETTLD_PAID_TOOLS_BASE_URL must be a valid http(s) URL");
343
+ err.code = "INVALID_ENV";
344
+ throw err;
345
+ }
346
+
347
+ let paidToolsAgentPassport = null;
348
+ try {
349
+ paidToolsAgentPassport = parsePaidToolsAgentPassportFromEnv(env);
350
+ } catch (cause) {
351
+ const err = new Error(cause?.message ?? "SETTLD_PAID_TOOLS_AGENT_PASSPORT is invalid");
352
+ err.code = "INVALID_ENV";
353
+ throw err;
354
+ }
355
+
356
+ const command = typeof env.SETTLD_MCP_COMMAND === "string" && env.SETTLD_MCP_COMMAND.trim() ? env.SETTLD_MCP_COMMAND.trim() : "npx";
357
+ const args = parseMcpArgsFromEnv(env);
358
+
359
+ const serverEnv = {
360
+ SETTLD_BASE_URL: baseUrl,
361
+ SETTLD_TENANT_ID: tenantId,
362
+ SETTLD_API_KEY: apiKey
363
+ };
364
+ if (paidToolsBaseUrl) {
365
+ serverEnv.SETTLD_PAID_TOOLS_BASE_URL = paidToolsBaseUrl;
366
+ }
367
+ if (paidToolsAgentPassport) {
368
+ serverEnv.SETTLD_PAID_TOOLS_AGENT_PASSPORT = paidToolsAgentPassport;
369
+ }
370
+
371
+ return {
372
+ command,
373
+ args,
374
+ env: serverEnv
375
+ };
376
+ }
377
+
378
+ function mergeUnderContainer(rootConfig, containerKey, settldServer) {
379
+ const existingContainer = isPlainObject(rootConfig[containerKey]) ? rootConfig[containerKey] : {};
380
+ const nextContainer = {
381
+ ...existingContainer,
382
+ settld: cloneJson(settldServer)
383
+ };
384
+ return {
385
+ ...rootConfig,
386
+ [containerKey]: nextContainer
387
+ };
388
+ }
389
+
390
+ export function applySettldServerConfig({ host, existingConfig, settldServer } = {}) {
391
+ const targetHost = normalizeHost(host);
392
+ const inputConfig = isPlainObject(existingConfig) ? cloneJson(existingConfig) : {};
393
+ if (!isPlainObject(settldServer)) {
394
+ throw new Error("settldServer must be an object");
395
+ }
396
+
397
+ let keyPath = "mcpServers.settld";
398
+ let nextConfig;
399
+
400
+ if (isPlainObject(inputConfig.mcpServers)) {
401
+ keyPath = "mcpServers.settld";
402
+ nextConfig = mergeUnderContainer(inputConfig, "mcpServers", settldServer);
403
+ } else if (isPlainObject(inputConfig.servers)) {
404
+ keyPath = "servers.settld";
405
+ nextConfig = mergeUnderContainer(inputConfig, "servers", settldServer);
406
+ } else if (targetHost === "openclaw") {
407
+ keyPath = "root";
408
+ nextConfig = {
409
+ ...inputConfig,
410
+ name: "settld",
411
+ command: settldServer.command,
412
+ args: cloneJson(settldServer.args),
413
+ env: cloneJson(settldServer.env)
414
+ };
415
+ } else {
416
+ keyPath = "mcpServers.settld";
417
+ nextConfig = mergeUnderContainer(inputConfig, "mcpServers", settldServer);
418
+ }
419
+
420
+ return {
421
+ keyPath,
422
+ changed: !deepEqual(inputConfig, nextConfig),
423
+ config: nextConfig
424
+ };
425
+ }
426
+
427
+ async function readConfigJson(configPath, { readFile = fs.readFile } = {}) {
428
+ try {
429
+ const raw = await readFile(configPath, "utf8");
430
+ const text = String(raw ?? "");
431
+ if (!text.trim()) return { exists: true, config: {} };
432
+ let parsed;
433
+ try {
434
+ parsed = JSON.parse(text);
435
+ } catch (err) {
436
+ const parseErr = new Error(`invalid JSON in ${configPath}: ${err?.message ?? "parse failed"}`);
437
+ parseErr.code = "INVALID_CONFIG_JSON";
438
+ throw parseErr;
439
+ }
440
+ if (!isPlainObject(parsed)) {
441
+ const typeErr = new Error(`config root must be a JSON object: ${configPath}`);
442
+ typeErr.code = "INVALID_CONFIG_JSON";
443
+ throw typeErr;
444
+ }
445
+ return { exists: true, config: parsed };
446
+ } catch (err) {
447
+ if (err?.code === "ENOENT") {
448
+ return { exists: false, config: {} };
449
+ }
450
+ throw err;
451
+ }
452
+ }
453
+
454
+ export async function runHostConfigSetup({
455
+ host,
456
+ configPath = null,
457
+ dryRun = false,
458
+ env = process.env,
459
+ platform = process.platform,
460
+ homeDir = os.homedir(),
461
+ cwd = process.cwd(),
462
+ readFile = fs.readFile,
463
+ writeFile = fs.writeFile,
464
+ mkdir = fs.mkdir
465
+ } = {}) {
466
+ const targetHost = normalizeHost(host ?? env.SETTLD_MCP_HOST);
467
+ const serverConfig = buildSettldMcpServerConfig({ env });
468
+
469
+ const candidates = configPath
470
+ ? [{ path: normalizeConfigPath(configPath, { cwd }), source: "cli:--config-path" }]
471
+ : resolveHostConfigPathCandidatesDetailed({ host: targetHost, platform, env, homeDir, cwd });
472
+
473
+ if (!candidates.length || !candidates[0]?.path) {
474
+ const err = new Error(`no candidate config path found for host ${targetHost}`);
475
+ err.code = "CONFIG_PATH_NOT_FOUND";
476
+ throw err;
477
+ }
478
+
479
+ const selectedPath = candidates[0].path;
480
+ const loaded = await readConfigJson(selectedPath, { readFile });
481
+ const merged = applySettldServerConfig({
482
+ host: targetHost,
483
+ existingConfig: loaded.config,
484
+ settldServer: serverConfig
485
+ });
486
+
487
+ let wroteFile = false;
488
+ if (!dryRun && merged.changed) {
489
+ await mkdir(path.dirname(selectedPath), { recursive: true });
490
+ await writeFile(selectedPath, JSON.stringify(merged.config, null, 2) + "\n", "utf8");
491
+ wroteFile = true;
492
+ }
493
+
494
+ return {
495
+ schemaVersion: SUMMARY_SCHEMA_VERSION,
496
+ ok: true,
497
+ host: targetHost,
498
+ dryRun: Boolean(dryRun),
499
+ configPath: selectedPath,
500
+ pathSource: candidates[0].source,
501
+ candidates: candidates.map((row) => row.path),
502
+ existed: loaded.exists,
503
+ changed: merged.changed,
504
+ wroteFile,
505
+ keyPath: merged.keyPath,
506
+ serverCommand: serverConfig.command,
507
+ serverArgs: serverConfig.args,
508
+ serverEnvKeys: Object.keys(serverConfig.env)
509
+ };
510
+ }
511
+
512
+ function usage() {
513
+ return [
514
+ "usage:",
515
+ " node scripts/setup/host-config.mjs --host <codex|claude|cursor|openclaw> [--config-path <path>] [--dry-run]",
516
+ "",
517
+ "notes:",
518
+ " - Required env vars: SETTLD_BASE_URL, SETTLD_TENANT_ID, SETTLD_API_KEY",
519
+ " - Optional env vars: SETTLD_PAID_TOOLS_BASE_URL, SETTLD_PAID_TOOLS_AGENT_PASSPORT, SETTLD_MCP_COMMAND, SETTLD_MCP_ARGS_JSON",
520
+ " - If --config-path is omitted, host-specific candidate paths are auto-detected"
521
+ ].join("\n");
522
+ }
523
+
524
+ export function parseCliArgs(argv) {
525
+ const out = {
526
+ host: null,
527
+ configPath: null,
528
+ dryRun: false,
529
+ help: false
530
+ };
531
+
532
+ for (let i = 0; i < argv.length; i += 1) {
533
+ const arg = argv[i];
534
+ if (arg === "--host") {
535
+ out.host = String(argv[i + 1] ?? "").trim().toLowerCase();
536
+ i += 1;
537
+ continue;
538
+ }
539
+ if (arg === "--config-path") {
540
+ out.configPath = String(argv[i + 1] ?? "").trim();
541
+ i += 1;
542
+ continue;
543
+ }
544
+ if (arg === "--dry-run") {
545
+ out.dryRun = true;
546
+ continue;
547
+ }
548
+ if (arg === "--help" || arg === "-h") {
549
+ out.help = true;
550
+ continue;
551
+ }
552
+ throw new Error(`unknown argument: ${arg}`);
553
+ }
554
+
555
+ return out;
556
+ }
557
+
558
+ function buildErrorSummary(err, { host = null, dryRun = false } = {}) {
559
+ const missingEnv = Array.isArray(err?.missingEnv) ? err.missingEnv : [];
560
+ return {
561
+ schemaVersion: SUMMARY_SCHEMA_VERSION,
562
+ ok: false,
563
+ host,
564
+ dryRun: Boolean(dryRun),
565
+ error: {
566
+ code: typeof err?.code === "string" ? err.code : "ERROR",
567
+ message: err?.message ?? String(err),
568
+ missingEnv
569
+ }
570
+ };
571
+ }
572
+
573
+ export async function runCli(argv = process.argv.slice(2), { env = process.env } = {}) {
574
+ let parsed;
575
+ try {
576
+ parsed = parseCliArgs(argv);
577
+ } catch (err) {
578
+ const summary = buildErrorSummary(err);
579
+ process.stderr.write(`${usage()}\n\n`);
580
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
581
+ return { exitCode: 2, summary };
582
+ }
583
+
584
+ if (parsed.help) {
585
+ process.stdout.write(`${usage()}\n`);
586
+ return { exitCode: 0, summary: null };
587
+ }
588
+
589
+ try {
590
+ const summary = await runHostConfigSetup({
591
+ host: parsed.host,
592
+ configPath: parsed.configPath,
593
+ dryRun: parsed.dryRun,
594
+ env
595
+ });
596
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
597
+ return { exitCode: 0, summary };
598
+ } catch (err) {
599
+ const summary = buildErrorSummary(err, {
600
+ host: parsed.host ?? null,
601
+ dryRun: parsed.dryRun
602
+ });
603
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
604
+ return { exitCode: 1, summary };
605
+ }
606
+ }
607
+
608
+ function isMainModule() {
609
+ if (!process.argv[1]) return false;
610
+ return import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
611
+ }
612
+
613
+ if (isMainModule()) {
614
+ runCli().then(({ exitCode }) => {
615
+ process.exitCode = exitCode;
616
+ });
617
+ }