@zvada/agent-server 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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/package.json +87 -0
  4. package/src/client/client.ts +589 -0
  5. package/src/client/index.ts +18 -0
  6. package/src/client/transports.ts +84 -0
  7. package/src/core/agents/acp/acp-agent.ts +322 -0
  8. package/src/core/agents/acp/adapter.ts +260 -0
  9. package/src/core/agents/acp/client.ts +212 -0
  10. package/src/core/agents/acp/known-agents.ts +29 -0
  11. package/src/core/agents/acp/mappings.ts +136 -0
  12. package/src/core/agents/base.ts +145 -0
  13. package/src/core/agents/claude-code/adapter.ts +451 -0
  14. package/src/core/agents/claude-code/claude-agent.ts +235 -0
  15. package/src/core/agents/claude-code/generator-session.ts +344 -0
  16. package/src/core/agents/claude-code/options.ts +161 -0
  17. package/src/core/agents/claude-code/session-manager.ts +159 -0
  18. package/src/core/agents/codex-app-server/adapter.ts +214 -0
  19. package/src/core/agents/codex-app-server/client.ts +221 -0
  20. package/src/core/agents/codex-app-server/codex-app-server-agent.ts +385 -0
  21. package/src/core/agents/codex-items.ts +122 -0
  22. package/src/core/agents/codex-sdk/adapter.ts +204 -0
  23. package/src/core/agents/codex-sdk/codex-sdk-agent.ts +236 -0
  24. package/src/core/agents/config-fingerprint.ts +19 -0
  25. package/src/core/agents/error-classifier.ts +68 -0
  26. package/src/core/agents/registry.ts +40 -0
  27. package/src/core/agents/session-store.ts +72 -0
  28. package/src/core/agents/tool-meta.ts +68 -0
  29. package/src/core/agents/types.ts +54 -0
  30. package/src/core/index.ts +114 -0
  31. package/src/core/presets.ts +78 -0
  32. package/src/core/provision/extract.ts +31 -0
  33. package/src/core/provision/index.ts +10 -0
  34. package/src/core/provision/npm.ts +114 -0
  35. package/src/core/provision/pins.ts +51 -0
  36. package/src/core/provision/platform.ts +73 -0
  37. package/src/core/provision/provisioner.ts +478 -0
  38. package/src/core/proxy/anthropic-proxy.ts +69 -0
  39. package/src/core/proxy/api-key-store.ts +34 -0
  40. package/src/core/proxy/index.ts +7 -0
  41. package/src/core/runtime/agent-runtime.ts +363 -0
  42. package/src/core/runtime/event-processor.ts +218 -0
  43. package/src/core/runtime/event-sink.ts +37 -0
  44. package/src/core/utils/errors.ts +41 -0
  45. package/src/index.ts +4 -0
  46. package/src/protocol/async-queue.ts +68 -0
  47. package/src/protocol/config.ts +100 -0
  48. package/src/protocol/factories.ts +125 -0
  49. package/src/protocol/harness.ts +50 -0
  50. package/src/protocol/ids.ts +53 -0
  51. package/src/protocol/index.ts +16 -0
  52. package/src/protocol/lifecycle.ts +309 -0
  53. package/src/protocol/models.ts +45 -0
  54. package/src/protocol/part-input.ts +58 -0
  55. package/src/protocol/parts.ts +60 -0
  56. package/src/protocol/thinking.ts +32 -0
  57. package/src/protocol/tokens.ts +39 -0
  58. package/src/protocol/tool-state.ts +89 -0
  59. package/src/protocol/wire.ts +313 -0
  60. package/src/server/acp/binding.ts +163 -0
  61. package/src/server/acp/translate.ts +160 -0
  62. package/src/server/agent-server.ts +357 -0
  63. package/src/server/bin.ts +174 -0
  64. package/src/server/index.ts +24 -0
  65. package/src/server/install.ts +51 -0
  66. package/src/server/session-log.ts +66 -0
  67. package/src/server/transports.ts +149 -0
@@ -0,0 +1,68 @@
1
+ import type { ToolKind, ToolLocation, ToolMeta } from "../../protocol/index.ts";
2
+
3
+ /**
4
+ * Maps provider-native tool names onto the normalized ToolKind taxonomy and
5
+ * extracts touched file locations from tool inputs. Pure best-effort: unknown
6
+ * tools are `other` and produce no locations.
7
+ */
8
+
9
+ const CLAUDE_TOOL_KINDS: Record<string, ToolKind> = {
10
+ Read: "read",
11
+ NotebookRead: "read",
12
+ Edit: "edit",
13
+ Write: "edit",
14
+ MultiEdit: "edit",
15
+ NotebookEdit: "edit",
16
+ Bash: "execute",
17
+ BashOutput: "execute",
18
+ KillShell: "execute",
19
+ Grep: "search",
20
+ Glob: "search",
21
+ LS: "search",
22
+ WebFetch: "fetch",
23
+ WebSearch: "fetch",
24
+ Task: "think",
25
+ Agent: "think",
26
+ TodoWrite: "think",
27
+ ExitPlanMode: "switch_mode",
28
+ EnterPlanMode: "switch_mode",
29
+ };
30
+
31
+ export function claudeToolKind(toolName: string): ToolKind {
32
+ if (CLAUDE_TOOL_KINDS[toolName]) return CLAUDE_TOOL_KINDS[toolName];
33
+ // MCP tools arrive as mcp__server__tool.
34
+ if (toolName.startsWith("mcp__")) return "other";
35
+ return "other";
36
+ }
37
+
38
+ const PATH_INPUT_KEYS = ["file_path", "filePath", "path", "notebook_path"] as const;
39
+
40
+ export function toolLocationsFromInput(input: Record<string, unknown>): ToolLocation[] | undefined {
41
+ for (const key of PATH_INPUT_KEYS) {
42
+ const v = input[key];
43
+ if (typeof v === "string" && v) return [{ path: v }];
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ export function claudeToolMeta(toolName: string, input?: Record<string, unknown>): ToolMeta {
49
+ return {
50
+ kind: claudeToolKind(toolName),
51
+ ...(input ? { locations: toolLocationsFromInput(input) } : {}),
52
+ };
53
+ }
54
+
55
+ /** Codex tools are already normalized names; kinds are fixed per item type. */
56
+ export const CODEX_TOOL_KINDS = {
57
+ shell: "execute",
58
+ apply_patch: "edit",
59
+ web_search: "fetch",
60
+ mcp: "other",
61
+ } as const satisfies Record<string, ToolKind>;
62
+
63
+ export function codexFileChangeLocations(
64
+ changes: Array<{ path: string }> | undefined,
65
+ ): ToolLocation[] | undefined {
66
+ if (!changes?.length) return undefined;
67
+ return changes.map((c) => ({ path: c.path }));
68
+ }
@@ -0,0 +1,54 @@
1
+ import type { Part, StopReason, TokenUsage } from "../../protocol/index.ts";
2
+
3
+ /**
4
+ * The normalized intermediate an adapter emits while consuming raw harness
5
+ * events. The engine's EventProcessor turns these into wire `LifecycleEvent`s,
6
+ * assigning message/part indices and ids. Keeping deltas here (rather than
7
+ * diffing snapshots downstream) lets each adapter express its harness's exact
8
+ * streaming granularity — token-level for Claude / Codex app-server,
9
+ * item-level for the Codex SDK.
10
+ */
11
+ export type AdapterEvent =
12
+ | { kind: "message-start"; role: "assistant" | "user" }
13
+ | { kind: "message-end" }
14
+ /** A new part appears (streaming text begins, a tool call opens, etc.). */
15
+ | { kind: "part-open"; part: Part }
16
+ /** A full new snapshot of an existing part (tool -> running/completed, text finalized). */
17
+ | { kind: "part-update"; part: Part }
18
+ | { kind: "text-delta"; partId: string; text: string }
19
+ | { kind: "reasoning-delta"; partId: string; text: string }
20
+ | {
21
+ kind: "tool-input-delta";
22
+ partId: string;
23
+ toolCallId: string;
24
+ toolName: string;
25
+ input: string;
26
+ }
27
+ /** Context-window gauge snapshot (→ `session.usage`). */
28
+ | { kind: "usage"; used: number; size?: number; cost?: number }
29
+ /** History-compaction boundary (→ `session.compacted`). */
30
+ | { kind: "compacted"; trigger?: string; preTokens?: number; postTokens?: number };
31
+
32
+ /** Terminal result of a turn, surfaced on `turn.ended`. */
33
+ export interface TransformResult {
34
+ usage: TokenUsage;
35
+ /** Normalized terminal status, when the harness reported one. The engine
36
+ * overrides with `cancelled`/`error` based on what it observed. */
37
+ stopReason?: StopReason;
38
+ /** Raw provider finish string, passed through untouched. */
39
+ finishReason?: string;
40
+ cost?: number;
41
+ error?: string;
42
+ cancelled?: boolean;
43
+ }
44
+
45
+ /**
46
+ * Per-turn stateful translator from one harness's raw events to AdapterEvents.
47
+ * Created fresh per turn (it holds streaming part state).
48
+ */
49
+ export interface EventTransformer<TEvent = unknown> {
50
+ process(event: TEvent): AdapterEvent[];
51
+ finish(): TransformResult;
52
+ }
53
+
54
+ export type AdapterFactory = (ctx: { sessionId: string }) => EventTransformer;
@@ -0,0 +1,114 @@
1
+ // @zvada/agent-server/core — harness-agnostic agent execution engine.
2
+
3
+ // Runtime
4
+ export { AgentRuntime, type RunSummary } from "./runtime/agent-runtime.ts";
5
+ export {
6
+ type EventSink,
7
+ callbackSink,
8
+ CollectingSink,
9
+ teeSink,
10
+ } from "./runtime/event-sink.ts";
11
+ export { EventProcessor } from "./runtime/event-processor.ts";
12
+
13
+ // Agent abstraction
14
+ export {
15
+ type Agent,
16
+ type AgentExecuteOptions,
17
+ type RawAgentEvent,
18
+ BaseAgent,
19
+ } from "./agents/base.ts";
20
+ export { AgentRegistry } from "./agents/registry.ts";
21
+ export {
22
+ type ErrorCategory,
23
+ ERROR_CATEGORIES,
24
+ classifyError,
25
+ isRecoverable,
26
+ isCancellation,
27
+ } from "./agents/error-classifier.ts";
28
+
29
+ // Adapter contract
30
+ export type {
31
+ AdapterEvent,
32
+ AdapterFactory,
33
+ EventTransformer,
34
+ TransformResult,
35
+ } from "./agents/types.ts";
36
+
37
+ // CLI provisioning
38
+ export {
39
+ CLI_TOOLS,
40
+ type CliTool,
41
+ CliProvisioner,
42
+ DEFAULT_PINS,
43
+ type PlatformInfo,
44
+ type ProvisionMode,
45
+ type ProvisionOptions,
46
+ type ProvisionerDeps,
47
+ type ResolvedCli,
48
+ detectPlatform,
49
+ toolForHarness,
50
+ } from "./provision/index.ts";
51
+
52
+ // Harnesses
53
+ export {
54
+ ClaudeCodeAgent,
55
+ type ClaudeCodeAgentOptions,
56
+ } from "./agents/claude-code/claude-agent.ts";
57
+ export type { ClaudeSdkOptionOverrides, SdkMcpServers } from "./agents/claude-code/options.ts";
58
+ export type {
59
+ ClaudeHooksFactory,
60
+ ClaudeSessionExtras,
61
+ ClaudeToolPolicy,
62
+ } from "./agents/claude-code/generator-session.ts";
63
+ export {
64
+ ClaudeCodeTransformer,
65
+ createClaudeCodeTransformer,
66
+ } from "./agents/claude-code/adapter.ts";
67
+ export {
68
+ CodexSdkAgent,
69
+ type CodexSdkAgentOptions,
70
+ } from "./agents/codex-sdk/codex-sdk-agent.ts";
71
+ export { CodexSdkTransformer, createCodexSdkTransformer } from "./agents/codex-sdk/adapter.ts";
72
+ export {
73
+ CodexAppServerAgent,
74
+ type CodexAppServerAgentOptions,
75
+ } from "./agents/codex-app-server/codex-app-server-agent.ts";
76
+ export {
77
+ CodexAppServerTransformer,
78
+ createCodexAppServerTransformer,
79
+ } from "./agents/codex-app-server/adapter.ts";
80
+ export {
81
+ CodexAppServerClient,
82
+ type CodexNotification,
83
+ } from "./agents/codex-app-server/client.ts";
84
+ export {
85
+ AcpAgent,
86
+ type AcpAgentOptions,
87
+ } from "./agents/acp/acp-agent.ts";
88
+ export { AcpTransformer, createAcpTransformer } from "./agents/acp/adapter.ts";
89
+ export {
90
+ AcpClient,
91
+ type AcpClientOptions,
92
+ type AcpLaunch,
93
+ parseAcpLaunch,
94
+ } from "./agents/acp/client.ts";
95
+ export { KNOWN_ACP_AGENTS, resolveAcpLaunch } from "./agents/acp/known-agents.ts";
96
+
97
+ // Presets
98
+ export {
99
+ type CreateRegistryOptions,
100
+ createAgentRegistry,
101
+ createAgentRuntime,
102
+ } from "./presets.ts";
103
+
104
+ // Utils
105
+ export {
106
+ AgentServerError,
107
+ AgentExecutionError,
108
+ CliNotFoundError,
109
+ CliProvisionError,
110
+ HarnessNotFoundError,
111
+ } from "./utils/errors.ts";
112
+
113
+ // Re-export the wire contract for convenience.
114
+ export * from "../protocol/index.ts";
@@ -0,0 +1,78 @@
1
+ import { AGENT_HARNESSES, type AgentHarness } from "../protocol/index.ts";
2
+ import { AcpAgent, type AcpAgentOptions } from "./agents/acp/acp-agent.ts";
3
+ import { createAcpTransformer } from "./agents/acp/adapter.ts";
4
+ import { createClaudeCodeTransformer } from "./agents/claude-code/adapter.ts";
5
+ import { ClaudeCodeAgent, type ClaudeCodeAgentOptions } from "./agents/claude-code/claude-agent.ts";
6
+ import { createCodexAppServerTransformer } from "./agents/codex-app-server/adapter.ts";
7
+ import { CodexAppServerAgent } from "./agents/codex-app-server/codex-app-server-agent.ts";
8
+ import { createCodexSdkTransformer } from "./agents/codex-sdk/adapter.ts";
9
+ import { CodexSdkAgent } from "./agents/codex-sdk/codex-sdk-agent.ts";
10
+ import { AgentRegistry } from "./agents/registry.ts";
11
+ import { CliProvisioner, type ProvisionOptions } from "./provision/provisioner.ts";
12
+ import { AgentRuntime } from "./runtime/agent-runtime.ts";
13
+
14
+ export interface CreateRegistryOptions {
15
+ /**
16
+ * Which harnesses to register. Defaults to the three native harnesses,
17
+ * plus `acp` when `acp` config is provided (it has no default launch).
18
+ */
19
+ harnesses?: AgentHarness[];
20
+ /**
21
+ * CLI provisioning policy — how the native `claude`/`codex` binaries are
22
+ * found or downloaded (see `CliProvisioner`). Defaults to `auto`.
23
+ */
24
+ provision?: ProvisionOptions;
25
+ /**
26
+ * ACP harness configuration. Required to enable `acp`: the harness needs an
27
+ * operator-supplied launch command for the target agent (never wire-driven).
28
+ */
29
+ acp?: AcpAgentOptions;
30
+ /**
31
+ * Claude harness embed-tier options (e.g. in-process `sdkMcpServers`).
32
+ * `resolveCliPath` stays provisioner-wired here.
33
+ */
34
+ claudeCode?: Omit<ClaudeCodeAgentOptions, "resolveCliPath">;
35
+ }
36
+
37
+ /** Build a registry with the standard harnesses wired to their adapters. */
38
+ export function createAgentRegistry(opts: CreateRegistryOptions = {}): AgentRegistry {
39
+ // Default: every harness that can run unconfigured — acp joins only when its
40
+ // operator launch config is present (it has nothing to run otherwise).
41
+ const enabled = new Set<AgentHarness>(
42
+ opts.harnesses ?? AGENT_HARNESSES.filter((h) => h !== "acp" || Boolean(opts.acp)),
43
+ );
44
+ // One provisioner per registry: resolution is memoized per tool, so both
45
+ // codex harnesses share a single download and every agent logs one source.
46
+ const provisioner = new CliProvisioner(opts.provision);
47
+ const claudeCli = () => provisioner.resolve("claude").then((cli) => cli?.path);
48
+ const codexCli = () => provisioner.resolve("codex").then((cli) => cli?.path);
49
+
50
+ const registry = new AgentRegistry();
51
+ if (enabled.has("claude-code")) {
52
+ registry.register(
53
+ new ClaudeCodeAgent({ ...opts.claudeCode, resolveCliPath: claudeCli }),
54
+ createClaudeCodeTransformer,
55
+ );
56
+ }
57
+ if (enabled.has("codex-sdk")) {
58
+ registry.register(new CodexSdkAgent({ resolveCliPath: codexCli }), createCodexSdkTransformer);
59
+ }
60
+ if (enabled.has("codex-app-server")) {
61
+ registry.register(
62
+ new CodexAppServerAgent({ resolveCliPath: codexCli }),
63
+ createCodexAppServerTransformer,
64
+ );
65
+ }
66
+ if (enabled.has("acp")) {
67
+ if (!opts.acp) {
68
+ throw new Error("the acp harness requires CreateRegistryOptions.acp (resolveLaunch)");
69
+ }
70
+ registry.register(new AcpAgent(opts.acp), createAcpTransformer);
71
+ }
72
+ return registry;
73
+ }
74
+
75
+ /** Convenience: a ready-to-use runtime with the standard harnesses. */
76
+ export function createAgentRuntime(opts: CreateRegistryOptions = {}): AgentRuntime {
77
+ return new AgentRuntime(createAgentRegistry(opts));
78
+ }
@@ -0,0 +1,31 @@
1
+ import { spawn } from "node:child_process";
2
+ import { CliProvisionError } from "../utils/errors.ts";
3
+
4
+ /**
5
+ * Extract a .tgz into a directory using the system `tar` (BSD, GNU, and
6
+ * Windows 10+ libarchive all accept these flags). npm tarballs are small
7
+ * enough in entry count that shelling out beats maintaining a tar parser.
8
+ */
9
+ export async function extractTarGz(tgzPath: string, destDir: string): Promise<void> {
10
+ await new Promise<void>((resolve, reject) => {
11
+ const proc = spawn("tar", ["-xzf", tgzPath, "-C", destDir], {
12
+ stdio: ["ignore", "ignore", "pipe"],
13
+ });
14
+ let stderr = "";
15
+ proc.stderr?.on("data", (chunk: Buffer) => {
16
+ stderr += chunk.toString();
17
+ });
18
+ proc.on("error", (error) =>
19
+ reject(
20
+ new CliProvisionError(
21
+ `Failed to run \`tar\` (${error.message}) — provisioning needs tar on PATH`,
22
+ { cause: error },
23
+ ),
24
+ ),
25
+ );
26
+ proc.on("exit", (code) => {
27
+ if (code === 0) resolve();
28
+ else reject(new CliProvisionError(`tar -xzf ${tgzPath} exited ${code}: ${stderr.trim()}`));
29
+ });
30
+ });
31
+ }
@@ -0,0 +1,10 @@
1
+ // CLI provisioning — resolve/download the native CLIs the harnesses spawn.
2
+ export { CLI_TOOLS, type CliTool, DEFAULT_PINS, toolForHarness } from "./pins.ts";
3
+ export { type PlatformInfo, detectPlatform } from "./platform.ts";
4
+ export {
5
+ CliProvisioner,
6
+ type ProvisionMode,
7
+ type ProvisionOptions,
8
+ type ProvisionerDeps,
9
+ type ResolvedCli,
10
+ } from "./provisioner.ts";
@@ -0,0 +1,114 @@
1
+ import { createHash } from "node:crypto";
2
+ import { once } from "node:events";
3
+ import { createWriteStream } from "node:fs";
4
+ import { CliProvisionError } from "../utils/errors.ts";
5
+
6
+ export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
7
+
8
+ /** The subset of npm packument `dist` we need to fetch a package verifiably. */
9
+ export interface NpmDist {
10
+ tarball: string;
11
+ /** Subresource integrity, e.g. `sha512-<base64>`. */
12
+ integrity: string;
13
+ }
14
+
15
+ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
16
+
17
+ /** One retry on network-level failures; 4xx/5xx are not retried. */
18
+ async function fetchOk(fetchImpl: FetchLike, url: string): Promise<Response> {
19
+ let lastError: unknown;
20
+ for (let attempt = 0; attempt < 2; attempt++) {
21
+ try {
22
+ const res = await fetchImpl(url);
23
+ if (!res.ok) {
24
+ throw new CliProvisionError(`GET ${url} failed: ${res.status} ${res.statusText}`);
25
+ }
26
+ return res;
27
+ } catch (error) {
28
+ if (error instanceof CliProvisionError) throw error;
29
+ lastError = error;
30
+ }
31
+ }
32
+ throw new CliProvisionError(`GET ${url} failed: ${String(lastError)}`, { cause: lastError });
33
+ }
34
+
35
+ /**
36
+ * Fetch the registry metadata for one exact package version and return its
37
+ * tarball location + integrity. The returned tarball URL is the registry's own
38
+ * (works with mirrors/private registries that rewrite hosts).
39
+ */
40
+ export async function fetchDist(
41
+ registry: string,
42
+ packageName: string,
43
+ version: string,
44
+ fetchImpl: FetchLike = fetch,
45
+ ): Promise<NpmDist> {
46
+ // Scoped names keep the literal `@` but escape the inner slash.
47
+ const base = registry.replace(/\/+$/, "");
48
+ const url = `${base}/${packageName.replace("/", "%2F")}/${version}`;
49
+ const res = await fetchOk(fetchImpl, url);
50
+ const body = (await res.json()) as { dist?: { tarball?: string; integrity?: string } };
51
+ const { tarball, integrity } = body.dist ?? {};
52
+ if (!tarball || !integrity) {
53
+ throw new CliProvisionError(
54
+ `Registry metadata for ${packageName}@${version} has no dist.tarball/integrity — is this version published for your platform?`,
55
+ );
56
+ }
57
+ return { tarball, integrity };
58
+ }
59
+
60
+ /**
61
+ * Stream a tarball to disk, hashing as it lands, and reject on integrity
62
+ * mismatch. Only sha512 (what the npm registry publishes) is supported.
63
+ * Returns the byte count for progress logs.
64
+ */
65
+ export async function downloadWithIntegrity(
66
+ url: string,
67
+ destFile: string,
68
+ integrity: string,
69
+ fetchImpl: FetchLike = fetch,
70
+ ): Promise<number> {
71
+ const expected = /^sha512-([A-Za-z0-9+/=]+)$/.exec(integrity)?.[1];
72
+ if (!expected) {
73
+ throw new CliProvisionError(`Unsupported integrity format (want sha512-…): ${integrity}`);
74
+ }
75
+ const res = await fetchOk(fetchImpl, url);
76
+ if (!res.body) throw new CliProvisionError(`GET ${url} returned no body`);
77
+
78
+ const hash = createHash("sha512");
79
+ const out = createWriteStream(destFile);
80
+ // One persistent listener: an unobserved WriteStream 'error' would crash the
81
+ // process, and per-chunk `once("error")` listeners would accumulate instead.
82
+ let streamError: unknown;
83
+ out.on("error", (error) => {
84
+ streamError = error;
85
+ });
86
+ let bytes = 0;
87
+ try {
88
+ for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) {
89
+ if (streamError) throw streamError;
90
+ hash.update(chunk);
91
+ bytes += chunk.byteLength;
92
+ // events.once rejects on 'error' and removes its listeners either way.
93
+ if (!out.write(chunk)) await once(out, "drain");
94
+ }
95
+ if (streamError) throw streamError;
96
+ // Reject on 'error' too: a failure during the terminal flush (e.g.
97
+ // ENOSPC on the last buffered chunk) never fires the end callback, and
98
+ // without this listener the await would hang instead of surfacing it.
99
+ await new Promise<void>((resolve, reject) => {
100
+ out.once("error", reject);
101
+ out.end(() => (streamError ? reject(streamError as Error) : resolve()));
102
+ });
103
+ } catch (error) {
104
+ out.destroy();
105
+ throw error;
106
+ }
107
+ const actual = hash.digest("base64");
108
+ if (actual !== expected) {
109
+ throw new CliProvisionError(
110
+ `Integrity mismatch for ${url}: expected sha512-${expected}, got sha512-${actual}`,
111
+ );
112
+ }
113
+ return bytes;
114
+ }
@@ -0,0 +1,51 @@
1
+ import type { AgentHarness } from "../../protocol/index.ts";
2
+
3
+ /**
4
+ * The managed CLIs. Each native harness maps onto exactly one of these; the
5
+ * `acp` harness maps onto none (its agent binaries are arbitrary third-party
6
+ * commands the operator supplies, not something we pin or download).
7
+ */
8
+ export const CLI_TOOLS = ["claude", "codex"] as const;
9
+ export type CliTool = (typeof CLI_TOOLS)[number];
10
+
11
+ /**
12
+ * The CLI versions this engine release is tested against — the versions
13
+ * `agent-server install` (and on-demand provisioning) downloads.
14
+ *
15
+ * - `claude` is the version of the `@anthropic-ai/claude-agent-sdk-<platform>`
16
+ * packages, which carry the native CLI matched to the SDK JS we bundle. It
17
+ * MUST track the lockfile's claude-agent-sdk version (a test enforces this);
18
+ * bump both together. When the SDK is resolvable on disk, its real installed
19
+ * version wins over this pin so the pair can never skew.
20
+ * - `codex` is the `@openai/codex` platform-package version the
21
+ * codex-app-server adapter's protocol snapshot was verified against (see
22
+ * CLAUDE.md "Verifying harness protocols"). Bump after re-verifying. Note
23
+ * the codex-sdk harness normally never reaches this pin: host resolution
24
+ * prefers the build vendored by the installed `@openai/codex-sdk` (its own
25
+ * pair). The pin governs downloads (compiled binaries / bare sandboxes /
26
+ * `pinned` mode); embedders wanting an exact codex-sdk pairing there can
27
+ * set `provision.pins.codex` to their SDK's vendored version.
28
+ */
29
+ export const DEFAULT_PINS: Record<CliTool, string> = {
30
+ claude: "0.3.168",
31
+ // 0.146.1 re-verified 2026-08-06: generate-ts snapshot carries our full
32
+ // surface unchanged (thread/turn/item methods, tokenUsage w/ window,
33
+ // approvals) and a live turn ran clean incl. the gpt-5.6-sol default.
34
+ codex: "0.146.1",
35
+ };
36
+
37
+ /**
38
+ * Which managed CLI a harness runs on — `undefined` for unmanaged harnesses
39
+ * (`acp`). Exhaustive by construction: adding a harness fails to compile here
40
+ * until someone decides whether it is provisioner-managed.
41
+ */
42
+ const TOOL_FOR_HARNESS: Record<AgentHarness, CliTool | undefined> = {
43
+ "claude-code": "claude",
44
+ "codex-sdk": "codex",
45
+ "codex-app-server": "codex",
46
+ acp: undefined,
47
+ };
48
+
49
+ export function toolForHarness(harness: AgentHarness): CliTool | undefined {
50
+ return TOOL_FOR_HARNESS[harness];
51
+ }
@@ -0,0 +1,73 @@
1
+ import { constants, accessSync, existsSync } from "node:fs";
2
+ import { delimiter, join } from "node:path";
3
+
4
+ /** The facts about this machine that pick a CLI build. Injectable in tests. */
5
+ export interface PlatformInfo {
6
+ /** `process.platform` value (darwin | linux | win32 | …). */
7
+ platform: string;
8
+ /** `process.arch` value (arm64 | x64 | …). */
9
+ arch: string;
10
+ /** linux only: glibc absent (Alpine et al.) — selects `-musl` builds. */
11
+ musl: boolean;
12
+ }
13
+
14
+ /**
15
+ * Musl detection mirrors the Claude SDK's own check: a Node process report
16
+ * without `glibcVersionRuntime` means musl. Runtimes without process.report
17
+ * (or partial implementations) fall back to the Alpine marker file.
18
+ */
19
+ function isMusl(): boolean {
20
+ if (process.platform !== "linux") return false;
21
+ try {
22
+ const report = process.report?.getReport?.() as
23
+ | { header?: { glibcVersionRuntime?: string } }
24
+ | undefined;
25
+ if (report?.header) return !report.header.glibcVersionRuntime;
26
+ } catch {
27
+ // fall through to the filesystem heuristic
28
+ }
29
+ return existsSync("/etc/alpine-release");
30
+ }
31
+
32
+ export function detectPlatform(): PlatformInfo {
33
+ return { platform: process.platform, arch: process.arch, musl: isMusl() };
34
+ }
35
+
36
+ /**
37
+ * Suffix of `@anthropic-ai/claude-agent-sdk-<suffix>` — the platform package
38
+ * carrying the native `claude` binary. Claude ships dedicated musl builds.
39
+ */
40
+ export function claudePackageSuffix(p: PlatformInfo): string {
41
+ return `${p.platform}-${p.arch}${p.musl ? "-musl" : ""}`;
42
+ }
43
+
44
+ /**
45
+ * Suffix of `@openai/codex@<version>-<suffix>` — codex publishes platform
46
+ * builds as version suffixes of the main package. Its linux builds are static
47
+ * (musl) already, so there is no separate musl variant.
48
+ */
49
+ export function codexPackageSuffix(p: PlatformInfo): string {
50
+ return `${p.platform}-${p.arch}`;
51
+ }
52
+
53
+ /** Locate an executable on PATH (the `which` we can unit-test and run anywhere). */
54
+ export function findOnPath(
55
+ name: string,
56
+ env: Record<string, string | undefined> = process.env,
57
+ platform: string = process.platform,
58
+ ): string | undefined {
59
+ const paths = (env.PATH ?? "").split(delimiter).filter(Boolean);
60
+ const names = platform === "win32" ? [`${name}.exe`, `${name}.cmd`, name] : [name];
61
+ for (const dir of paths) {
62
+ for (const file of names) {
63
+ const candidate = join(dir, file);
64
+ try {
65
+ accessSync(candidate, constants.X_OK);
66
+ return candidate;
67
+ } catch {
68
+ // not here — keep walking
69
+ }
70
+ }
71
+ }
72
+ return undefined;
73
+ }