@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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/package.json +87 -0
- package/src/client/client.ts +589 -0
- package/src/client/index.ts +18 -0
- package/src/client/transports.ts +84 -0
- package/src/core/agents/acp/acp-agent.ts +322 -0
- package/src/core/agents/acp/adapter.ts +260 -0
- package/src/core/agents/acp/client.ts +212 -0
- package/src/core/agents/acp/known-agents.ts +29 -0
- package/src/core/agents/acp/mappings.ts +136 -0
- package/src/core/agents/base.ts +145 -0
- package/src/core/agents/claude-code/adapter.ts +451 -0
- package/src/core/agents/claude-code/claude-agent.ts +235 -0
- package/src/core/agents/claude-code/generator-session.ts +344 -0
- package/src/core/agents/claude-code/options.ts +161 -0
- package/src/core/agents/claude-code/session-manager.ts +159 -0
- package/src/core/agents/codex-app-server/adapter.ts +214 -0
- package/src/core/agents/codex-app-server/client.ts +221 -0
- package/src/core/agents/codex-app-server/codex-app-server-agent.ts +385 -0
- package/src/core/agents/codex-items.ts +122 -0
- package/src/core/agents/codex-sdk/adapter.ts +204 -0
- package/src/core/agents/codex-sdk/codex-sdk-agent.ts +236 -0
- package/src/core/agents/config-fingerprint.ts +19 -0
- package/src/core/agents/error-classifier.ts +68 -0
- package/src/core/agents/registry.ts +40 -0
- package/src/core/agents/session-store.ts +72 -0
- package/src/core/agents/tool-meta.ts +68 -0
- package/src/core/agents/types.ts +54 -0
- package/src/core/index.ts +114 -0
- package/src/core/presets.ts +78 -0
- package/src/core/provision/extract.ts +31 -0
- package/src/core/provision/index.ts +10 -0
- package/src/core/provision/npm.ts +114 -0
- package/src/core/provision/pins.ts +51 -0
- package/src/core/provision/platform.ts +73 -0
- package/src/core/provision/provisioner.ts +478 -0
- package/src/core/proxy/anthropic-proxy.ts +69 -0
- package/src/core/proxy/api-key-store.ts +34 -0
- package/src/core/proxy/index.ts +7 -0
- package/src/core/runtime/agent-runtime.ts +363 -0
- package/src/core/runtime/event-processor.ts +218 -0
- package/src/core/runtime/event-sink.ts +37 -0
- package/src/core/utils/errors.ts +41 -0
- package/src/index.ts +4 -0
- package/src/protocol/async-queue.ts +68 -0
- package/src/protocol/config.ts +100 -0
- package/src/protocol/factories.ts +125 -0
- package/src/protocol/harness.ts +50 -0
- package/src/protocol/ids.ts +53 -0
- package/src/protocol/index.ts +16 -0
- package/src/protocol/lifecycle.ts +309 -0
- package/src/protocol/models.ts +45 -0
- package/src/protocol/part-input.ts +58 -0
- package/src/protocol/parts.ts +60 -0
- package/src/protocol/thinking.ts +32 -0
- package/src/protocol/tokens.ts +39 -0
- package/src/protocol/tool-state.ts +89 -0
- package/src/protocol/wire.ts +313 -0
- package/src/server/acp/binding.ts +163 -0
- package/src/server/acp/translate.ts +160 -0
- package/src/server/agent-server.ts +357 -0
- package/src/server/bin.ts +174 -0
- package/src/server/index.ts +24 -0
- package/src/server/install.ts +51 -0
- package/src/server/session-log.ts +66 -0
- 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
|
+
}
|