@honua/mcp-server 0.2.3 → 0.2.5

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 (32) hide show
  1. package/README.md +63 -0
  2. package/dist/src/agent/cli.d.ts +3 -0
  3. package/dist/src/agent/cli.d.ts.map +1 -0
  4. package/dist/src/agent/cli.js +33 -0
  5. package/dist/src/agent/devops-configuration.d.ts +5 -0
  6. package/dist/src/agent/devops-configuration.d.ts.map +1 -0
  7. package/dist/src/agent/devops-configuration.js +130 -0
  8. package/dist/src/agent/devops-runtime.d.ts +42 -0
  9. package/dist/src/agent/devops-runtime.d.ts.map +1 -0
  10. package/dist/src/agent/devops-runtime.js +242 -0
  11. package/dist/src/agent/devops-targets.d.ts +33 -0
  12. package/dist/src/agent/devops-targets.d.ts.map +1 -0
  13. package/dist/src/agent/devops-targets.js +25 -0
  14. package/dist/src/agent/gateway.d.ts +58 -0
  15. package/dist/src/agent/gateway.d.ts.map +1 -0
  16. package/dist/src/agent/gateway.js +381 -0
  17. package/dist/src/agent/local-runtime.d.ts +26 -0
  18. package/dist/src/agent/local-runtime.d.ts.map +1 -0
  19. package/dist/src/agent/local-runtime.js +152 -0
  20. package/dist/src/agent/native-cli.d.ts +4 -0
  21. package/dist/src/agent/native-cli.d.ts.map +1 -0
  22. package/dist/src/agent/native-cli.js +49 -0
  23. package/dist/src/agent/native-http.d.ts +18 -0
  24. package/dist/src/agent/native-http.d.ts.map +1 -0
  25. package/dist/src/agent/native-http.js +261 -0
  26. package/dist/src/agent/workflow-knowledge.d.ts +73 -0
  27. package/dist/src/agent/workflow-knowledge.d.ts.map +1 -0
  28. package/dist/src/agent/workflow-knowledge.js +230 -0
  29. package/package.json +4 -2
  30. package/release/zero-to-map/configs/claude-code.mcp.json +1 -1
  31. package/release/zero-to-map/configs/claude-desktop.json +1 -1
  32. package/release/zero-to-map/configs/cursor.json +1 -1
package/README.md CHANGED
@@ -29,6 +29,69 @@ Two modes:
29
29
 
30
30
  ## Requirements
31
31
 
32
+ ### Persistent local agent connector — source preview
33
+
34
+ This branch adds `honua-agent-mcp`: one stdio connection that starts with local
35
+ setup tools and attaches the installed Honua server's canonical MCP surface
36
+ without replacing the client's registration. It is a source preview, not yet
37
+ the qualified public Docker-to-map getting-started path.
38
+
39
+ Start the built `dist/src/agent/cli.js` with Node from this package. The host sets
40
+ `HONUA_AGENT_INSTALL_DIRECTORY` (default `~/.honua/local`); the agent cannot
41
+ choose arbitrary credential files or remote URLs. Node and a local Unix-socket
42
+ Docker engine with Compose 2.23.1 or later are required. Linux/WSL is the initial
43
+ lane; Windows named pipes and remote Docker contexts are refused.
44
+
45
+ Prompt: **"Check this machine, then set up Honua locally in Docker. Show me the
46
+ target and available workflows when it is ready."** The agent calls
47
+ `honua_agent_inspect_local`, obtains installation authorization, and calls
48
+ `honua_agent_install_local` with `confirm: true`. Keep the installation tool
49
+ timeout long enough for image pulls and database initialization (ten minutes is
50
+ a useful starting point). If the client times out, inspect `honua_agent_status`
51
+ and the installation before issuing another install. Cancellation of a local
52
+ installation does not undo Docker work already started.
53
+
54
+ The connector verifies readiness and the installed credential before announcing
55
+ the server's tools. Existing installations attach on startup; after a transport
56
+ disconnect, `honua_agent_connect` verifies the same target again. A changed
57
+ credential identity requires a host restart. It never retries an upstream
58
+ mutation after an uncertain response.
59
+
60
+ Credentials stay in the installer's protected file and the HTTP transport. The
61
+ local agent key has `admin:read`, `admin:write`, and `read:*`; it has no
62
+ `admin:approve` or feature-write grant. Every upstream request carries that key,
63
+ and the server enforces current RBAC, resource access, and governance. Discovery
64
+ is not authorization; a denied operation remains denied. Browser human sessions
65
+ and their approval authority are separate.
66
+
67
+ This preview forwards canonical paginated tools, resources, resource templates,
68
+ prompts, request metadata, tool progress, cancellation, and catalog-change
69
+ notifications. It negotiates its own MCP identity and preserves the configured
70
+ workflow view. It does not advertise subscriptions, sampling, roots,
71
+ completions, or tasks. Durable workflow context, command reconciliation, browser
72
+ pairing, and coordinated Console/Studio artifacts remain tracked in
73
+ [the unified workspace workstream](https://github.com/honua-io/honua-server/issues/6297).
74
+
75
+ After building the SDK and this package, maintainers can run the opt-in Docker
76
+ handoff and process-restart check with
77
+ `HONUA_AGENT_LIVE_ENABLED=true npm run test:unit --prefix mcp -- test/agent-local-live.test.ts`
78
+ from the repository root. It creates and removes its own isolated Compose project.
79
+ This tests the built connector against the installer's pinned server image; it
80
+ does not certify the full map-making or browser journey.
81
+
82
+ The packaged workflow guide is available as the MCP resource
83
+ `honua://agent/knowledge/workflows/v1`, even before installation. Optional private
84
+ DevOps tools can share this registration through host configuration; see
85
+ [the host reference](../docs/unified-agent-host-configuration.md) for target and
86
+ credential requirements and the current qualification limits.
87
+
88
+ Native Console chat can use the preview `honua-agent-gateway` backend HTTP
89
+ transport to reach this same public/private catalog. Its separate bridge key,
90
+ workspace-turn checks and host configuration are described in that reference.
91
+ External agents continue to use their single `honua-agent-mcp` registration.
92
+
93
+ ### Existing standalone and proxy modes
94
+
32
95
  - Node.js `>=22.12`
33
96
  - Any reachable FeatureServer/OGC endpoint (public or Honua)
34
97
 
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export declare function runAgent(env?: NodeJS.ProcessEnv): Promise<void>;
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../../src/agent/cli.ts"],"names":[],"mappings":";AASA,wBAAsB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAalF"}
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { isMainEntrypoint } from "../entrypoint.js";
6
+ import { configuredDevOps } from "./devops-configuration.js";
7
+ import { HonuaAgentGateway } from "./gateway.js";
8
+ import { createLocalAgentRuntime, nativeWorkspaceFromEnv } from "./local-runtime.js";
9
+ export async function runAgent(env = process.env) {
10
+ const directory = env.HONUA_AGENT_INSTALL_DIRECTORY ?? path.join(homedir(), ".honua", "local");
11
+ const runtime = createLocalAgentRuntime(directory, nativeWorkspaceFromEnv(env));
12
+ const devops = env.HONUA_AGENT_DEVOPS_CONFIG
13
+ ? await configuredDevOps(env.HONUA_AGENT_DEVOPS_CONFIG, runtime)
14
+ : undefined;
15
+ const gateway = new HonuaAgentGateway(runtime, undefined, devops);
16
+ const close = () => {
17
+ void gateway.close().catch(() => { });
18
+ };
19
+ process.stdin.once("end", close);
20
+ process.stdin.once("close", close);
21
+ await gateway.connect(new StdioServerTransport());
22
+ }
23
+ if (isMainEntrypoint(import.meta.url)) {
24
+ if (process.argv.includes("--help")) {
25
+ process.stdout.write("honua-agent-mcp — persistent local Honua MCP gateway\nConfigure HONUA_AGENT_INSTALL_DIRECTORY in the host; default ~/.honua/local.\n");
26
+ }
27
+ else {
28
+ runAgent().catch(() => {
29
+ process.stderr.write("Honua agent connector failed; inspect local configuration.\n");
30
+ process.exitCode = 1;
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,5 @@
1
+ import { type DevOpsExtension } from "./devops-runtime.js";
2
+ import type { AgentRuntime } from "./local-runtime.js";
3
+ /** The host selects this protected file; MCP arguments cannot select executable paths or credentials. */
4
+ export declare function configuredDevOps(file: string, runtime: AgentRuntime, fetchImpl?: typeof fetch): Promise<DevOpsExtension>;
5
+ //# sourceMappingURL=devops-configuration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devops-configuration.d.ts","sourceRoot":"","sources":["../../../src/agent/devops-configuration.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAyB,MAAM,qBAAqB,CAAC;AAElF,OAAO,KAAK,EAAmB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAyBxE,yGAAyG;AACzG,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,EACrB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,eAAe,CAAC,CAoG1B"}
@@ -0,0 +1,130 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { buildUpstreamHeaders } from "../proxy.js";
5
+ import { createDevOpsExtension } from "./devops-runtime.js";
6
+ import { devOpsTarget } from "./devops-targets.js";
7
+ const configuration = z
8
+ .object({
9
+ serverTargetId: z.string().min(1).max(128),
10
+ environment: z.string().min(1).max(128),
11
+ deployTargetId: z.string().min(1).max(128),
12
+ runtimeKind: z.enum(["aws-ecs", "aws-lambda", "aws-eks"]).default("aws-ecs"),
13
+ command: z.string().min(1),
14
+ args: z.array(z.string()).max(64).optional(),
15
+ cwd: z.string().optional(),
16
+ env: z.record(z.string(), z.string()).optional(),
17
+ fixedArguments: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
18
+ provisioningScope: z
19
+ .object({
20
+ environment: z.string().min(1),
21
+ region: z.string().min(1),
22
+ namePrefix: z.string().min(1),
23
+ allowedImages: z.array(z.string().min(1)).min(1),
24
+ })
25
+ .strict()
26
+ .optional(),
27
+ })
28
+ .strict();
29
+ /** The host selects this protected file; MCP arguments cannot select executable paths or credentials. */
30
+ export async function configuredDevOps(file, runtime, fetchImpl = fetch) {
31
+ if (!path.isAbsolute(file))
32
+ throw new Error("DevOps configuration requires an absolute host path");
33
+ const info = await lstat(file);
34
+ if (!info.isFile() || info.size > 65536 || (process.platform !== "win32" && (info.mode & 0o077) !== 0))
35
+ throw new Error("DevOps configuration must be a private, bounded regular file");
36
+ const config = configuration.parse(JSON.parse(await readFile(file, "utf8")));
37
+ const target = devOpsTarget(config.runtimeKind);
38
+ if (!path.isAbsolute(config.command) || (config.cwd && !path.isAbsolute(config.cwd)))
39
+ throw new Error("DevOps executable and working directory must be host-selected absolute paths");
40
+ if (config.provisioningScope && config.provisioningScope.environment !== config.environment)
41
+ throw new Error("Provisioning scope must match the configured environment");
42
+ let extension;
43
+ let closed = false;
44
+ async function get() {
45
+ if (closed)
46
+ throw new Error("Private integration closed");
47
+ if (!extension) {
48
+ extension = (async () => {
49
+ const connection = await runtime.resolve();
50
+ const base = new URL(connection.transport.remoteUrl);
51
+ if (!/\/mcp\/?$/.test(base.pathname))
52
+ throw new Error("Private integration requires the canonical MCP endpoint");
53
+ base.pathname = base.pathname.replace(/mcp\/?$/, "");
54
+ base.search = "";
55
+ base.hash = "";
56
+ // Child operations use this same scoped agent credential. A separately configured
57
+ // human/admin key must never become a silent authority fallback.
58
+ if (!connection.transport.apiKey)
59
+ throw new Error("A scoped private integration credential is required");
60
+ const authorize = async (action) => {
61
+ const current = await runtime.resolve();
62
+ requireSameConnection(connection, current);
63
+ const response = await fetchImpl(new URL("api/v1/agent/devops/authorize", base), {
64
+ method: "POST",
65
+ redirect: "error",
66
+ signal: AbortSignal.timeout(10000),
67
+ headers: { ...buildUpstreamHeaders(current.transport), "Content-Type": "application/json" },
68
+ body: JSON.stringify({
69
+ targetId: config.serverTargetId,
70
+ deployTargetId: config.deployTargetId,
71
+ environment: config.environment,
72
+ canonicalTool: action.canonicalTool,
73
+ arguments: action.arguments,
74
+ }),
75
+ });
76
+ if (!response.ok)
77
+ throw new Error("Private integration unauthorized");
78
+ const receipt = (await response.json());
79
+ if (receipt.targetId !== config.serverTargetId ||
80
+ receipt.deployTargetId !== config.deployTargetId ||
81
+ receipt.environment !== config.environment)
82
+ throw new Error("Private integration target changed");
83
+ const legacyEcs = config.runtimeKind === "aws-ecs" && receipt.runtimeKind === undefined && receipt.backend === undefined;
84
+ if (!legacyEcs && (receipt.runtimeKind !== config.runtimeKind || receipt.backend !== target.backend))
85
+ throw new Error("Private integration runtime changed or is unsupported");
86
+ };
87
+ await authorize({ canonicalTool: null });
88
+ return createDevOpsExtension({
89
+ ...config,
90
+ env: {
91
+ ...config.env,
92
+ HONUA_DEVOPS_HONUA_API_BASE_URL: base.toString().replace(/\/$/, ""),
93
+ HONUA_DEVOPS_HONUA_API_KEY: connection.transport.apiKey,
94
+ HONUA_DEVOPS_TERRAFORM_TARGETS: target.terraformTarget,
95
+ HONUA_DEVOPS_RUNTIME_KIND: config.runtimeKind,
96
+ HONUA_DEVOPS_RUNTIME_BACKEND: target.backend,
97
+ HONUA_DEVOPS_SERVER_TARGET_ID: config.serverTargetId,
98
+ HONUA_DEVOPS_ALLOWED_ENVIRONMENTS: config.environment,
99
+ HONUA_DEVOPS_DEPLOY_TARGET_ID: config.deployTargetId,
100
+ },
101
+ }, authorize);
102
+ })();
103
+ const attempt = extension;
104
+ void attempt.catch(() => {
105
+ if (extension === attempt)
106
+ extension = undefined;
107
+ });
108
+ }
109
+ return extension;
110
+ }
111
+ return {
112
+ async listTools() {
113
+ return (await get()).listTools();
114
+ },
115
+ async callTool(name, args, options) {
116
+ return (await get()).callTool(name, args, options);
117
+ },
118
+ async close() {
119
+ closed = true;
120
+ await extension?.then((value) => value.close()).catch(() => { });
121
+ },
122
+ };
123
+ }
124
+ function requireSameConnection(expected, actual) {
125
+ if (expected.target.id !== actual.target.id ||
126
+ expected.target.credentialId !== actual.target.credentialId ||
127
+ expected.transport.remoteUrl !== actual.transport.remoteUrl ||
128
+ expected.transport.apiKey !== actual.transport.apiKey)
129
+ throw new Error("Private integration identity changed; restart explicitly");
130
+ }
@@ -0,0 +1,42 @@
1
+ import { type CallToolResult, type Progress, type Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import { type DevOpsRuntimeKind } from "./devops-targets.js";
3
+ export declare const DEVOPS_TOOL_FAMILY = "honua.workspace.devops";
4
+ /** Private provisioning/planning only. Day-2 operations remain on the server's canonical MCP surface. */
5
+ export declare const DEVOPS_TOOL_ALLOWLIST: ReadonlySet<string>;
6
+ export interface DevOpsExtension {
7
+ listTools(): Promise<Tool[]>;
8
+ callTool(name: string, args: Record<string, unknown>, options?: {
9
+ signal?: AbortSignal;
10
+ onprogress?: (progress: Progress) => void;
11
+ }): Promise<CallToolResult>;
12
+ close(): Promise<void>;
13
+ }
14
+ /** Host configuration only. Never accept this configuration from an MCP tool argument. */
15
+ export interface DevOpsProcessConfiguration {
16
+ command: string;
17
+ runtimeKind?: DevOpsRuntimeKind;
18
+ args?: string[];
19
+ cwd?: string;
20
+ /** Explicit child environment, including any operator credentials. Never returned to the model. */
21
+ env?: Record<string, string>;
22
+ /** Host-only bindings; absent path bindings hide handoff tools entirely. */
23
+ fixedArguments?: Record<string, Record<string, unknown>>;
24
+ provisioningScope?: {
25
+ environment: string;
26
+ region: string;
27
+ namePrefix: string;
28
+ allowedImages: string[];
29
+ };
30
+ }
31
+ export type DevOpsAuthorization = (action: {
32
+ canonicalTool: string | null;
33
+ arguments?: Readonly<Record<string, unknown>>;
34
+ }) => Promise<void>;
35
+ /**
36
+ * One optional private subprocess behind the existing public registration. Authorization is
37
+ * rechecked before EVERY discovery/action by the gateway against its current bound principal
38
+ * and target. Canonical handlers still enforce their own approval, audit and exact-plan gates.
39
+ * Errors are intentionally opaque; upstream exceptions may contain credentials or stderr.
40
+ */
41
+ export declare function createDevOpsExtension(configuration: DevOpsProcessConfiguration, authorize: DevOpsAuthorization): DevOpsExtension;
42
+ //# sourceMappingURL=devops-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devops-runtime.d.ts","sourceRoot":"","sources":["../../../src/agent/devops-runtime.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,QAAQ,EACb,KAAK,IAAI,EACV,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,KAAK,iBAAiB,EAAgB,MAAM,qBAAqB,CAAC;AAG3E,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,yGAAyG;AACzG,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAgBpD,CAAC;AAEH,MAAM,WAAW,eAAe;IAC9B,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7B,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;KAAE,GAC5E,OAAO,CAAC,cAAc,CAAC,CAAC;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,0FAA0F;AAC1F,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mGAAmG;IACnG,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,iBAAiB,CAAC,EAAE;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC;CACH;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE;IACzC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC/C,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,0BAA0B,EACzC,SAAS,EAAE,mBAAmB,GAC7B,eAAe,CAoNjB"}
@@ -0,0 +1,242 @@
1
+ import path from "node:path";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { CallToolResultSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { devOpsTarget } from "./devops-targets.js";
6
+ const PREFIX = "honua_agent_devops_";
7
+ export const DEVOPS_TOOL_FAMILY = "honua.workspace.devops";
8
+ /** Private provisioning/planning only. Day-2 operations remain on the server's canonical MCP surface. */
9
+ export const DEVOPS_TOOL_ALLOWLIST = new Set([
10
+ "describe_environment",
11
+ "find_recent_operations",
12
+ "provision_infrastructure",
13
+ "install_handoff",
14
+ "verify_install_handoff",
15
+ "plan_gitops_engine",
16
+ "generate_metadata_release_changeset",
17
+ "explain_metadata_release_changeset",
18
+ "plan_metadata_release_gitops",
19
+ "create_gitops_proposal",
20
+ "get_gitops_proposal",
21
+ "get_devops_operation_status",
22
+ "deploy_service_gitops",
23
+ "create_scoped_gitops_pull_request",
24
+ "revert_scoped_gitops_pull_request",
25
+ ]);
26
+ /**
27
+ * One optional private subprocess behind the existing public registration. Authorization is
28
+ * rechecked before EVERY discovery/action by the gateway against its current bound principal
29
+ * and target. Canonical handlers still enforce their own approval, audit and exact-plan gates.
30
+ * Errors are intentionally opaque; upstream exceptions may contain credentials or stderr.
31
+ */
32
+ export function createDevOpsExtension(configuration, authorize) {
33
+ if (!path.isAbsolute(configuration.command))
34
+ throw new Error("DevOps executable must be an absolute host path");
35
+ const runtimeKind = configuration.runtimeKind ?? "aws-ecs";
36
+ const target = devOpsTarget(runtimeKind);
37
+ // Copy configuration so a caller cannot silently change credentials/commands after binding.
38
+ const config = {
39
+ command: configuration.command,
40
+ args: [...(configuration.args ?? ["--mcp"])],
41
+ ...(configuration.cwd ? { cwd: configuration.cwd } : {}),
42
+ ...(configuration.env ? { env: { ...configuration.env } } : {}),
43
+ };
44
+ const bindings = structuredClone(configuration.fixedArguments ?? {});
45
+ const provisioningScope = configuration.provisioningScope
46
+ ? structuredClone(configuration.provisioningScope)
47
+ : undefined;
48
+ function enabled(name) {
49
+ if (name === "install_handoff")
50
+ return ["baseUrl", "adminKeySecretRef", "outputDirectory"].every((key) => typeof bindings[name]?.[key] === "string" && String(bindings[name][key]).length > 0);
51
+ if (name === "verify_install_handoff")
52
+ return (typeof bindings[name]?.handoffConfigPath === "string" && String(bindings[name].handoffConfigPath).length > 0);
53
+ if (name === "provision_infrastructure")
54
+ return Boolean(provisioningScope?.environment &&
55
+ provisioningScope.region &&
56
+ provisioningScope.namePrefix &&
57
+ provisioningScope.allowedImages.length);
58
+ return true;
59
+ }
60
+ function bindArguments(name, supplied) {
61
+ if (!enabled(name))
62
+ throw new Error("Host scope binding required");
63
+ const effective = structuredClone(supplied);
64
+ for (const [key, value] of Object.entries(bindings[name] ?? {})) {
65
+ if (effective[key] !== undefined &&
66
+ effective[key] !== "" &&
67
+ JSON.stringify(effective[key]) !== JSON.stringify(value))
68
+ throw new Error("Host argument binding mismatch");
69
+ effective[key] = value;
70
+ }
71
+ if (name === "provision_infrastructure") {
72
+ if (effective.stack !== runtimeKind || !provisioningScope)
73
+ throw new Error("Unsupported adapter");
74
+ if (effective.variablesJson !== undefined && typeof effective.variablesJson !== "string")
75
+ throw new Error("Invalid variable document type");
76
+ if (typeof effective.variablesJson === "string" && effective.variablesJson.length > 64 * 1024)
77
+ throw new Error("Variable document exceeds scope limit");
78
+ const variables = JSON.parse(typeof effective.variablesJson === "string" && effective.variablesJson.trim() ? effective.variablesJson : "{}");
79
+ if (!variables || typeof variables !== "object" || Array.isArray(variables))
80
+ throw new Error("Invalid variables");
81
+ const scoped = variables;
82
+ for (const [key, value] of Object.entries({
83
+ environment: provisioningScope.environment,
84
+ region: provisioningScope.region,
85
+ name_prefix: provisioningScope.namePrefix,
86
+ })) {
87
+ if (scoped[key] !== undefined && scoped[key] !== value)
88
+ throw new Error("Cloud target scope mismatch");
89
+ scoped[key] = value;
90
+ }
91
+ const requireImage = (image) => {
92
+ if (typeof image !== "string" || !provisioningScope.allowedImages.includes(image))
93
+ throw new Error("Image outside approved scope");
94
+ };
95
+ // Marketplace install.artifact.image overrides the flat Terraform variable.
96
+ // Validate that effective image too, rather than letting the nested input
97
+ // bypass the host's allowlist.
98
+ if (scoped.install !== undefined) {
99
+ if (!scoped.install || typeof scoped.install !== "object" || Array.isArray(scoped.install))
100
+ throw new Error("Invalid install scope");
101
+ const artifact = scoped.install.artifact;
102
+ if (artifact !== undefined && artifact !== null) {
103
+ if (typeof artifact !== "object" || Array.isArray(artifact))
104
+ throw new Error("Invalid artifact scope");
105
+ const image = artifact.image;
106
+ if (image !== undefined && image !== null)
107
+ requireImage(image);
108
+ }
109
+ }
110
+ if (target.imageVariable) {
111
+ if (scoped[target.imageVariable] !== undefined)
112
+ requireImage(scoped[target.imageVariable]);
113
+ scoped[target.imageVariable] ??= provisioningScope.allowedImages[0];
114
+ }
115
+ for (const variable of ["honua_image", "honua_image_uri"]) {
116
+ if (variable !== target.imageVariable && scoped[variable] !== undefined)
117
+ throw new Error("Image variable does not belong to the selected runtime");
118
+ }
119
+ effective.variablesJson = JSON.stringify(scoped);
120
+ }
121
+ return effective;
122
+ }
123
+ let pending;
124
+ let closed = false;
125
+ let progressToken = 0;
126
+ let generation = 0;
127
+ const secretValues = Object.entries(config.env ?? {})
128
+ .filter(([key, value]) => /secret|token|password|(?:^|_)key(?:$|_)|api_?key|credential|authorization|bearer/i.test(key) &&
129
+ value.length >= 4)
130
+ .map(([, value]) => value);
131
+ function scrub(value) {
132
+ return JSON.parse(JSON.stringify(value, (_key, field) => {
133
+ if (typeof field !== "string")
134
+ return field;
135
+ let text = field;
136
+ for (const secret of secretValues)
137
+ text = text.split(secret).join("[REDACTED]");
138
+ return text;
139
+ }));
140
+ }
141
+ async function connect() {
142
+ if (closed)
143
+ throw new Error("DevOps integration is closed");
144
+ if (!pending) {
145
+ const currentGeneration = ++generation;
146
+ pending = (async () => {
147
+ const client = new Client({ name: "honua-agent-devops", version: "1.0.0" }, { capabilities: {} });
148
+ client.onerror = () => { };
149
+ client.onclose = () => {
150
+ if (generation === currentGeneration)
151
+ pending = undefined;
152
+ };
153
+ // Ignore child stderr: operator diagnostics are not model-visible tool content.
154
+ const transport = new StdioClientTransport({ ...config, stderr: "ignore" });
155
+ try {
156
+ await client.connect(transport);
157
+ if (closed)
158
+ throw new Error("closed");
159
+ return client;
160
+ }
161
+ catch {
162
+ await client.close().catch(() => { });
163
+ throw new Error("Private DevOps connection failed");
164
+ }
165
+ })();
166
+ // A failed handshake can be retried, but a tool call is NEVER automatically replayed.
167
+ const attempt = pending;
168
+ void attempt.catch(() => {
169
+ if (pending === attempt)
170
+ pending = undefined;
171
+ });
172
+ }
173
+ return pending;
174
+ }
175
+ async function catalog(client) {
176
+ const tools = [];
177
+ const cursors = new Set();
178
+ let cursor;
179
+ do {
180
+ const page = await client.listTools(cursor ? { cursor } : {});
181
+ for (const tool of page.tools) {
182
+ if (!DEVOPS_TOOL_ALLOWLIST.has(tool.name) || !enabled(tool.name))
183
+ continue;
184
+ if (tools.some((existing) => existing.name === tool.name))
185
+ throw new Error("Duplicate private tool");
186
+ tools.push(tool);
187
+ }
188
+ cursor = page.nextCursor;
189
+ if (cursor && cursors.has(cursor))
190
+ throw new Error("Invalid private tool cursor");
191
+ if (cursor)
192
+ cursors.add(cursor);
193
+ if (cursors.size > 100)
194
+ throw new Error("Private tool catalog too large");
195
+ } while (cursor);
196
+ return tools;
197
+ }
198
+ return {
199
+ async listTools() {
200
+ try {
201
+ await authorize({ canonicalTool: null });
202
+ const tools = await catalog(await connect());
203
+ return scrub(tools.map((tool) => ({
204
+ ...tool,
205
+ name: `${PREFIX}${tool.name}`,
206
+ _meta: { ...tool._meta, "honua.studio": { family: DEVOPS_TOOL_FAMILY, view: "workspace", revision: "1" } },
207
+ })));
208
+ }
209
+ catch {
210
+ throw new Error("Private DevOps discovery unavailable or unauthorized");
211
+ }
212
+ },
213
+ async callTool(name, args, options) {
214
+ const canonicalTool = name.startsWith(PREFIX) ? name.slice(PREFIX.length) : "";
215
+ if (!DEVOPS_TOOL_ALLOWLIST.has(canonicalTool))
216
+ throw new Error("Unsupported private DevOps tool");
217
+ try {
218
+ const effectiveArguments = bindArguments(canonicalTool, args);
219
+ await authorize({ canonicalTool, arguments: effectiveArguments });
220
+ const client = await connect();
221
+ // Discover afresh: do not use a stale descriptor after a target/policy change.
222
+ if (!(await catalog(client)).some((tool) => tool.name === canonicalTool))
223
+ throw new Error("Private DevOps capability unavailable");
224
+ return scrub(await client.request({
225
+ method: "tools/call",
226
+ params: {
227
+ name: canonicalTool,
228
+ arguments: effectiveArguments,
229
+ ...(options?.onprogress ? { _meta: { progressToken: ++progressToken } } : {}),
230
+ },
231
+ }, CallToolResultSchema, { signal: options?.signal, onprogress: options?.onprogress }));
232
+ }
233
+ catch {
234
+ throw new Error("Private DevOps action failed or unauthorized; reconcile canonical operation state before retry");
235
+ }
236
+ },
237
+ async close() {
238
+ closed = true;
239
+ await pending?.then((client) => client.close()).catch(() => { });
240
+ },
241
+ };
242
+ }
@@ -0,0 +1,33 @@
1
+ /** Host-selected runtimes and their canonical server actuators, never model-selected backends. */
2
+ export declare const DEVOPS_TARGETS: {
3
+ readonly "aws-ecs": {
4
+ readonly backend: "honua-aws-ecs-alb";
5
+ readonly terraformTarget: "ecs";
6
+ readonly imageVariable: "honua_image";
7
+ };
8
+ readonly "aws-lambda": {
9
+ readonly backend: "honua-gitops-aws-lambda";
10
+ readonly terraformTarget: "lambda";
11
+ readonly imageVariable: "honua_image_uri";
12
+ };
13
+ readonly "aws-eks": {
14
+ readonly backend: "honua-kubernetes-deployment";
15
+ readonly terraformTarget: "eks";
16
+ readonly imageVariable: undefined;
17
+ };
18
+ };
19
+ export type DevOpsRuntimeKind = keyof typeof DEVOPS_TARGETS;
20
+ export declare function devOpsTarget(runtimeKind?: DevOpsRuntimeKind): {
21
+ readonly backend: "honua-aws-ecs-alb";
22
+ readonly terraformTarget: "ecs";
23
+ readonly imageVariable: "honua_image";
24
+ } | {
25
+ readonly backend: "honua-gitops-aws-lambda";
26
+ readonly terraformTarget: "lambda";
27
+ readonly imageVariable: "honua_image_uri";
28
+ } | {
29
+ readonly backend: "honua-kubernetes-deployment";
30
+ readonly terraformTarget: "eks";
31
+ readonly imageVariable: undefined;
32
+ };
33
+ //# sourceMappingURL=devops-targets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devops-targets.d.ts","sourceRoot":"","sources":["../../../src/agent/devops-targets.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;CAkBjB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,cAAc,CAAC;AAE5D,wBAAgB,YAAY,CAAC,WAAW,GAAE,iBAA6B;;;;;;;;;;;;EAGtE"}
@@ -0,0 +1,25 @@
1
+ /** Host-selected runtimes and their canonical server actuators, never model-selected backends. */
2
+ export const DEVOPS_TARGETS = {
3
+ "aws-ecs": {
4
+ backend: "honua-aws-ecs-alb",
5
+ terraformTarget: "ecs",
6
+ imageVariable: "honua_image",
7
+ },
8
+ "aws-lambda": {
9
+ backend: "honua-gitops-aws-lambda",
10
+ terraformTarget: "lambda",
11
+ imageVariable: "honua_image_uri",
12
+ },
13
+ "aws-eks": {
14
+ backend: "honua-kubernetes-deployment",
15
+ terraformTarget: "eks",
16
+ // The EKS infrastructure root does not install the application. Its separate
17
+ // workload handoff must verify the actual Deployment, Service and cluster identity.
18
+ imageVariable: undefined,
19
+ },
20
+ };
21
+ export function devOpsTarget(runtimeKind = "aws-ecs") {
22
+ if (!Object.hasOwn(DEVOPS_TARGETS, runtimeKind))
23
+ throw new Error("Unsupported private deployment runtime");
24
+ return DEVOPS_TARGETS[runtimeKind];
25
+ }