@honua/mcp-server 0.1.4-beta.0 → 0.1.9-beta.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 (50) hide show
  1. package/README.md +30 -2
  2. package/dist/src/certification/admin-parity.d.ts +96 -0
  3. package/dist/src/certification/admin-parity.d.ts.map +1 -0
  4. package/dist/src/certification/admin-parity.js +235 -0
  5. package/dist/src/certification/target.d.ts.map +1 -1
  6. package/dist/src/certification/target.js +18 -26
  7. package/dist/src/credential-endpoint.d.ts +3 -0
  8. package/dist/src/credential-endpoint.d.ts.map +1 -0
  9. package/dist/src/credential-endpoint.js +24 -0
  10. package/dist/src/entrypoint.d.ts +20 -0
  11. package/dist/src/entrypoint.d.ts.map +1 -0
  12. package/dist/src/entrypoint.js +38 -0
  13. package/dist/src/index.d.ts +22 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +33 -5
  16. package/dist/src/proxy.d.ts.map +1 -1
  17. package/dist/src/proxy.js +31 -16
  18. package/dist/src/release/zero-to-map-checkpoint.d.ts +34 -0
  19. package/dist/src/release/zero-to-map-checkpoint.d.ts.map +1 -0
  20. package/dist/src/release/zero-to-map-checkpoint.js +369 -0
  21. package/dist/src/release/zero-to-map-cli.d.ts +51 -0
  22. package/dist/src/release/zero-to-map-cli.d.ts.map +1 -0
  23. package/dist/src/release/zero-to-map-cli.js +881 -0
  24. package/dist/src/release/zero-to-map-provision.d.ts +37 -0
  25. package/dist/src/release/zero-to-map-provision.d.ts.map +1 -0
  26. package/dist/src/release/zero-to-map-provision.js +177 -0
  27. package/dist/src/release/zero-to-map.d.ts +296 -0
  28. package/dist/src/release/zero-to-map.d.ts.map +1 -0
  29. package/dist/src/release/zero-to-map.js +1069 -0
  30. package/dist/src/tools/admin-install-local.d.ts +30 -0
  31. package/dist/src/tools/admin-install-local.d.ts.map +1 -0
  32. package/dist/src/tools/admin-install-local.js +22 -0
  33. package/dist/src/tools/control-plane-command.d.ts +99 -0
  34. package/dist/src/tools/control-plane-command.d.ts.map +1 -0
  35. package/dist/src/tools/control-plane-command.js +180 -0
  36. package/dist/src/tools/docs-search.d.ts +100 -0
  37. package/dist/src/tools/docs-search.d.ts.map +1 -0
  38. package/dist/src/tools/docs-search.js +381 -0
  39. package/package.json +8 -3
  40. package/release/zero-to-map/README.md +174 -0
  41. package/release/zero-to-map/configs/claude-code.mcp.json +11 -0
  42. package/release/zero-to-map/configs/claude-desktop.json +11 -0
  43. package/release/zero-to-map/configs/cursor.json +11 -0
  44. package/release/zero-to-map/contracts/aws-ecs-provision-binding.schema.json +59 -0
  45. package/release/zero-to-map/contracts/checkpoint.schema.json +152 -0
  46. package/release/zero-to-map/contracts/console-receipt.schema.json +150 -0
  47. package/release/zero-to-map/contracts/esri-gp-mcp.v1.json +73 -0
  48. package/release/zero-to-map/fixtures/parcels.geojson +33 -0
  49. package/release/zero-to-map/fixtures/zoning.geojson +24 -0
  50. package/release/zero-to-map/journey.v1.json +1248 -0
@@ -0,0 +1,881 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { constants } from "node:fs";
5
+ import { access, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
6
+ import path, { dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { HonuaClient } from "@honua/sdk-js";
9
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
10
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
11
+ import { listAllTools } from "../certification/admin-parity.js";
12
+ import { validateAgainstSchema } from "../certification/json-schema.js";
13
+ import { requireSecureCredentialEndpoint } from "../credential-endpoint.js";
14
+ import { isMainEntrypoint } from "../entrypoint.js";
15
+ import { resolveProxyOptions } from "../proxy.js";
16
+ import { assertZeroToMapCheckpointBindings, assertZeroToMapCheckpointDigest, assertZeroToMapCheckpointFresh, consumeZeroToMapCheckpoint, createZeroToMapCheckpoint, parseZeroToMapCheckpoint, } from "./zero-to-map-checkpoint.js";
17
+ import { ZERO_TO_MAP_ACCESS_GRANTS, assertAwsEcsProvisionBindings, parseAwsEcsProvisionBinding, } from "./zero-to-map-provision.js";
18
+ import { JourneyBlockedError, parseZeroToMapPlan, runZeroToMapJourney, } from "./zero-to-map.js";
19
+ class ContractAdapter {
20
+ runCli() {
21
+ return Promise.reject(new Error("contract adapter must not execute CLI work"));
22
+ }
23
+ listTools() {
24
+ return Promise.reject(new Error("contract adapter must not connect to MCP"));
25
+ }
26
+ callTool() {
27
+ return Promise.reject(new Error("contract adapter must not call MCP tools"));
28
+ }
29
+ readResource() {
30
+ return Promise.reject(new Error("contract adapter must not read MCP resources"));
31
+ }
32
+ runGpServer() {
33
+ return Promise.reject(new Error("contract adapter must not execute GPServer jobs"));
34
+ }
35
+ readReceipt() {
36
+ return Promise.resolve(undefined);
37
+ }
38
+ checkHttp() {
39
+ return Promise.reject(new Error("contract adapter must not make HTTP requests"));
40
+ }
41
+ }
42
+ class LiveAdapter {
43
+ options;
44
+ env;
45
+ #client;
46
+ constructor(options, env) {
47
+ this.options = options;
48
+ this.env = env;
49
+ }
50
+ async runCli(args) {
51
+ if (this.options.target === "aws-ecs")
52
+ return this.awsProvisionEvidence(args);
53
+ const result = await runProcess(this.options.honuaCommand, args, this.env);
54
+ if (result.exitCode !== 0) {
55
+ throw new Error(`honua CLI exited ${result.exitCode}; command output was suppressed`);
56
+ }
57
+ if (args[0] === "admin" && args[1] === "install" && args[2] === "local") {
58
+ const accessCredential = parseInstallAccessCredential(result.stdout);
59
+ return {
60
+ value: { accessCredential },
61
+ evidence: {
62
+ command: "honua",
63
+ args: redactCliArgs(args),
64
+ exitCode: result.exitCode,
65
+ accessCredential,
66
+ },
67
+ };
68
+ }
69
+ return { evidence: { command: "honua", args: redactCliArgs(args), exitCode: result.exitCode } };
70
+ }
71
+ awsProvisionEvidence(args) {
72
+ const binding = this.options.provisionBinding;
73
+ if (!binding)
74
+ throw new Error("AWS ECS target has no verified provision binding");
75
+ if (args[0] !== "admin" || args[1] !== "install") {
76
+ throw new Error("AWS ECS target can replace only the Stage 1 install actions");
77
+ }
78
+ if (args[2] === "local") {
79
+ return {
80
+ value: { accessCredential: binding.accessCredential },
81
+ evidence: {
82
+ target: binding.target,
83
+ candidateId: binding.candidateId,
84
+ releaseId: binding.releaseId,
85
+ serverImage: binding.serverImage,
86
+ components: binding.components,
87
+ terraformPlan: binding.checks["terraform-plan"],
88
+ terraformApply: binding.checks["terraform-apply"],
89
+ producerEvidenceUrl: binding.evidence.url,
90
+ producerEvidenceSha256: binding.evidence.sha256,
91
+ accessCredential: binding.accessCredential,
92
+ },
93
+ };
94
+ }
95
+ if (args[2] === "status") {
96
+ return {
97
+ evidence: {
98
+ target: binding.target,
99
+ endpoint: binding.endpoint,
100
+ mcpUrl: this.options.mcpUrl,
101
+ readiness: binding.checks.readiness,
102
+ adminMcpHandoff: binding.checks["admin-mcp-handoff"],
103
+ credentialReferencePresent: binding.adminKeySecretRef.length > 0,
104
+ },
105
+ };
106
+ }
107
+ throw new Error("AWS ECS target encountered an unknown Stage 1 install action");
108
+ }
109
+ async listTools() {
110
+ const client = await this.client();
111
+ const tools = await listAllTools(client);
112
+ return tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema }));
113
+ }
114
+ async callTool(tool, args) {
115
+ const result = await (await this.client()).callTool({ name: tool, arguments: args });
116
+ if (result.isError === true) {
117
+ throw new Error(`${tool} returned isError=true: ${toolErrorMessage(result)}`);
118
+ }
119
+ const operation = requireCompletedPublishedOperation(tool, result);
120
+ return {
121
+ value: result,
122
+ evidence: {
123
+ tool,
124
+ isError: false,
125
+ ...(operation
126
+ ? {
127
+ operationId: operation.operationId,
128
+ status: operation.status,
129
+ handleId: operation.handleId,
130
+ metadataRevision: operation.metadataRevision,
131
+ }
132
+ : {}),
133
+ },
134
+ };
135
+ }
136
+ async readResource(action) {
137
+ const client = await this.client();
138
+ return readJourneyMcpResource(action, (uri) => client.readResource({ uri }));
139
+ }
140
+ async runGpServer(action) {
141
+ const mcpUrl = this.options.mcpUrl;
142
+ if (!mcpUrl) {
143
+ throw new JourneyBlockedError("The GPServer run requires --mcp-url to identify the Honua deployment.", "mcp-url-missing");
144
+ }
145
+ const proxyOptions = resolveJourneyProxyOptions(mcpUrl, this.env);
146
+ const endpoint = new URL(proxyOptions.remoteUrl);
147
+ endpoint.pathname = endpoint.pathname.replace(/\/mcp\/?$/, "") || "/";
148
+ endpoint.search = "";
149
+ endpoint.hash = "";
150
+ const client = new HonuaClient({
151
+ baseUrl: endpoint.toString(),
152
+ apiKey: proxyOptions.apiKey,
153
+ bearerToken: proxyOptions.authToken,
154
+ });
155
+ const runner = client.geoprocessingRunner(action.serviceId, action.taskName);
156
+ const job = await runner.execute({
157
+ processId: action.processId,
158
+ parameters: action.parameters,
159
+ resultNames: action.resultNames,
160
+ });
161
+ const result = await job.results({ pollIntervalMs: 500, deadlineMs: 120_000 });
162
+ return {
163
+ value: { jobId: job.id, jobType: job.type, status: job.status, outputs: result.outputs },
164
+ evidence: {
165
+ protocol: runner.protocol,
166
+ processId: action.processId,
167
+ serviceId: action.serviceId,
168
+ taskName: action.taskName,
169
+ jobId: job.id,
170
+ status: job.status,
171
+ resultNames: Object.keys(result.outputs),
172
+ },
173
+ };
174
+ }
175
+ async readReceipt(actionId) {
176
+ const path = this.options.receiptPaths[actionId];
177
+ if (!path)
178
+ return undefined;
179
+ const bytes = await readFile(path);
180
+ const value = JSON.parse(bytes.toString("utf8"));
181
+ if (actionId === "console-approval") {
182
+ const schemaPath = fileURLToPath(new URL("../../../release/zero-to-map/contracts/console-receipt.schema.json", import.meta.url));
183
+ const schema = JSON.parse(await readFile(schemaPath, "utf8"));
184
+ const validation = validateAgainstSchema(schema, value);
185
+ if (!validation.valid) {
186
+ throw new Error(`Console receipt failed its strict schema: ${validation.errors.join("; ")}`);
187
+ }
188
+ }
189
+ return {
190
+ value,
191
+ evidence: {
192
+ source: "external-receipt",
193
+ sha256: createHash("sha256").update(bytes).digest("hex"),
194
+ },
195
+ };
196
+ }
197
+ async checkHttp(url, expectedStatus) {
198
+ const requested = new URL(url);
199
+ if (requested.protocol !== "https:" || requested.username || requested.password) {
200
+ throw new Error("published artifact URL must be HTTPS and must not embed credentials");
201
+ }
202
+ const response = await fetch(url, { method: "GET", redirect: "follow" });
203
+ if (response.status !== expectedStatus) {
204
+ throw new Error(`GET ${url} returned ${response.status}; expected ${expectedStatus}`);
205
+ }
206
+ if (normalizeEndpoint(response.url) !== normalizeEndpoint(url)) {
207
+ throw new Error(`GET ${url} resolved to a different published identity: ${response.url}`);
208
+ }
209
+ const contentType = response.headers.get("content-type");
210
+ if (!contentType)
211
+ throw new Error(`GET ${url} returned no content type`);
212
+ return {
213
+ evidence: {
214
+ requestedUrl: url,
215
+ url: response.url,
216
+ status: response.status,
217
+ contentType,
218
+ identityMatched: true,
219
+ },
220
+ };
221
+ }
222
+ async close() {
223
+ await this.#client?.close();
224
+ }
225
+ async client() {
226
+ if (this.#client)
227
+ return this.#client;
228
+ const mcpUrl = this.options.mcpUrl;
229
+ if (!mcpUrl) {
230
+ throw new JourneyBlockedError("HONUA_MCP_REMOTE_URL (or --mcp-url) is required for a live journey.", "mcp-url-missing");
231
+ }
232
+ const proxyEntry = fileURLToPath(new URL("../proxy.js", import.meta.url));
233
+ const childEnv = {
234
+ ...this.env,
235
+ HONUA_MCP_REMOTE_URL: mcpUrl,
236
+ };
237
+ resolveProxyOptions(childEnv);
238
+ const transport = new StdioClientTransport({
239
+ command: process.execPath,
240
+ args: [proxyEntry],
241
+ env: childEnv,
242
+ stderr: "inherit",
243
+ });
244
+ const client = new Client({ name: "honua-zero-to-map-release", version: "1.0.0" });
245
+ try {
246
+ await client.connect(transport);
247
+ }
248
+ catch (error) {
249
+ throw new JourneyBlockedError(`Could not connect through honua-mcp-proxy: ${error instanceof Error ? error.message : String(error)}`, "mcp-proxy-unavailable");
250
+ }
251
+ this.#client = client;
252
+ return client;
253
+ }
254
+ }
255
+ export async function runZeroToMapCli(argv = process.argv.slice(2), env = process.env) {
256
+ const options = parseZeroToMapCliArgs(argv, env);
257
+ if (options.execute && !options.confirmed) {
258
+ throw new Error("--execute requires --yes because the journey creates and publishes server state");
259
+ }
260
+ if (options.execute && !options.checkpointPath) {
261
+ throw new Error("--execute requires --checkpoint so Console approval cannot replay prior mutations");
262
+ }
263
+ const planBytes = await readFile(options.planPath);
264
+ const plan = parseZeroToMapPlan(JSON.parse(planBytes.toString("utf8")));
265
+ const liveOptions = options.execute ? await prepareLiveOptions(options, env) : options;
266
+ const bindings = options.execute
267
+ ? checkpointBindings(liveOptions, plan.journeyId, plan.releaseContract, planBytes)
268
+ : undefined;
269
+ let checkpoint;
270
+ let claimPath;
271
+ if (options.execute && options.checkpointPath) {
272
+ checkpoint = await readCheckpointIfPresent(options.checkpointPath);
273
+ if (checkpoint) {
274
+ if (!options.consoleReceiptPath)
275
+ throw new Error("a paused checkpoint requires --console-receipt to resume");
276
+ if (!options.checkpointDigest)
277
+ throw new Error("--checkpoint-digest is required to resume");
278
+ assertZeroToMapCheckpointDigest(checkpoint, options.checkpointDigest);
279
+ if (checkpoint.state !== "paused")
280
+ throw new Error("checkpoint has already been consumed");
281
+ assertZeroToMapCheckpointFresh(checkpoint);
282
+ if (!bindings)
283
+ throw new Error("internal checkpoint binding error");
284
+ assertZeroToMapCheckpointBindings(checkpoint, bindings);
285
+ claimPath = await claimZeroToMapCheckpoint(options.checkpointPath, checkpoint.integrity.digest);
286
+ }
287
+ else if (options.consoleReceiptPath || options.checkpointDigest) {
288
+ throw new Error("--console-receipt and --checkpoint-digest require an existing paused checkpoint");
289
+ }
290
+ }
291
+ const adapter = options.execute ? new LiveAdapter(liveOptions, env) : new ContractAdapter();
292
+ const receipt = await runZeroToMapJourney(plan, adapter, {
293
+ execute: options.execute,
294
+ target: options.target,
295
+ variables: options.variables,
296
+ ...(checkpoint ? { resume: checkpoint.resume } : {}),
297
+ ...(!checkpoint && bindings && options.checkpointPath
298
+ ? {
299
+ onExternalReceiptMissing: async (snapshot) => {
300
+ await writeJsonNew(options.checkpointPath, createZeroToMapCheckpoint(bindings, snapshot));
301
+ },
302
+ }
303
+ : {}),
304
+ });
305
+ const receiptBytes = `${JSON.stringify(receipt, null, 2)}\n`;
306
+ await writeFile(options.outputPath, receiptBytes, "utf8");
307
+ if (checkpoint && claimPath && options.checkpointPath) {
308
+ if (receipt.status === "passed") {
309
+ const consumed = consumeZeroToMapCheckpoint(checkpoint, createHash("sha256").update(receiptBytes).digest("hex"));
310
+ await writeFile(claimPath, `${JSON.stringify(consumed, null, 2)}\n`, "utf8");
311
+ await rename(claimPath, options.checkpointPath);
312
+ }
313
+ }
314
+ process.stdout.write(`${receipt.mode} journey ${receipt.status}; receipt written to ${options.outputPath}\n`);
315
+ return receipt.status === "passed" ? 0 : receipt.status === "blocked" ? 2 : 1;
316
+ }
317
+ export function parseZeroToMapCliArgs(argv, env) {
318
+ let execute = false;
319
+ let confirmed = false;
320
+ let planPath = fileURLToPath(new URL("../../../release/zero-to-map/journey.v1.json", import.meta.url));
321
+ let outputPath = "zero-to-map-receipt.json";
322
+ let honuaCommand = env.HONUA_CLI_COMMAND ?? "honua";
323
+ let mcpUrl = env.HONUA_MCP_REMOTE_URL ?? env.HONUA_MCP_URL;
324
+ let target = "local-docker";
325
+ let checkpointPath;
326
+ let checkpointDigest;
327
+ let provisionReceiptPath;
328
+ let consoleReceiptPath;
329
+ const variables = {};
330
+ const receiptPaths = {};
331
+ for (let index = 0; index < argv.length; index += 1) {
332
+ const arg = argv[index];
333
+ switch (arg) {
334
+ case "--execute":
335
+ execute = true;
336
+ break;
337
+ case "--yes":
338
+ confirmed = true;
339
+ break;
340
+ case "--plan":
341
+ planPath = requireNext(argv, ++index, arg);
342
+ break;
343
+ case "--output":
344
+ outputPath = requireNext(argv, ++index, arg);
345
+ break;
346
+ case "--honua-command":
347
+ honuaCommand = requireNext(argv, ++index, arg);
348
+ break;
349
+ case "--mcp-url":
350
+ mcpUrl = requireNext(argv, ++index, arg);
351
+ break;
352
+ case "--target": {
353
+ const value = requireNext(argv, ++index, arg);
354
+ if (value !== "local-docker" && value !== "aws-ecs") {
355
+ throw new Error("--target must be local-docker or aws-ecs");
356
+ }
357
+ target = value;
358
+ break;
359
+ }
360
+ case "--checkpoint":
361
+ checkpointPath = requireNext(argv, ++index, arg);
362
+ break;
363
+ case "--checkpoint-digest":
364
+ checkpointDigest = requireNext(argv, ++index, arg);
365
+ if (!/^[0-9a-f]{64}$/.test(checkpointDigest)) {
366
+ throw new Error("--checkpoint-digest must be a lowercase SHA-256 digest");
367
+ }
368
+ break;
369
+ case "--provision-receipt":
370
+ provisionReceiptPath = requireNext(argv, ++index, arg);
371
+ break;
372
+ case "--var": {
373
+ const pair = requireNext(argv, ++index, arg);
374
+ const separator = pair.indexOf("=");
375
+ if (separator < 1)
376
+ throw new Error("--var requires name=value");
377
+ variables[pair.slice(0, separator)] = parseVariable(pair.slice(separator + 1));
378
+ break;
379
+ }
380
+ case "--var-env": {
381
+ const pair = requireNext(argv, ++index, arg);
382
+ const separator = pair.indexOf("=");
383
+ if (separator < 1)
384
+ throw new Error("--var-env requires name=ENV_NAME");
385
+ const name = pair.slice(0, separator);
386
+ const envName = pair.slice(separator + 1);
387
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName))
388
+ throw new Error("--var-env requires a valid environment name");
389
+ const value = env[envName];
390
+ if (!value)
391
+ throw new Error(`--var-env ${name} references a missing or empty environment value`);
392
+ variables[name] = parseVariable(value);
393
+ break;
394
+ }
395
+ case "--console-receipt": {
396
+ consoleReceiptPath = requireNext(argv, ++index, arg);
397
+ receiptPaths["console-approval"] = consoleReceiptPath;
398
+ break;
399
+ }
400
+ case "--help":
401
+ printHelp();
402
+ throw new HelpRequested();
403
+ default:
404
+ throw new Error(`Unknown argument: ${arg}`);
405
+ }
406
+ }
407
+ return {
408
+ execute,
409
+ confirmed,
410
+ planPath,
411
+ outputPath,
412
+ honuaCommand,
413
+ target,
414
+ ...(mcpUrl ? { mcpUrl } : {}),
415
+ ...(checkpointPath ? { checkpointPath } : {}),
416
+ ...(checkpointDigest ? { checkpointDigest } : {}),
417
+ ...(provisionReceiptPath ? { provisionReceiptPath } : {}),
418
+ ...(consoleReceiptPath ? { consoleReceiptPath } : {}),
419
+ ...(env.HONUA_SOURCE_REVISION ? { sourceRevision: env.HONUA_SOURCE_REVISION } : {}),
420
+ variables,
421
+ receiptPaths,
422
+ };
423
+ }
424
+ async function prepareLiveOptions(options, env) {
425
+ if (!options.sourceRevision || !/^[0-9a-f]{40}$/.test(options.sourceRevision)) {
426
+ throw new Error("HONUA_SOURCE_REVISION must be the exact 40-character SDK candidate SHA");
427
+ }
428
+ if (!options.mcpUrl)
429
+ throw new Error("--mcp-url is required for live execution");
430
+ requireSecureCredentialEndpoint(options.mcpUrl, "--mcp-url");
431
+ resolveJourneyProxyOptions(options.mcpUrl, env);
432
+ requireBindingVariable(options.variables, "candidateId");
433
+ requireBindingVariable(options.variables, "releaseId");
434
+ if (options.target === "local-docker") {
435
+ if (options.provisionReceiptPath)
436
+ throw new Error("--provision-receipt is valid only with --target aws-ecs");
437
+ return { ...options, honuaCommand: await resolveLiveExecutable(options.honuaCommand, env, process.cwd()) };
438
+ }
439
+ if (!options.provisionReceiptPath)
440
+ throw new Error("--target aws-ecs requires --provision-receipt");
441
+ const provisionBytes = await readFile(options.provisionReceiptPath);
442
+ const provisionBinding = parseAwsEcsProvisionBinding(JSON.parse(provisionBytes.toString("utf8")));
443
+ assertAwsEcsProvisionBindings(provisionBinding, {
444
+ candidateId: requireBindingVariable(options.variables, "candidateId"),
445
+ releaseId: requireBindingVariable(options.variables, "releaseId"),
446
+ mcpUrl: options.mcpUrl,
447
+ });
448
+ return {
449
+ ...options,
450
+ provisionBinding,
451
+ provisionReceiptSha256: createHash("sha256").update(provisionBytes).digest("hex"),
452
+ };
453
+ }
454
+ function resolveJourneyProxyOptions(mcpUrl, env) {
455
+ return resolveProxyOptions({
456
+ HONUA_MCP_REMOTE_URL: mcpUrl,
457
+ HONUA_MCP_AUTH_TOKEN: env.HONUA_MCP_AUTH_TOKEN,
458
+ HONUA_ADMIN_KEY: env.HONUA_ADMIN_KEY,
459
+ HONUA_API_KEY: env.HONUA_API_KEY,
460
+ });
461
+ }
462
+ /** Resolve the live mutating CLI without permitting Windows' cwd-first executable search. */
463
+ export async function resolveLiveExecutable(command, env, excludedDirectory) {
464
+ if (path.isAbsolute(command))
465
+ return verifyLiveExecutable(command, excludedDirectory);
466
+ if (command.includes("/") || command.includes("\\")) {
467
+ throw new Error(`Live Honua CLI path must be absolute: ${command}`);
468
+ }
469
+ const names = process.platform === "win32" ? windowsExecutableNames(command, env.PATHEXT) : [command];
470
+ for (const rawDirectory of (env.PATH ?? "").split(path.delimiter)) {
471
+ const directory = rawDirectory.trim().replace(/^"|"$/g, "");
472
+ if (!directory || !path.isAbsolute(directory))
473
+ continue;
474
+ for (const name of names) {
475
+ try {
476
+ return await verifyLiveExecutable(path.join(directory, name), excludedDirectory);
477
+ }
478
+ catch {
479
+ // Continue through the absolute PATH roster, never the working directory.
480
+ }
481
+ }
482
+ }
483
+ throw new Error(`Live Honua CLI was not found in an absolute PATH entry: ${command}`);
484
+ }
485
+ function windowsExecutableNames(command, pathExt) {
486
+ if (path.extname(command))
487
+ return [command];
488
+ return [...new Set((pathExt ?? ".COM;.EXE;.BAT;.CMD").split(";").map((value) => value.trim().toLowerCase()))]
489
+ .filter((extension) => /^\.[a-z0-9]+$/.test(extension))
490
+ .map((extension) => `${command}${extension}`);
491
+ }
492
+ async function verifyLiveExecutable(candidate, excludedDirectory) {
493
+ const [resolved, excluded] = await Promise.all([
494
+ realpath(candidate),
495
+ realpath(excludedDirectory).catch(() => path.resolve(excludedDirectory)),
496
+ ]);
497
+ const relative = path.relative(excluded, resolved);
498
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
499
+ throw new Error("Refusing to execute the live Honua CLI from the working directory.");
500
+ }
501
+ const metadata = await lstat(resolved);
502
+ if (!metadata.isFile())
503
+ throw new Error("Live Honua CLI is not a regular file.");
504
+ if (process.platform !== "win32")
505
+ await access(resolved, constants.X_OK);
506
+ return resolved;
507
+ }
508
+ function checkpointBindings(options, journeyId, releaseContract, planBytes) {
509
+ if (!options.sourceRevision || !options.mcpUrl)
510
+ throw new Error("live checkpoint bindings are incomplete");
511
+ return {
512
+ journeyId,
513
+ releaseContract,
514
+ target: options.target,
515
+ planSha256: createHash("sha256").update(planBytes).digest("hex"),
516
+ sourceRevision: options.sourceRevision,
517
+ mcpEndpointSha256: createHash("sha256").update(normalizeEndpoint(options.mcpUrl)).digest("hex"),
518
+ candidateId: requireBindingVariable(options.variables, "candidateId"),
519
+ releaseId: requireBindingVariable(options.variables, "releaseId"),
520
+ ...(options.provisionReceiptSha256 ? { provisionReceiptSha256: options.provisionReceiptSha256 } : {}),
521
+ };
522
+ }
523
+ function normalizeEndpoint(value) {
524
+ const url = new URL(value);
525
+ url.hash = "";
526
+ return url.toString().replace(/\/$/, "");
527
+ }
528
+ function requireBindingVariable(variables, name) {
529
+ const value = variables[name];
530
+ if (typeof value !== "string" || !value)
531
+ throw new Error(`--var ${name}=... is required for live execution`);
532
+ return value;
533
+ }
534
+ async function readCheckpointIfPresent(path) {
535
+ try {
536
+ return parseZeroToMapCheckpoint(JSON.parse(await readFile(path, "utf8")));
537
+ }
538
+ catch (error) {
539
+ if (error.code === "ENOENT")
540
+ return undefined;
541
+ throw error;
542
+ }
543
+ }
544
+ async function writeJsonNew(path, value) {
545
+ await mkdir(dirname(path), { recursive: true });
546
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
547
+ }
548
+ /** Atomically transfer one paused checkpoint to a single resume owner. */
549
+ export async function claimZeroToMapCheckpoint(path, digest) {
550
+ if (!/^[0-9a-f]{64}$/.test(digest))
551
+ throw new Error("checkpoint claim digest must be a lowercase SHA-256");
552
+ const checkpoint = await readCheckpointIfPresent(path);
553
+ if (!checkpoint)
554
+ throw new Error("checkpoint is already claimed by another resume");
555
+ if (checkpoint.state !== "paused")
556
+ throw new Error("checkpoint has already been consumed");
557
+ assertZeroToMapCheckpointDigest(checkpoint, digest);
558
+ const claimPath = `${path}.claimed-${digest}`;
559
+ const lockPath = `${claimPath}.lock`;
560
+ let lockCreated = false;
561
+ try {
562
+ const lock = await open(lockPath, "wx");
563
+ await lock.close();
564
+ lockCreated = true;
565
+ await rename(path, claimPath);
566
+ return claimPath;
567
+ }
568
+ catch (error) {
569
+ if (lockCreated)
570
+ await unlink(lockPath).catch(() => undefined);
571
+ const code = error.code;
572
+ throw new Error(code === "ENOENT" || code === "EEXIST"
573
+ ? "checkpoint is already claimed by another resume"
574
+ : `could not atomically claim checkpoint: ${error instanceof Error ? error.message : String(error)}`);
575
+ }
576
+ }
577
+ function runProcess(command, args, env) {
578
+ return new Promise((resolve, reject) => {
579
+ const childEnv = process.platform === "win32" ? { ...env, NoDefaultCurrentDirectoryInExePath: "1" } : env;
580
+ const isWindowsShim = process.platform === "win32" && [".bat", ".cmd"].includes(path.extname(command).toLowerCase());
581
+ const child = spawn(isWindowsShim ? "C:\\Windows\\System32\\cmd.exe" : command, isWindowsShim ? ["/d", "/s", "/c", windowsCommandLine([command, ...args])] : [...args], {
582
+ env: childEnv,
583
+ stdio: ["ignore", "pipe", "pipe"],
584
+ windowsHide: true,
585
+ });
586
+ const stdout = [];
587
+ const stderr = [];
588
+ let outputBytes = 0;
589
+ let outputExceeded = false;
590
+ const collect = (target, chunk) => {
591
+ outputBytes += chunk.byteLength;
592
+ if (outputBytes > 1024 * 1024) {
593
+ outputExceeded = true;
594
+ child.kill();
595
+ return;
596
+ }
597
+ target.push(chunk);
598
+ };
599
+ child.stdout.on("data", (chunk) => collect(stdout, chunk));
600
+ child.stderr.on("data", (chunk) => collect(stderr, chunk));
601
+ child.once("error", (error) => {
602
+ const code = error.code;
603
+ if (code === "ENOENT") {
604
+ reject(new JourneyBlockedError(`Control-plane CLI command was not found: ${command}`, "control-plane-cli-missing"));
605
+ }
606
+ else {
607
+ reject(error);
608
+ }
609
+ });
610
+ child.once("close", (code) => {
611
+ if (outputExceeded) {
612
+ reject(new Error("honua CLI exceeded the bounded command-output limit; output was suppressed"));
613
+ return;
614
+ }
615
+ resolve({
616
+ exitCode: code ?? 1,
617
+ stdout: Buffer.concat(stdout).toString("utf8"),
618
+ stderr: Buffer.concat(stderr).toString("utf8"),
619
+ });
620
+ });
621
+ });
622
+ }
623
+ function windowsCommandLine(args) {
624
+ return args.map((value) => `"${value.replaceAll('"', '\\"')}"`).join(" ");
625
+ }
626
+ export function parseInstallAccessCredential(stdout) {
627
+ let parsed;
628
+ try {
629
+ parsed = JSON.parse(stdout);
630
+ }
631
+ catch {
632
+ throw new Error("local installer did not emit a valid secret-free access credential receipt");
633
+ }
634
+ const root = releaseRecord(parsed, "local installer receipt");
635
+ releaseExactKeys(root, [
636
+ "accessCredential",
637
+ "adminKeyWritten",
638
+ "baseUrl",
639
+ "claudeDesktopConfigFile",
640
+ "composeFile",
641
+ "directory",
642
+ "envFile",
643
+ "mcpConfigFile",
644
+ "profile",
645
+ "readyUrl",
646
+ "reused",
647
+ "serverImage",
648
+ "status",
649
+ ], "local installer receipt");
650
+ const credential = releaseRecord(root.accessCredential, "local installer access credential");
651
+ releaseExactKeys(credential, [
652
+ "canAuthenticate",
653
+ "effectiveGrants",
654
+ "id",
655
+ "name",
656
+ "provisioned",
657
+ "referenceDigestSha256",
658
+ "referenceType",
659
+ "requestedGrants",
660
+ "status",
661
+ ], "local installer access credential");
662
+ const id = releaseText(credential.id, "local installer access credential id");
663
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)) {
664
+ throw new Error("local installer access credential id is invalid");
665
+ }
666
+ const requestedGrants = releaseStringList(credential.requestedGrants, "local installer access credential requested grants");
667
+ const effectiveGrants = releaseStringList(credential.effectiveGrants, "local installer access credential effective grants");
668
+ if (stableStringSet(requestedGrants) !== stableStringSet(effectiveGrants)) {
669
+ throw new Error("local installer requested and effective access grants do not match");
670
+ }
671
+ if (stableStringSet(requestedGrants) !== stableStringSet(ZERO_TO_MAP_ACCESS_GRANTS)) {
672
+ throw new Error("local installer access credential is not scoped to the required release grants");
673
+ }
674
+ if (credential.status !== "active" ||
675
+ credential.canAuthenticate !== true ||
676
+ credential.referenceType !== "private-env-file" ||
677
+ typeof credential.provisioned !== "boolean") {
678
+ throw new Error("local installer access credential is not an active private-file handoff");
679
+ }
680
+ const referenceDigestSha256 = releaseText(credential.referenceDigestSha256, "local installer access credential reference digest");
681
+ if (!/^[0-9a-f]{64}$/.test(referenceDigestSha256)) {
682
+ throw new Error("local installer access credential reference digest is invalid");
683
+ }
684
+ return {
685
+ id,
686
+ name: releaseText(credential.name, "local installer access credential name"),
687
+ status: "active",
688
+ requestedGrants,
689
+ effectiveGrants,
690
+ canAuthenticate: true,
691
+ referenceType: "private-env-file",
692
+ referenceDigestSha256,
693
+ provisioned: credential.provisioned,
694
+ };
695
+ }
696
+ function releaseRecord(value, path) {
697
+ if (!value || typeof value !== "object" || Array.isArray(value))
698
+ throw new Error(`${path} must be an object`);
699
+ return value;
700
+ }
701
+ function releaseExactKeys(value, expected, path) {
702
+ const actual = Object.keys(value).sort();
703
+ const allowed = [...expected].sort();
704
+ if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) {
705
+ throw new Error(`${path} contains unexpected or missing fields`);
706
+ }
707
+ }
708
+ function releaseText(value, path) {
709
+ if (typeof value !== "string" || !value)
710
+ throw new Error(`${path} must be a non-empty string`);
711
+ return value;
712
+ }
713
+ function releaseStringList(value, path) {
714
+ if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || !item)) {
715
+ throw new Error(`${path} must be a non-empty string array`);
716
+ }
717
+ return value;
718
+ }
719
+ function stableStringSet(value) {
720
+ return [...new Set(value)].sort().join("\n");
721
+ }
722
+ function toolErrorMessage(value) {
723
+ if (value && typeof value === "object") {
724
+ const result = value;
725
+ if (result.structuredContent)
726
+ return JSON.stringify(result.structuredContent);
727
+ if (Array.isArray(result.content)) {
728
+ const text = result.content.find((block) => Boolean(block) && typeof block === "object" && block.type === "text");
729
+ if (text && typeof text.text === "string")
730
+ return text.text;
731
+ }
732
+ }
733
+ return "no structured error was returned";
734
+ }
735
+ /** Poll one MCP job resource to its declared successful terminal state. */
736
+ export async function readJourneyMcpResource(action, read) {
737
+ const startedAt = Date.now();
738
+ for (;;) {
739
+ const result = await read(action.uri);
740
+ const body = readJsonResource(result, action.uri);
741
+ const wait = action.waitFor;
742
+ if (!wait)
743
+ return { value: result, evidence: { uri: action.uri } };
744
+ const actual = resourceJsonPointer(body, wait.pointer);
745
+ if (wait.terminal.includes(String(actual))) {
746
+ if (actual !== wait.equals) {
747
+ throw new Error(`${action.uri} reached terminal ${String(actual)}; expected ${wait.equals}`);
748
+ }
749
+ return {
750
+ value: result,
751
+ evidence: { uri: action.uri, [wait.pointer.slice(1) || "value"]: actual },
752
+ };
753
+ }
754
+ if (Date.now() - startedAt >= wait.deadlineMs) {
755
+ throw new Error(`${action.uri} did not reach ${wait.equals} within ${wait.deadlineMs}ms`);
756
+ }
757
+ await new Promise((resolve) => setTimeout(resolve, wait.pollIntervalMs));
758
+ }
759
+ }
760
+ function readJsonResource(value, uri) {
761
+ if (!value || typeof value !== "object")
762
+ throw new Error(`${uri} returned an invalid resources/read response`);
763
+ const contents = value.contents;
764
+ if (!Array.isArray(contents))
765
+ throw new Error(`${uri} returned no resource contents`);
766
+ const content = contents.find((candidate) => Boolean(candidate) && typeof candidate === "object" && typeof candidate.text === "string");
767
+ if (!content)
768
+ throw new Error(`${uri} returned no JSON text content`);
769
+ try {
770
+ return JSON.parse(content.text);
771
+ }
772
+ catch {
773
+ throw new Error(`${uri} returned invalid JSON text content`);
774
+ }
775
+ }
776
+ function resourceJsonPointer(value, pointer) {
777
+ if (pointer === "")
778
+ return value;
779
+ if (!pointer.startsWith("/"))
780
+ throw new Error(`invalid resource JSON pointer: ${pointer}`);
781
+ let current = value;
782
+ for (const raw of pointer.slice(1).split("/")) {
783
+ const key = raw.replaceAll("~1", "/").replaceAll("~0", "~");
784
+ if (!current || typeof current !== "object" || Array.isArray(current))
785
+ return undefined;
786
+ current = current[key];
787
+ }
788
+ return current;
789
+ }
790
+ export function requireCompletedPublishedOperation(tool, value) {
791
+ const operation = publishedOperation(value);
792
+ if (!operation)
793
+ return undefined;
794
+ const status = operation.status.toLowerCase();
795
+ if (status === "requiresapproval" || status === "queued") {
796
+ throw new JourneyBlockedError(`${tool} returned ${operation.status}; handle=${operation.handleId ?? "unknown"}, approvalLane=${operation.approvalLane ?? "none"}, job=${operation.jobId ?? "none"}.`, status === "requiresapproval" ? "operation-approval-required" : "operation-queued");
797
+ }
798
+ if (status !== "completed") {
799
+ throw new Error(`${tool} operation ${operation.operationId} returned status ${operation.status}`);
800
+ }
801
+ return operation;
802
+ }
803
+ function publishedOperation(value) {
804
+ if (!value || typeof value !== "object")
805
+ return undefined;
806
+ const structured = value.structuredContent;
807
+ if (!structured || typeof structured !== "object" || Array.isArray(structured))
808
+ return undefined;
809
+ const body = structured;
810
+ if (typeof body.operationId !== "string" || !body.operationId.startsWith("admin."))
811
+ return undefined;
812
+ if (typeof body.status !== "string")
813
+ throw new Error(`${body.operationId} omitted its published-operation status`);
814
+ return {
815
+ operationId: body.operationId,
816
+ status: body.status,
817
+ ...(typeof body.handleId === "string" ? { handleId: body.handleId } : {}),
818
+ ...(typeof body.approvalLane === "string" ? { approvalLane: body.approvalLane } : {}),
819
+ ...(typeof body.jobId === "string" ? { jobId: body.jobId } : {}),
820
+ ...(typeof body.metadataRevision === "number" ? { metadataRevision: body.metadataRevision } : {}),
821
+ };
822
+ }
823
+ function redactCliArgs(args) {
824
+ const redacted = [...args];
825
+ for (let index = 0; index < redacted.length; index += 1) {
826
+ const argument = redacted[index] ?? "";
827
+ if (["--api-key", "--admin-key", "--password", "--token"].includes(argument)) {
828
+ if (index + 1 < redacted.length)
829
+ redacted[index + 1] = "[REDACTED]";
830
+ }
831
+ else if (["--api-key", "--admin-key", "--password", "--token"].some((flag) => argument.startsWith(`${flag}=`))) {
832
+ redacted[index] = `${argument.slice(0, argument.indexOf("=") + 1)}[REDACTED]`;
833
+ }
834
+ }
835
+ return redacted;
836
+ }
837
+ function parseVariable(value) {
838
+ if (value === "true")
839
+ return true;
840
+ if (value === "false")
841
+ return false;
842
+ if (/^-?\d+(?:\.\d+)?$/.test(value))
843
+ return Number(value);
844
+ return value;
845
+ }
846
+ function requireNext(argv, index, option) {
847
+ const value = argv[index];
848
+ if (!value)
849
+ throw new Error(`${option} requires a value`);
850
+ return value;
851
+ }
852
+ function printHelp() {
853
+ process.stdout.write("Usage: honua-zero-to-map-release [options]\n\n");
854
+ process.stdout.write("Without --execute, validates the plan and writes an explicitly blocked contract receipt.\n\n");
855
+ process.stdout.write(" --execute --yes Run the state-changing candidate journey\n");
856
+ process.stdout.write(" --plan <path> Override the checked-in journey plan\n");
857
+ process.stdout.write(" --output <path> Receipt output (default zero-to-map-receipt.json)\n");
858
+ process.stdout.write(" --mcp-url <url> Server /mcp URL consumed through honua-mcp-proxy\n");
859
+ process.stdout.write(" --target <name> local-docker (default) or aws-ecs\n");
860
+ process.stdout.write(" --provision-receipt <p> Required pre-teardown binding for aws-ecs\n");
861
+ process.stdout.write(" --honua-command <path> Control-plane CLI executable (default honua)\n");
862
+ process.stdout.write(" --var name=value Supply a journey variable; repeat as needed\n");
863
+ process.stdout.write(" --var-env name=ENV_NAME Read a secret variable from the child environment\n");
864
+ process.stdout.write(" --checkpoint <path> Persist the secret-free Console pause/resume boundary\n");
865
+ process.stdout.write(" --checkpoint-digest <sha> Externally carried digest required to claim a resume\n");
866
+ process.stdout.write(" --console-receipt <path> Import the separately captured Console gate receipt\n");
867
+ }
868
+ class HelpRequested extends Error {
869
+ }
870
+ if (isMainEntrypoint(import.meta.url)) {
871
+ runZeroToMapCli().then((code) => {
872
+ process.exitCode = code;
873
+ }, (error) => {
874
+ if (error instanceof HelpRequested) {
875
+ process.exitCode = 0;
876
+ return;
877
+ }
878
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
879
+ process.exitCode = 1;
880
+ });
881
+ }