@fusionkit/ensemble 0.1.5 → 0.1.7

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.
@@ -1,398 +0,0 @@
1
- import { artifactHash } from "@fusionkit/protocol";
2
- import { CapabilityMismatchError, prepareExecution } from "@fusionkit/runner";
3
- import { aiSdkHarnessBackend } from "@fusionkit/session-harness";
4
- const ZERO_HASH = "0".repeat(64);
5
- const ZERO_GIT_SHA = "0".repeat(40);
6
- const DEFAULT_POOL = "ensemble";
7
- const DEFAULT_RUNTIME = "node24";
8
- const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
9
- const DEFAULT_LOG_MAX_BYTES = 256 * 1024;
10
- const DEFAULT_CLAUDE_NETWORK = {
11
- defaultDeny: true,
12
- allowHosts: ["registry.npmjs.org", "api.anthropic.com", "ai-gateway.vercel.sh"]
13
- };
14
- const AUTH_ENV_NAMES = [
15
- "AI_GATEWAY_API_KEY",
16
- "AI_GATEWAY_BASE_URL",
17
- "ANTHROPIC_API_KEY",
18
- "ANTHROPIC_AUTH_TOKEN",
19
- "ANTHROPIC_BASE_URL"
20
- ];
21
- function candidateId(input) {
22
- return `${input.descriptor.id}_${input.model.id}_${input.ordinal}`;
23
- }
24
- function envValue(env, name) {
25
- const value = env[name];
26
- return value && value.length > 0 ? value : undefined;
27
- }
28
- function authEnvFrom(env) {
29
- const authEnv = {};
30
- for (const name of AUTH_ENV_NAMES) {
31
- const value = envValue(env, name);
32
- if (value !== undefined)
33
- authEnv[name] = value;
34
- }
35
- return authEnv;
36
- }
37
- function credentialGate(env, options) {
38
- const missing = [];
39
- const hasProviderCredential = envValue(env, "AI_GATEWAY_API_KEY") ??
40
- envValue(env, "ANTHROPIC_API_KEY") ??
41
- envValue(env, "ANTHROPIC_AUTH_TOKEN");
42
- const hasSandboxCredential = options.backend !== undefined ||
43
- options.createSandboxProvider !== undefined ||
44
- options.token !== undefined ||
45
- envValue(env, "VERCEL_TOKEN") !== undefined;
46
- if (!hasSandboxCredential)
47
- missing.push("VERCEL_TOKEN");
48
- if (!hasProviderCredential) {
49
- missing.push("ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|AI_GATEWAY_API_KEY");
50
- }
51
- if (missing.length > 0) {
52
- return {
53
- available: false,
54
- missing,
55
- reason: "Claude Code harness skipped: missing Claude Code credential/env; set VERCEL_TOKEN and one of " +
56
- "ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, or AI_GATEWAY_API_KEY."
57
- };
58
- }
59
- return { available: true, authEnv: authEnvFrom(env) };
60
- }
61
- export function claudeCodeHarnessCredentialSkipReason(env = process.env, options = {}) {
62
- const gate = credentialGate(env, options);
63
- return gate.available ? undefined : gate.reason;
64
- }
65
- function backendFor(options, env) {
66
- return (options.backend ??
67
- aiSdkHarnessBackend({
68
- ...(options.runtime !== undefined ? { runtime: options.runtime } : {}),
69
- ...(options.bridgePort !== undefined ? { bridgePort: options.bridgePort } : {}),
70
- token: options.token ?? envValue(env, "VERCEL_TOKEN"),
71
- teamId: options.teamId ?? envValue(env, "VERCEL_TEAM_ID"),
72
- projectId: options.projectId ?? envValue(env, "VERCEL_PROJECT_ID"),
73
- ...(options.model !== undefined ? { model: options.model } : {}),
74
- ...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}),
75
- ...(options.thinking !== undefined ? { thinking: options.thinking } : {}),
76
- ...(options.startupTimeoutMs !== undefined
77
- ? { startupTimeoutMs: options.startupTimeoutMs }
78
- : {}),
79
- ...(options.createHarness !== undefined ? { createHarness: options.createHarness } : {}),
80
- ...(options.createSandboxProvider !== undefined
81
- ? { createSandboxProvider: options.createSandboxProvider }
82
- : {})
83
- }));
84
- }
85
- function contractFor(input) {
86
- const timeoutMs = input.options.timeoutMs ?? input.descriptor.policy.timeoutMs ?? DEFAULT_TIMEOUT_MS;
87
- return {
88
- version: "warrant.contract.v1",
89
- runId: `ensemble_${input.candidateId}`,
90
- issuedAt: new Date().toISOString(),
91
- issuer: { keyId: "ensemble-claude-code", role: "plane" },
92
- requestedBy: { kind: "service", id: "handoffkit-ensemble" },
93
- agent: { kind: "claude-code" },
94
- task: { prompt: input.descriptor.prompt },
95
- runner: {
96
- pool: input.options.pool ??
97
- input.descriptor.runtime.environmentId ??
98
- input.descriptor.runtime.id ??
99
- DEFAULT_POOL
100
- },
101
- workspace: {
102
- version: "warrant.manifest.v1",
103
- baseRef: (input.repoBaseSha ?? input.descriptor.baseGitSha) || ZERO_GIT_SHA,
104
- bundleHash: ZERO_HASH,
105
- untrackedFiles: [],
106
- deniedPatterns: [],
107
- deniedPaths: []
108
- },
109
- policyHash: ZERO_HASH,
110
- secrets: input.options.secrets?.map((secret) => ({ name: secret.name, scope: "ensemble" })) ?? [],
111
- network: input.options.network ??
112
- (input.descriptor.runtime.isolation?.networkPolicy
113
- ? {
114
- defaultDeny: input.descriptor.runtime.isolation.networkPolicy.defaultDeny,
115
- allowHosts: [...input.descriptor.runtime.isolation.networkPolicy.allowHosts]
116
- }
117
- : DEFAULT_CLAUDE_NETWORK),
118
- budget: {
119
- ...(input.descriptor.policy.budgetUsd !== undefined
120
- ? { maxSpendUsd: input.descriptor.policy.budgetUsd }
121
- : {}),
122
- maxDurationMin: Math.ceil(timeoutMs / 60_000)
123
- },
124
- disclosure: "minimal-context",
125
- isolation: "vercel-sandbox",
126
- execution: {
127
- kind: "agent",
128
- agent: { kind: "claude-code" },
129
- prompt: input.descriptor.prompt,
130
- timeoutMs,
131
- env: { vars: input.gate.authEnv, egressProxy: false },
132
- log: {
133
- stdout: "capture",
134
- stderr: "merge",
135
- maxBytes: input.options.logMaxBytes ?? DEFAULT_LOG_MAX_BYTES
136
- }
137
- },
138
- expiresAt: new Date(Date.now() + timeoutMs).toISOString(),
139
- signatures: []
140
- };
141
- }
142
- function hardeningFor(input) {
143
- const networkPolicy = input.options.network ??
144
- input.descriptor.runtime.isolation?.networkPolicy ??
145
- DEFAULT_CLAUDE_NETWORK;
146
- const mountPolicy = input.descriptor.runtime.isolation?.mountPolicy;
147
- const secretPolicy = input.descriptor.runtime.isolation?.secretPolicy;
148
- return {
149
- requested_isolation: "microvm",
150
- actual_isolation: input.finished ? "vercel-sandbox" : "process",
151
- runtime: {
152
- provider: "vercel-sandbox",
153
- runtime: input.options.runtime ??
154
- (input.descriptor.runtime.isolation?.kind === "microvm"
155
- ? input.descriptor.runtime.isolation.runtime
156
- : undefined) ??
157
- DEFAULT_RUNTIME,
158
- workdir: mountPolicy?.workdir ?? input.repoDir
159
- },
160
- mount_policy: {
161
- worktree_writable: mountPolicy?.worktreeWritable ?? true,
162
- read_only_caches: [...(mountPolicy?.readOnlyCachePaths ?? [])],
163
- ignored_dirs: [...(mountPolicy?.ignoredDirs ?? [".git", "node_modules", ".warrant"])]
164
- },
165
- network_policy: {
166
- default_deny: networkPolicy.defaultDeny,
167
- allow_hosts: [...networkPolicy.allowHosts],
168
- enforced: input.finished
169
- },
170
- cleanup: input.finished
171
- ? { attempted: true, succeeded: true, status: "succeeded" }
172
- : { attempted: false, succeeded: true, status: "not_required" },
173
- secret_absence: {
174
- secret_names: [
175
- ...(secretPolicy?.secretNames ?? input.options.secrets?.map((secret) => secret.name) ?? [])
176
- ],
177
- secret_value_hashes: [...(secretPolicy?.secretValueHashes ?? [])],
178
- injected_env_names: [...(secretPolicy?.injectedEnvNames ?? input.authEnvNames)],
179
- scanned: false,
180
- leaks_found: false,
181
- scan_scope: [],
182
- leak_count: 0
183
- }
184
- };
185
- }
186
- function skippedOutput(input) {
187
- const evidenceHash = artifactHash(input.reason);
188
- const repoDir = input.runInput.worktree?.path ?? input.runInput.descriptor.sourceRepo;
189
- return {
190
- candidateId: candidateId(input.runInput),
191
- model: input.runInput.model,
192
- status: "skipped",
193
- ...(input.runInput.worktree
194
- ? {
195
- branchName: input.runInput.worktree.branchName,
196
- worktreePath: input.runInput.worktree.path
197
- }
198
- : {}),
199
- transcript: input.reason,
200
- summary: input.reason,
201
- error: {
202
- kind: "capability_missing",
203
- message: input.reason,
204
- retryable: false
205
- },
206
- verification: {
207
- status: "skipped",
208
- evidence: [input.reason, evidenceHash],
209
- exitCode: 0
210
- },
211
- metadata: {
212
- adapter: "claude-code",
213
- credential_gate: "skipped",
214
- missing_credentials: [...input.missing],
215
- hardening: hardeningFor({
216
- descriptor: input.runInput.descriptor,
217
- options: input.options,
218
- repoDir,
219
- authEnvNames: [],
220
- finished: false
221
- })
222
- }
223
- };
224
- }
225
- function failureOutput(input) {
226
- const message = input.error instanceof Error ? input.error.message : String(input.error);
227
- const errorHash = artifactHash(message);
228
- const repoDir = input.runInput.worktree?.path ?? input.runInput.descriptor.sourceRepo;
229
- return {
230
- candidateId: candidateId(input.runInput),
231
- model: input.runInput.model,
232
- status: "failed",
233
- ...(input.runInput.worktree
234
- ? {
235
- branchName: input.runInput.worktree.branchName,
236
- worktreePath: input.runInput.worktree.path
237
- }
238
- : {}),
239
- transcript: `Claude Code harness failed: ${message}`,
240
- error: {
241
- kind: "provider_error",
242
- message,
243
- retryable: true
244
- },
245
- verification: {
246
- status: "failed",
247
- evidence: [errorHash],
248
- exitCode: 1
249
- },
250
- metadata: {
251
- adapter: "claude-code",
252
- credential_gate: "available",
253
- event_count: 0,
254
- auth_env_names: [...input.authEnvNames],
255
- hardening: hardeningFor({
256
- descriptor: input.runInput.descriptor,
257
- options: input.options,
258
- repoDir,
259
- authEnvNames: input.authEnvNames,
260
- finished: false
261
- })
262
- }
263
- };
264
- }
265
- export function createClaudeCodeHarness(options = {}) {
266
- const id = options.id ?? "claude-code";
267
- const env = options.env ?? process.env;
268
- const skipWhenUnavailable = options.skipWhenUnavailable ?? true;
269
- return {
270
- id,
271
- harnessKind: "claude_code",
272
- prepare: () => {
273
- const gate = credentialGate(env, options);
274
- if (!gate.available) {
275
- if (skipWhenUnavailable)
276
- return { gate };
277
- throw new CapabilityMismatchError(gate.reason);
278
- }
279
- return { gate, backend: backendFor(options, env) };
280
- },
281
- capabilities: () => {
282
- const gate = credentialGate(env, options);
283
- return {
284
- workspace_read: gate.available ? "supported" : "degraded",
285
- workspace_write: gate.available ? "supported" : "degraded",
286
- apply_patch: gate.available ? "supported" : "degraded",
287
- tool_records: "supported",
288
- verification: gate.available ? "supported" : "degraded",
289
- microvm_isolation: gate.available ? "supported" : "degraded",
290
- credential_gate: gate.available ? "supported" : "degraded"
291
- };
292
- },
293
- verificationProfile: () => ({
294
- id: `${id}-verification`,
295
- requiredEvidence: ["structured transcript", "exit code", "worktree diff or skip reason"]
296
- }),
297
- run: async (runInput) => {
298
- const state = runInput.prepared;
299
- if (!state.gate.available) {
300
- return skippedOutput({
301
- runInput,
302
- reason: state.gate.reason,
303
- missing: state.gate.missing,
304
- options
305
- });
306
- }
307
- const id = candidateId(runInput);
308
- const repoDir = runInput.worktree?.path ?? runInput.descriptor.workspace ?? runInput.descriptor.sourceRepo;
309
- const backend = state.backend ?? backendFor(options, env);
310
- const contract = contractFor({
311
- descriptor: runInput.descriptor,
312
- candidateId: id,
313
- options,
314
- gate: state.gate,
315
- ...(runInput.worktree ? { repoBaseSha: runInput.worktree.baseGitSha } : {})
316
- });
317
- const events = [];
318
- const authEnvNames = Object.keys(state.gate.authEnv);
319
- try {
320
- const result = await backend.execute({
321
- contract,
322
- repoDir,
323
- secrets: options.secrets ?? [],
324
- execution: prepareExecution({ contract, mockScriptPath: "/tmp/mock-agent.js" }),
325
- emit: (event) => {
326
- events.push(event);
327
- }
328
- });
329
- const transcript = result.log.toString("utf8");
330
- const outputHash = artifactHash(transcript);
331
- const status = result.exitCode === 0 ? "succeeded" : "failed";
332
- return {
333
- candidateId: id,
334
- model: runInput.model,
335
- status,
336
- ...(runInput.worktree
337
- ? {
338
- branchName: runInput.worktree.branchName,
339
- worktreePath: runInput.worktree.path
340
- }
341
- : {}),
342
- transcript,
343
- toolRecords: [
344
- {
345
- execution_id: `exec_${id}`,
346
- plan_id: `plan_${id}`,
347
- status,
348
- output_hash: outputHash
349
- }
350
- ],
351
- verification: {
352
- status,
353
- evidence: [`exit_code=${result.exitCode}`, outputHash],
354
- exitCode: result.exitCode
355
- },
356
- ...(status === "failed"
357
- ? {
358
- error: {
359
- kind: "provider_error",
360
- message: "Claude Code harness exited non-zero",
361
- retryable: true
362
- }
363
- }
364
- : {}),
365
- metadata: {
366
- adapter: "claude-code",
367
- backend_isolation: backend.isolation,
368
- credential_gate: "available",
369
- event_count: events.length,
370
- auth_env_names: authEnvNames,
371
- hardening: hardeningFor({
372
- descriptor: runInput.descriptor,
373
- options,
374
- repoDir,
375
- authEnvNames,
376
- finished: true
377
- })
378
- }
379
- };
380
- }
381
- catch (error) {
382
- if (skipWhenUnavailable && error instanceof CapabilityMismatchError) {
383
- return skippedOutput({
384
- runInput,
385
- reason: error.message,
386
- missing: ["capability_mismatch"],
387
- options
388
- });
389
- }
390
- return failureOutput({ runInput, error, options, authEnvNames });
391
- }
392
- },
393
- collectArtifacts: () => []
394
- };
395
- }
396
- export function claudeCodeHarness(options = {}) {
397
- return createClaudeCodeHarness(options);
398
- }
package/dist/codex.d.ts DELETED
@@ -1,69 +0,0 @@
1
- import type { HarnessAdapter } from "./harness.js";
2
- export type CodexSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
3
- export type CodexApprovalPolicy = "untrusted" | "on-failure" | "on-request" | "never";
4
- export type CodexAmbientProvider = {
5
- kind: "ambient";
6
- credentialEnvNames?: readonly string[];
7
- };
8
- export type CodexResponsesProvider = {
9
- kind: "responses";
10
- baseUrl: string;
11
- apiKey?: string;
12
- apiKeyEnvName?: string;
13
- requiresOpenAiAuth?: boolean;
14
- providerId?: string;
15
- name?: string;
16
- };
17
- export type CodexOpenAiCompatibleProvider = {
18
- kind: "openai-compatible";
19
- baseUrl: string;
20
- apiKey?: string;
21
- apiKeyEnvName?: string;
22
- defaultModel?: string;
23
- providerId?: string;
24
- name?: string;
25
- };
26
- export type CodexProvider = CodexAmbientProvider | CodexResponsesProvider | CodexOpenAiCompatibleProvider;
27
- export type CodexExecInput = {
28
- command: string;
29
- args: string[];
30
- cwd: string;
31
- env: Record<string, string>;
32
- timeoutMs?: number;
33
- };
34
- export type CodexExecResult = {
35
- stdout: string;
36
- stderr: string;
37
- exitCode: number;
38
- timedOut?: boolean;
39
- };
40
- export type CodexExecRunner = (input: CodexExecInput) => Promise<CodexExecResult> | CodexExecResult;
41
- export type CodexHarnessOptions = {
42
- id?: string;
43
- command?: string;
44
- cwd?: string;
45
- timeoutMs?: number;
46
- env?: Record<string, string | undefined>;
47
- provider?: CodexProvider;
48
- runner?: CodexExecRunner;
49
- sandboxMode?: CodexSandboxMode;
50
- approvalPolicy?: CodexApprovalPolicy;
51
- keepCodexHome?: boolean;
52
- };
53
- export type CodexHarnessEnv = Record<string, string | undefined>;
54
- export type CodexConfigTomlInput = {
55
- model: string;
56
- sandboxMode: CodexSandboxMode;
57
- approvalPolicy: CodexApprovalPolicy;
58
- provider?: {
59
- providerId?: string;
60
- name?: string;
61
- baseUrl: string;
62
- apiKeyEnvName?: string;
63
- requiresOpenAiAuth: boolean;
64
- };
65
- };
66
- export declare function codexHarnessCredentialSkipReason(env?: CodexHarnessEnv, options?: Pick<CodexHarnessOptions, "provider">): string | undefined;
67
- export declare function codexConfigToml(input: CodexConfigTomlInput): string;
68
- export declare function createCodexHarness(options?: CodexHarnessOptions): HarnessAdapter;
69
- export declare const codexHarness: typeof createCodexHarness;