@sweny-ai/core 0.1.198 → 0.1.200
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/dist/agent-env.d.ts +94 -2
- package/dist/agent-env.js +144 -7
- package/dist/budget.d.ts +77 -11
- package/dist/budget.js +132 -16
- package/dist/cli/renderer.js +5 -0
- package/dist/cli/run-history.d.ts +12 -0
- package/dist/cli/run-history.js +16 -0
- package/dist/cli/run-output.d.ts +16 -1
- package/dist/cli/run-output.js +46 -4
- package/dist/cli/runs.js +38 -1
- package/dist/cli/ticket.js +7 -2
- package/dist/eval/index.d.ts +10 -0
- package/dist/eval/index.js +27 -6
- package/dist/eval/judge.d.ts +3 -0
- package/dist/eval/judge.js +12 -2
- package/dist/executor.d.ts +5 -1
- package/dist/executor.js +276 -31
- package/dist/harness/acp.d.ts +5 -2
- package/dist/harness/acp.js +11 -23
- package/dist/harness/auth.js +2 -1
- package/dist/harness/claude-code.d.ts +18 -13
- package/dist/harness/claude-code.js +34 -15
- package/dist/harness/codex.d.ts +22 -5
- package/dist/harness/codex.js +67 -11
- package/dist/harness/compat.js +1 -0
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/harness/pi.d.ts +11 -5
- package/dist/harness/pi.js +27 -9
- package/dist/harness/prompts.d.ts +11 -1
- package/dist/harness/prompts.js +21 -2
- package/dist/harness/sandbox-wrapper.d.ts +13 -2
- package/dist/harness/sandbox-wrapper.js +29 -4
- package/dist/harness/types.d.ts +9 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/journal.d.ts +48 -10
- package/dist/journal.js +164 -16
- package/dist/safe-outputs.d.ts +9 -0
- package/dist/safe-outputs.js +41 -26
- package/dist/skills/linear.js +3 -1
- package/dist/studio.js +2 -1
- package/dist/types.d.ts +34 -1
- package/package.json +1 -1
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* This is the ONLY supported LLM backend. The whole point of sweny
|
|
9
9
|
* is to use headless Claude Code - never the raw Anthropic API.
|
|
10
10
|
*/
|
|
11
|
-
import type { Claude, Tool, ToolContext, Logger, McpServerConfig } from "../types.js";
|
|
12
|
-
import { type SandboxMode } from "../agent-env.js";
|
|
11
|
+
import type { Claude, Tool, ToolContext, NodeUsage, Logger, McpServerConfig } from "../types.js";
|
|
12
|
+
import { type HarnessEnvOptions, type SandboxMode } from "../agent-env.js";
|
|
13
13
|
import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, ToolClass } from "./types.js";
|
|
14
14
|
import { type HarnessPolicyMode } from "./policy.js";
|
|
15
15
|
import { CLAUDE_CODE_CAPABILITIES } from "./capabilities.js";
|
|
@@ -108,7 +108,7 @@ export interface ResolveAuthEnvOpts {
|
|
|
108
108
|
* Pure: returns a copy, never mutates the input.
|
|
109
109
|
*/
|
|
110
110
|
export declare function resolveAuthEnv(env: Record<string, string>, opts?: ResolveAuthEnvOpts): Record<string, string>;
|
|
111
|
-
export interface ClaudeCodeHarnessOptions {
|
|
111
|
+
export interface ClaudeCodeHarnessOptions extends HarnessEnvOptions {
|
|
112
112
|
/** Model override */
|
|
113
113
|
model?: string;
|
|
114
114
|
/** Max turns for tool use loop (default: 20) */
|
|
@@ -144,7 +144,7 @@ export interface ClaudeCodeHarnessOptions {
|
|
|
144
144
|
sandboxAllowedDomains?: string[];
|
|
145
145
|
/** Sandbox preflight probe (test seam). Returns a reason when the sandbox cannot run. */
|
|
146
146
|
sandboxProbe?: () => string | undefined;
|
|
147
|
-
/** Login probe for `preflight()` (test seam). Default: {@link claudeCodeAuth} over `process.env
|
|
147
|
+
/** Login probe for `preflight()` (test seam). Default: {@link claudeCodeAuth} over `process.env` with `env` / `envRemove` applied. */
|
|
148
148
|
authProbe?: AuthProbe;
|
|
149
149
|
/**
|
|
150
150
|
* Serve skill tools through the SwenyToolBridge (a stdio MCP shim over a
|
|
@@ -184,6 +184,7 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
|
|
|
184
184
|
private sandboxProbe;
|
|
185
185
|
private sandboxWarned;
|
|
186
186
|
private envScope;
|
|
187
|
+
private envOverlay;
|
|
187
188
|
/** Floor facts from the latest runQuery, read by run() for the receipt. */
|
|
188
189
|
private lastPolicyFacts;
|
|
189
190
|
private toolBridge;
|
|
@@ -192,15 +193,17 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
|
|
|
192
193
|
private policyMode;
|
|
193
194
|
constructor(opts?: ClaudeCodeHarnessOptions);
|
|
194
195
|
/**
|
|
195
|
-
* Build env for the Claude Code subprocess (#360).
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
196
|
+
* Build env for the Claude Code subprocess (#360). When env scoping is on
|
|
197
|
+
* (default in CI, see {@link resolveEnvScope}) the process env is narrowed
|
|
198
|
+
* to the allowlist plus `extraVars` (the node's declared skill env vars)
|
|
199
|
+
* and the operator passthrough list, and the withheld names (never values)
|
|
200
|
+
* are reported once per process ({@link reportWithheldEnv}). When off, the
|
|
201
|
+
* full env passes through as before. Then the caller's `envRemove` / `env`
|
|
202
|
+
* ({@link applyEnvOverlay}; overlaid auth replaces the inherited auth), then
|
|
203
|
+
* auth precedence ({@link resolveAuthEnv}, mode from `process.env.SWENY_AUTH`),
|
|
204
|
+
* then skill credentials are dropped ({@link finishAgentEnv}), overlaid ones
|
|
205
|
+
* included: only the names the node granted with `agent_env` reach the agent.
|
|
206
|
+
* `process.env` is never modified.
|
|
204
207
|
*/
|
|
205
208
|
private buildEnv;
|
|
206
209
|
/** Which harness (and SDK version) produced a result. */
|
|
@@ -247,6 +250,7 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
|
|
|
247
250
|
timeoutMs?: number;
|
|
248
251
|
/** Caller-supplied abort signal. Aborting it interrupts the query. */
|
|
249
252
|
signal?: AbortSignal;
|
|
253
|
+
onUsage?: (usage: NodeUsage) => void;
|
|
250
254
|
}): Promise<string | null>;
|
|
251
255
|
/** Free-text completion. Delegates to core {@link coreAsk} over {@link complete}. */
|
|
252
256
|
ask(opts: {
|
|
@@ -257,6 +261,7 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
|
|
|
257
261
|
timeoutMs?: number;
|
|
258
262
|
/** Caller-supplied abort signal. Aborting it interrupts the query. */
|
|
259
263
|
signal?: AbortSignal;
|
|
264
|
+
onUsage?: (usage: NodeUsage) => void;
|
|
260
265
|
}): Promise<string>;
|
|
261
266
|
}
|
|
262
267
|
/** @deprecated Use {@link ClaudeCodeHarnessOptions}. */
|
|
@@ -12,11 +12,11 @@ import { query, createSdkMcpServer, tool as sdkTool } from "@anthropic-ai/claude
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { realpathSync } from "node:fs";
|
|
14
14
|
import { consoleLogger } from "../types.js";
|
|
15
|
-
import { AGENT_AUTH_VARS, finishAgentEnv, reportWithheldEnv, parseList, resolveEnvScope, scopeAgentEnv, withPushBlocked, mcpWithheld, resolveAgentSandbox, } from "../agent-env.js";
|
|
15
|
+
import { AGENT_AUTH_VARS, agentEnvOverlay, applyEnvOverlay, finishAgentEnv, overlaidProcessEnv, reportWithheldEnv, parseList, resolveEnvScope, scopeAgentEnv, withPushBlocked, mcpWithheld, resolveAgentSandbox, } from "../agent-env.js";
|
|
16
16
|
import { gitCredentialGap, policyGate } from "./policy.js";
|
|
17
17
|
import { gitCredentialPolicy } from "../git-credentials.js";
|
|
18
18
|
import { CLAUDE_CODE_CAPABILITIES } from "./capabilities.js";
|
|
19
|
-
import { ask as coreAsk, evaluate as coreEvaluate, buildEvaluatePrompt, buildNodePrompt } from "./prompts.js";
|
|
19
|
+
import { ask as coreAsk, evaluate as coreEvaluate, buildEvaluatePrompt, buildNodePrompt, reportCompletionUsage, } from "./prompts.js";
|
|
20
20
|
import { jsonSchemaToZodShape, toolErrorToMcpResult, toolOutputToMcpResult } from "./tool-bridge/protocol.js";
|
|
21
21
|
import { startToolBridge } from "./tool-bridge/server.js";
|
|
22
22
|
import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./parse.js";
|
|
@@ -262,6 +262,7 @@ export class ClaudeCodeHarness {
|
|
|
262
262
|
sandboxProbe;
|
|
263
263
|
sandboxWarned = false;
|
|
264
264
|
envScope;
|
|
265
|
+
envOverlay;
|
|
265
266
|
/** Floor facts from the latest runQuery, read by run() for the receipt. */
|
|
266
267
|
lastPolicyFacts;
|
|
267
268
|
toolBridge;
|
|
@@ -278,35 +279,47 @@ export class ClaudeCodeHarness {
|
|
|
278
279
|
this.defaultMcpServers = opts.defaultMcpServers ?? {};
|
|
279
280
|
this.envPassthrough = opts.envPassthrough;
|
|
280
281
|
this.envScope = opts.envScope;
|
|
282
|
+
this.envOverlay = agentEnvOverlay(opts);
|
|
281
283
|
this.sandboxMode = opts.sandbox;
|
|
282
284
|
this.sandboxAllowedDomains = opts.sandboxAllowedDomains;
|
|
283
285
|
this.sandboxProbe = opts.sandboxProbe;
|
|
284
286
|
this.toolBridge = opts.toolBridge ?? process.env.SWENY_TOOL_BRIDGE === "1";
|
|
285
287
|
this.toolBridgeShim = opts.toolBridgeShim;
|
|
286
|
-
this.authProbe = opts.authProbe ?? (() => claudeCodeAuth(
|
|
288
|
+
this.authProbe = opts.authProbe ?? (() => claudeCodeAuth(overlaidProcessEnv(this.envOverlay, AGENT_AUTH_VARS)));
|
|
287
289
|
this.policyMode = opts.policy ?? "warn";
|
|
288
290
|
}
|
|
289
291
|
/**
|
|
290
|
-
* Build env for the Claude Code subprocess (#360).
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
292
|
+
* Build env for the Claude Code subprocess (#360). When env scoping is on
|
|
293
|
+
* (default in CI, see {@link resolveEnvScope}) the process env is narrowed
|
|
294
|
+
* to the allowlist plus `extraVars` (the node's declared skill env vars)
|
|
295
|
+
* and the operator passthrough list, and the withheld names (never values)
|
|
296
|
+
* are reported once per process ({@link reportWithheldEnv}). When off, the
|
|
297
|
+
* full env passes through as before. Then the caller's `envRemove` / `env`
|
|
298
|
+
* ({@link applyEnvOverlay}; overlaid auth replaces the inherited auth), then
|
|
299
|
+
* auth precedence ({@link resolveAuthEnv}, mode from `process.env.SWENY_AUTH`),
|
|
300
|
+
* then skill credentials are dropped ({@link finishAgentEnv}), overlaid ones
|
|
301
|
+
* included: only the names the node granted with `agent_env` reach the agent.
|
|
302
|
+
* `process.env` is never modified.
|
|
299
303
|
*/
|
|
300
304
|
buildEnv(access) {
|
|
301
305
|
const full = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
|
|
302
|
-
const authed = resolveAuthEnv(full, { logger: this.logger });
|
|
303
306
|
const passthrough = this.envPassthrough ?? parseList(process.env.SWENY_ENV_PASSTHROUGH);
|
|
304
|
-
let env =
|
|
307
|
+
let env = full;
|
|
305
308
|
if (resolveEnvScope(process.env, this.envScope, this.logger)) {
|
|
306
|
-
const scoped = scopeAgentEnv(
|
|
309
|
+
const scoped = scopeAgentEnv(full, {
|
|
310
|
+
extraVars: access?.envVars ?? [],
|
|
311
|
+
passthrough,
|
|
312
|
+
// An overlaid CLAUDE_CODE_USE_BEDROCK / _VERTEX decides which cloud vars are kept.
|
|
313
|
+
routing: this.envOverlay ? overlaidProcessEnv(this.envOverlay, AGENT_AUTH_VARS) : undefined,
|
|
314
|
+
logger: this.logger,
|
|
315
|
+
});
|
|
307
316
|
reportWithheldEnv(scoped.withheld, this.logger);
|
|
308
317
|
env = scoped.env;
|
|
309
318
|
}
|
|
319
|
+
env = applyEnvOverlay(env, this.envOverlay, AGENT_AUTH_VARS);
|
|
320
|
+
// Scoping keeps every ANTHROPIC_* / CLAUDE_* name, so auth resolves the same after it. The mode
|
|
321
|
+
// comes from process.env: an overlaid SWENY_AUTH reaches the agent, not sweny ("" reads as auto).
|
|
322
|
+
env = resolveAuthEnv(env, { mode: (process.env.SWENY_AUTH ?? ""), logger: this.logger });
|
|
310
323
|
return finishAgentEnv(env, { access, keep: AGENT_AUTH_VARS, passthrough, logger: this.logger }).env;
|
|
311
324
|
}
|
|
312
325
|
/** Which harness (and SDK version) produced a result. */
|
|
@@ -401,6 +414,9 @@ export class ClaudeCodeHarness {
|
|
|
401
414
|
// `strict` fails the node here instead.
|
|
402
415
|
const sandbox = resolveAgentSandbox({
|
|
403
416
|
env: process.env,
|
|
417
|
+
// A `HOME` / `CLAUDE_CONFIG_DIR` overlay selects another login file; deny it too.
|
|
418
|
+
agentEnv: overlaidProcessEnv(this.envOverlay, AGENT_AUTH_VARS),
|
|
419
|
+
cwd: this.cwd,
|
|
404
420
|
mode: opts.sandboxMode ?? this.sandboxMode,
|
|
405
421
|
allowedDomains: this.sandboxAllowedDomains,
|
|
406
422
|
nodeDomains: agentAccess?.domains,
|
|
@@ -816,6 +832,9 @@ export class ClaudeCodeHarness {
|
|
|
816
832
|
if (message.type === "result") {
|
|
817
833
|
const resultMsg = message;
|
|
818
834
|
sawResult = true;
|
|
835
|
+
// A completion that failed was still billed: report before the outcome.
|
|
836
|
+
if (req.onUsage)
|
|
837
|
+
reportCompletionUsage(req.onUsage, extractUsage(resultMsg), this.logger);
|
|
819
838
|
if (resultMsg.subtype === "success" && "result" in resultMsg) {
|
|
820
839
|
response = resultMsg.result;
|
|
821
840
|
}
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* by `policyGate`. Nothing is dropped silently.
|
|
25
25
|
*/
|
|
26
26
|
import type { Logger, McpServerConfig, ToolContext } from "../types.js";
|
|
27
|
-
import { type SandboxMode } from "../agent-env.js";
|
|
27
|
+
import { type HarnessEnvOptions, type SandboxMode } from "../agent-env.js";
|
|
28
28
|
import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, ToolClass } from "./types.js";
|
|
29
29
|
import { CODEX_CAPABILITIES } from "./capabilities.js";
|
|
30
30
|
import { type HarnessPolicyMode } from "./policy.js";
|
|
@@ -52,7 +52,7 @@ export declare const CODEX_ISOLATION_FEATURES_OFF: readonly ["apps", "plugins",
|
|
|
52
52
|
* through the sandbox wrapper, plus the host of `OPENAI_BASE_URL` when set.
|
|
53
53
|
*/
|
|
54
54
|
export declare function codexBackendHosts(env?: Record<string, string | undefined>): string[];
|
|
55
|
-
export interface CodexHarnessOptions {
|
|
55
|
+
export interface CodexHarnessOptions extends HarnessEnvOptions {
|
|
56
56
|
/** Model override (`--model`). Free text; sweny has no model opinion. */
|
|
57
57
|
model?: string;
|
|
58
58
|
/** Tool-call budget the watchdog enforces when a node sets no `max_turns` (default: 20). */
|
|
@@ -97,7 +97,7 @@ export interface CodexHarnessOptions {
|
|
|
97
97
|
};
|
|
98
98
|
/** Grace period between SIGTERM and SIGKILL when stopping Codex (default: 2000 ms). */
|
|
99
99
|
killGraceMs?: number;
|
|
100
|
-
/** Login probe for `preflight()` (test seam). Default: {@link codexAuth} over `process.env
|
|
100
|
+
/** Login probe for `preflight()` (test seam). Default: {@link codexAuth} over `process.env` with `env` / `envRemove` applied. */
|
|
101
101
|
authProbe?: AuthProbe;
|
|
102
102
|
}
|
|
103
103
|
/** A TOML value for `-c key=value`, as the official SDK writes it (sdk/typescript/src/exec.ts). */
|
|
@@ -134,6 +134,7 @@ export declare class CodexHarness implements AgentHarness {
|
|
|
134
134
|
private defaultMcpServers;
|
|
135
135
|
private envPassthrough;
|
|
136
136
|
private envScope;
|
|
137
|
+
private envOverlay;
|
|
137
138
|
private sandboxMode;
|
|
138
139
|
private sandboxProbe;
|
|
139
140
|
private sandboxWarned;
|
|
@@ -141,6 +142,7 @@ export declare class CodexHarness implements AgentHarness {
|
|
|
141
142
|
private loginWarned;
|
|
142
143
|
private policyMode;
|
|
143
144
|
private codexCommand;
|
|
145
|
+
private resolvedCommand;
|
|
144
146
|
private toolBridgeShim;
|
|
145
147
|
private killGraceMs;
|
|
146
148
|
private preflightResult;
|
|
@@ -159,12 +161,27 @@ export declare class CodexHarness implements AgentHarness {
|
|
|
159
161
|
ok: false;
|
|
160
162
|
reason: string;
|
|
161
163
|
}>;
|
|
164
|
+
/**
|
|
165
|
+
* `process.env` with the caller's `env` / `envRemove` applied, unscoped: where
|
|
166
|
+
* sweny reads Codex's own settings (`OPENAI_BASE_URL`) and the login probe.
|
|
167
|
+
* The scoped agent env would drop an inherited base URL.
|
|
168
|
+
*/
|
|
169
|
+
private hostEnv;
|
|
170
|
+
/**
|
|
171
|
+
* {@link hostEnv} as the login probe should see it: an inherited
|
|
172
|
+
* OPENAI_API_KEY does not count once `envRemove` names CODEX_API_KEY, since
|
|
173
|
+
* the agent env no longer derives CODEX_API_KEY from it.
|
|
174
|
+
*/
|
|
175
|
+
private loginEnv;
|
|
176
|
+
/** The Codex executable to spawn: the one the version check resolved and ran. */
|
|
177
|
+
private cliPath;
|
|
162
178
|
/** `codex --version`, checked against {@link MIN_CODEX_VERSION}. Cached per instance. */
|
|
163
179
|
private checkCli;
|
|
164
180
|
/**
|
|
165
181
|
* Env for the Codex process: scoped like Claude Code's, with Codex's auth
|
|
166
|
-
* vars instead of Anthropic's
|
|
167
|
-
* ({@link
|
|
182
|
+
* vars instead of Anthropic's, then the caller's `envRemove` / `env`
|
|
183
|
+
* ({@link applyEnvOverlay}). Skill credentials are dropped either way, overlaid
|
|
184
|
+
* ones included ({@link finishAgentEnv}), unless the node granted them with `agent_env`.
|
|
168
185
|
*/
|
|
169
186
|
private buildEnv;
|
|
170
187
|
/** The node policy after `disallowed_tools` names are translated into Codex classes. */
|
package/dist/harness/codex.js
CHANGED
|
@@ -28,11 +28,11 @@ import * as fs from "node:fs";
|
|
|
28
28
|
import * as os from "node:os";
|
|
29
29
|
import * as path from "node:path";
|
|
30
30
|
import { consoleLogger } from "../types.js";
|
|
31
|
-
import { CODEX_AUTH_VARS, CODEX_ENV_PREFIXES, checkSandboxSupport, finishAgentEnv, heldCredentials, parseList, reportWithheldEnv, resolveCodexAuthEnv, resolveEnvScope, resolveSandboxMode, scopeAgentEnv, withPushBlocked, mcpWithheld, } from "../agent-env.js";
|
|
31
|
+
import { CODEX_AUTH_VARS, CODEX_ENV_PREFIXES, agentEnvOverlay, applyEnvOverlay, checkSandboxSupport, finishAgentEnv, resolveExecutable, overlaidProcessEnv, heldCredentials, parseList, reportWithheldEnv, resolveCodexAuthEnv, resolveEnvScope, resolveSandboxMode, scopeAgentEnv, withPushBlocked, mcpWithheld, } from "../agent-env.js";
|
|
32
32
|
import { CODEX_CAPABILITIES } from "./capabilities.js";
|
|
33
33
|
import { policyGate, resolveHarnessPolicy } from "./policy.js";
|
|
34
34
|
import { gitCredentialPolicy } from "../git-credentials.js";
|
|
35
|
-
import { buildNodePrompt } from "./prompts.js";
|
|
35
|
+
import { buildNodePrompt, reportCompletionUsage } from "./prompts.js";
|
|
36
36
|
import { makeAbort } from "./abort.js";
|
|
37
37
|
import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./parse.js";
|
|
38
38
|
import { startToolBridge } from "./tool-bridge/server.js";
|
|
@@ -40,6 +40,13 @@ import { defaultSandboxWrapper, prepareAgentSpawn, wrappersFrom, } from "./sandb
|
|
|
40
40
|
import { TOKEN_ENV } from "./tool-bridge/protocol.js";
|
|
41
41
|
import { codexAuth } from "./auth.js";
|
|
42
42
|
export { CODEX_CAPABILITIES };
|
|
43
|
+
/**
|
|
44
|
+
* The credentials an `env` overlay replaces as a set ({@link applyEnvOverlay}):
|
|
45
|
+
* Codex's API key and access token, and `OPENAI_API_KEY`, which stands in for
|
|
46
|
+
* `CODEX_API_KEY` ({@link resolveCodexAuthEnv}). `CODEX_HOME` is a location,
|
|
47
|
+
* not a credential: overlaying it does not drop an inherited key.
|
|
48
|
+
*/
|
|
49
|
+
const CODEX_CREDENTIAL_VARS = ["CODEX_API_KEY", "CODEX_ACCESS_TOKEN", "OPENAI_API_KEY"];
|
|
43
50
|
/** Oldest Codex CLI whose flags this adapter uses (`--ignore-user-config`, `--ignore-rules`). */
|
|
44
51
|
export const MIN_CODEX_VERSION = "0.159.0";
|
|
45
52
|
/**
|
|
@@ -286,6 +293,7 @@ export class CodexHarness {
|
|
|
286
293
|
defaultMcpServers;
|
|
287
294
|
envPassthrough;
|
|
288
295
|
envScope;
|
|
296
|
+
envOverlay;
|
|
289
297
|
sandboxMode;
|
|
290
298
|
sandboxProbe;
|
|
291
299
|
sandboxWarned = false;
|
|
@@ -293,6 +301,7 @@ export class CodexHarness {
|
|
|
293
301
|
loginWarned = false;
|
|
294
302
|
policyMode;
|
|
295
303
|
codexCommand;
|
|
304
|
+
resolvedCommand;
|
|
296
305
|
toolBridgeShim;
|
|
297
306
|
killGraceMs;
|
|
298
307
|
preflightResult;
|
|
@@ -308,6 +317,7 @@ export class CodexHarness {
|
|
|
308
317
|
this.defaultMcpServers = opts.defaultMcpServers ?? {};
|
|
309
318
|
this.envPassthrough = opts.envPassthrough;
|
|
310
319
|
this.envScope = opts.envScope;
|
|
320
|
+
this.envOverlay = agentEnvOverlay(opts);
|
|
311
321
|
this.sandboxMode = opts.sandbox;
|
|
312
322
|
this.sandboxProbe = opts.sandboxProbe;
|
|
313
323
|
this.sandboxWrapper = opts.sandboxWrapper;
|
|
@@ -315,7 +325,7 @@ export class CodexHarness {
|
|
|
315
325
|
this.codexCommand = opts.codexCommand ?? { command: process.env.SWENY_CODEX_PATH || "codex", args: [] };
|
|
316
326
|
this.toolBridgeShim = opts.toolBridgeShim;
|
|
317
327
|
this.killGraceMs = opts.killGraceMs ?? 2000;
|
|
318
|
-
this.authProbe = opts.authProbe ?? (() => codexAuth(
|
|
328
|
+
this.authProbe = opts.authProbe ?? (() => codexAuth(this.loginEnv()));
|
|
319
329
|
}
|
|
320
330
|
info() {
|
|
321
331
|
return { id: this.id, version: this.version };
|
|
@@ -331,11 +341,40 @@ export class CodexHarness {
|
|
|
331
341
|
const auth = this.authProbe();
|
|
332
342
|
return auth.ok ? cli : { ok: false, reason: auth.reason };
|
|
333
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* `process.env` with the caller's `env` / `envRemove` applied, unscoped: where
|
|
346
|
+
* sweny reads Codex's own settings (`OPENAI_BASE_URL`) and the login probe.
|
|
347
|
+
* The scoped agent env would drop an inherited base URL.
|
|
348
|
+
*/
|
|
349
|
+
hostEnv() {
|
|
350
|
+
return overlaidProcessEnv(this.envOverlay, CODEX_CREDENTIAL_VARS);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* {@link hostEnv} as the login probe should see it: an inherited
|
|
354
|
+
* OPENAI_API_KEY does not count once `envRemove` names CODEX_API_KEY, since
|
|
355
|
+
* the agent env no longer derives CODEX_API_KEY from it.
|
|
356
|
+
*/
|
|
357
|
+
loginEnv() {
|
|
358
|
+
const env = this.hostEnv();
|
|
359
|
+
const overlay = this.envOverlay;
|
|
360
|
+
if (overlay?.remove.includes("CODEX_API_KEY") && !Object.hasOwn(overlay.env, "OPENAI_API_KEY")) {
|
|
361
|
+
delete env.OPENAI_API_KEY;
|
|
362
|
+
}
|
|
363
|
+
return env;
|
|
364
|
+
}
|
|
365
|
+
/** The Codex executable to spawn: the one the version check resolved and ran. */
|
|
366
|
+
cliPath() {
|
|
367
|
+
return this.resolvedCommand ?? this.codexCommand.command;
|
|
368
|
+
}
|
|
334
369
|
/** `codex --version`, checked against {@link MIN_CODEX_VERSION}. Cached per instance. */
|
|
335
370
|
checkCli() {
|
|
336
371
|
this.preflightResult ??= new Promise((resolve) => {
|
|
337
372
|
const { command, args } = this.codexCommand;
|
|
338
|
-
|
|
373
|
+
// Probe the file the spawn will run: resolved on the run's PATH and cwd, then reused at spawn.
|
|
374
|
+
const hostEnv = this.hostEnv();
|
|
375
|
+
this.resolvedCommand = resolveExecutable(command, hostEnv, this.cwd);
|
|
376
|
+
const opts = { timeout: 30_000, cwd: this.cwd, env: hostEnv };
|
|
377
|
+
execFile(this.resolvedCommand ?? command, [...args, "--version"], opts, (err, stdout) => {
|
|
339
378
|
if (err) {
|
|
340
379
|
const code = err.code;
|
|
341
380
|
resolve({
|
|
@@ -370,11 +409,16 @@ export class CodexHarness {
|
|
|
370
409
|
}
|
|
371
410
|
/**
|
|
372
411
|
* Env for the Codex process: scoped like Claude Code's, with Codex's auth
|
|
373
|
-
* vars instead of Anthropic's
|
|
374
|
-
* ({@link
|
|
412
|
+
* vars instead of Anthropic's, then the caller's `envRemove` / `env`
|
|
413
|
+
* ({@link applyEnvOverlay}). Skill credentials are dropped either way, overlaid
|
|
414
|
+
* ones included ({@link finishAgentEnv}), unless the node granted them with `agent_env`.
|
|
375
415
|
*/
|
|
376
416
|
buildEnv(access) {
|
|
377
|
-
const
|
|
417
|
+
const overlay = this.envOverlay;
|
|
418
|
+
const removed = new Set(overlay?.remove ?? []);
|
|
419
|
+
// `envRemove` goes first, before the OPENAI_API_KEY alias, so a removed OPENAI_API_KEY is not
|
|
420
|
+
// copied; a removed CODEX_API_KEY made by the alias is dropped again with the overlay below.
|
|
421
|
+
const full = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null && !removed.has(e[0])));
|
|
378
422
|
const authed = resolveCodexAuthEnv(full);
|
|
379
423
|
const passthrough = this.envPassthrough ?? parseList(process.env.SWENY_ENV_PASSTHROUGH);
|
|
380
424
|
let env = authed;
|
|
@@ -389,6 +433,16 @@ export class CodexHarness {
|
|
|
389
433
|
reportWithheldEnv(scoped.withheld, this.logger);
|
|
390
434
|
env = scoped.env;
|
|
391
435
|
}
|
|
436
|
+
if (overlay) {
|
|
437
|
+
env = applyEnvOverlay(env, overlay, CODEX_CREDENTIAL_VARS);
|
|
438
|
+
if (CODEX_CREDENTIAL_VARS.some((c) => Object.hasOwn(overlay.env, c))) {
|
|
439
|
+
// An overlaid OPENAI_API_KEY stands in for CODEX_API_KEY like an inherited one, and is then
|
|
440
|
+
// dropped, as scoping drops an inherited one: Codex reads only CODEX_API_KEY.
|
|
441
|
+
if (!env.CODEX_API_KEY && overlay.env.OPENAI_API_KEY)
|
|
442
|
+
env.CODEX_API_KEY = overlay.env.OPENAI_API_KEY;
|
|
443
|
+
delete env.OPENAI_API_KEY;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
392
446
|
return finishAgentEnv(env, { access, keep: CODEX_AUTH_VARS, passthrough, logger: this.logger }).env;
|
|
393
447
|
}
|
|
394
448
|
/** The node policy after `disallowed_tools` names are translated into Codex classes. */
|
|
@@ -466,7 +520,7 @@ export class CodexHarness {
|
|
|
466
520
|
if (opts.sandbox === "workspace-write") {
|
|
467
521
|
args.push(...cfg("sandbox_workspace_write.network_access", opts.network));
|
|
468
522
|
}
|
|
469
|
-
const baseUrl =
|
|
523
|
+
const baseUrl = this.hostEnv().OPENAI_BASE_URL;
|
|
470
524
|
if (baseUrl)
|
|
471
525
|
args.push(...cfg("openai_base_url", baseUrl));
|
|
472
526
|
return args;
|
|
@@ -866,9 +920,9 @@ export class CodexHarness {
|
|
|
866
920
|
const prep = await prepareAgentSpawn({
|
|
867
921
|
caps: this.capabilities,
|
|
868
922
|
policy,
|
|
869
|
-
spawn: { command: this.
|
|
923
|
+
spawn: { command: this.cliPath(), args: argsFor(withSchema), env, cwd: this.cwd },
|
|
870
924
|
wrapper: wrapper ?? null,
|
|
871
|
-
harnessEgress: codexBackendHosts(),
|
|
925
|
+
harnessEgress: codexBackendHosts(this.hostEnv()),
|
|
872
926
|
env: process.env,
|
|
873
927
|
});
|
|
874
928
|
degraded = prep.degraded;
|
|
@@ -994,13 +1048,15 @@ export class CodexHarness {
|
|
|
994
1048
|
"-",
|
|
995
1049
|
];
|
|
996
1050
|
const out = await this.exec({
|
|
997
|
-
spawn: { command: this.
|
|
1051
|
+
spawn: { command: this.cliPath(), args, env: this.buildEnv(), cwd: this.cwd },
|
|
998
1052
|
prompt: req.prompt,
|
|
999
1053
|
timeoutMs: req.timeoutMs,
|
|
1000
1054
|
signal: req.signal,
|
|
1001
1055
|
// Classification calls use no tools; any tool call is over budget.
|
|
1002
1056
|
maxToolCalls: 0,
|
|
1003
1057
|
});
|
|
1058
|
+
// A completion that failed was still billed: report before the outcome.
|
|
1059
|
+
reportCompletionUsage(req.onUsage, out.usage, this.logger);
|
|
1004
1060
|
if (out.spawnError)
|
|
1005
1061
|
return failClosed(`codex could not start: ${out.spawnError}`);
|
|
1006
1062
|
if (out.abortReason === "timeout")
|
package/dist/harness/compat.js
CHANGED
package/dist/harness/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { claudeCompat, asClaude } from "./compat.js";
|
|
|
10
10
|
export type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, } from "./types.js";
|
|
11
11
|
export { policyGate, budgetGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES, } from "./policy.js";
|
|
12
12
|
export type { HarnessPolicyMode } from "./policy.js";
|
|
13
|
-
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
|
|
13
|
+
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt, reportCompletionUsage } from "./prompts.js";
|
|
14
14
|
export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
|
|
15
15
|
export type { ClaudeCodeHarnessOptions } from "./claude-code.js";
|
|
16
16
|
export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
|
package/dist/harness/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { PiHarness } from "./pi.js";
|
|
|
9
9
|
import { parseAcpAgent, unsupportedAgentError } from "./agents.js";
|
|
10
10
|
export { claudeCompat, asClaude } from "./compat.js";
|
|
11
11
|
export { policyGate, budgetGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES, } from "./policy.js";
|
|
12
|
-
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
|
|
12
|
+
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt, reportCompletionUsage } from "./prompts.js";
|
|
13
13
|
export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
|
|
14
14
|
export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
|
|
15
15
|
export { AcpHarness, ACP_CAPABILITIES, ACP_PROTOCOL_VERSION } from "./acp.js";
|
package/dist/harness/pi.d.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* `policyGate`. Nothing is dropped silently.
|
|
30
30
|
*/
|
|
31
31
|
import type { Logger, McpServerConfig, NodeUsage, ToolContext, Tool } from "../types.js";
|
|
32
|
-
import { type SandboxMode } from "../agent-env.js";
|
|
32
|
+
import { type HarnessEnvOptions, type SandboxMode } from "../agent-env.js";
|
|
33
33
|
import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, ToolClass } from "./types.js";
|
|
34
34
|
import { PI_CAPABILITIES } from "./capabilities.js";
|
|
35
35
|
import { type HarnessPolicyMode } from "./policy.js";
|
|
@@ -49,7 +49,7 @@ export declare const PI_BRIDGE_SERVER = "sweny-core";
|
|
|
49
49
|
* providers) are added with `SWENY_SANDBOX_ALLOWED_DOMAINS`.
|
|
50
50
|
*/
|
|
51
51
|
export declare function piBackendHosts(env?: Record<string, string | undefined>, model?: string): string[];
|
|
52
|
-
export interface PiHarnessOptions {
|
|
52
|
+
export interface PiHarnessOptions extends HarnessEnvOptions {
|
|
53
53
|
/** Model passed to `--model`. Free text (`provider/id`, `id:thinking`); sweny has no model opinion. */
|
|
54
54
|
model?: string;
|
|
55
55
|
/**
|
|
@@ -160,10 +160,12 @@ export declare class PiHarness implements AgentHarness {
|
|
|
160
160
|
private defaultMcpServers;
|
|
161
161
|
private envPassthrough;
|
|
162
162
|
private envScope;
|
|
163
|
+
private envOverlay;
|
|
163
164
|
private sandboxMode;
|
|
164
165
|
private sandboxWrapper;
|
|
165
166
|
private policyMode;
|
|
166
167
|
private piCommand;
|
|
168
|
+
private resolvedCommand;
|
|
167
169
|
private modelsFile;
|
|
168
170
|
private toolBridgeShim;
|
|
169
171
|
private abortGraceMs;
|
|
@@ -174,6 +176,8 @@ export declare class PiHarness implements AgentHarness {
|
|
|
174
176
|
private version;
|
|
175
177
|
constructor(opts?: PiHarnessOptions);
|
|
176
178
|
info(): HarnessInfo;
|
|
179
|
+
/** The pi executable to spawn: the one the version check resolved and ran. */
|
|
180
|
+
private cliPath;
|
|
177
181
|
/** `pi --version`, checked against {@link MIN_PI_VERSION}. Cached per instance. */
|
|
178
182
|
preflight(): Promise<{
|
|
179
183
|
ok: true;
|
|
@@ -182,14 +186,16 @@ export declare class PiHarness implements AgentHarness {
|
|
|
182
186
|
ok: false;
|
|
183
187
|
reason: string;
|
|
184
188
|
}>;
|
|
185
|
-
/** Which provider's credential this call gets ({@link resolvePiProvider}). */
|
|
189
|
+
/** Which provider's credential this call gets ({@link resolvePiProvider}), over the env with the overlay applied. */
|
|
186
190
|
private resolveProvider;
|
|
187
191
|
/**
|
|
188
192
|
* Env for the pi process: scoped like Claude Code's, with ONE provider's
|
|
189
193
|
* credentials instead of Anthropic's (`providerVars`, from
|
|
190
194
|
* {@link resolvePiProvider}). Every other provider key is dropped, scoped or
|
|
191
|
-
* not, since pi's bash tool inherits this env.
|
|
192
|
-
*
|
|
195
|
+
* not, since pi's bash tool inherits this env. The caller's `envRemove` /
|
|
196
|
+
* `env` are applied after scoping ({@link applyEnvOverlay}), so the same rules
|
|
197
|
+
* hold for overlaid names. Skill credentials are dropped too, overlaid ones
|
|
198
|
+
* included ({@link finishAgentEnv}), unless the node granted them with `agent_env`.
|
|
193
199
|
*/
|
|
194
200
|
private buildEnv;
|
|
195
201
|
/** The node policy after `disallowed_tools` names are translated into pi classes. */
|
package/dist/harness/pi.js
CHANGED
|
@@ -34,17 +34,19 @@ import * as fs from "node:fs";
|
|
|
34
34
|
import * as os from "node:os";
|
|
35
35
|
import * as path from "node:path";
|
|
36
36
|
import { consoleLogger } from "../types.js";
|
|
37
|
-
import { PI_AUTH_VARS, PI_ENV_PREFIXES, finishAgentEnv, heldCredentials, parseList, reportWithheldEnv, resolveEnvScope, resolvePiProvider, resolveSandboxMode, scopeAgentEnv, withPushBlocked, mcpWithheld, } from "../agent-env.js";
|
|
37
|
+
import { PI_AUTH_VARS, PI_ENV_PREFIXES, PI_NON_SECRET_VARS, agentEnvOverlay, applyEnvOverlay, overlaidProcessEnv, resolveExecutable, finishAgentEnv, heldCredentials, parseList, reportWithheldEnv, resolveEnvScope, resolvePiProvider, resolveSandboxMode, scopeAgentEnv, withPushBlocked, mcpWithheld, } from "../agent-env.js";
|
|
38
38
|
import { PI_CAPABILITIES } from "./capabilities.js";
|
|
39
39
|
import { policyGate, resolveHarnessPolicy } from "./policy.js";
|
|
40
40
|
import { gitCredentialPolicy } from "../git-credentials.js";
|
|
41
|
-
import { buildNodePrompt } from "./prompts.js";
|
|
41
|
+
import { buildNodePrompt, reportCompletionUsage } from "./prompts.js";
|
|
42
42
|
import { makeAbort } from "./abort.js";
|
|
43
43
|
import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./parse.js";
|
|
44
44
|
import { startToolBridge } from "./tool-bridge/server.js";
|
|
45
45
|
import { defaultSandboxWrapper, prepareAgentSpawn, wrappersFrom, } from "./sandbox-wrapper.js";
|
|
46
46
|
import { TOKEN_ENV } from "./tool-bridge/protocol.js";
|
|
47
47
|
export { PI_CAPABILITIES };
|
|
48
|
+
/** The provider secrets an `env` overlay replaces as a set ({@link applyEnvOverlay}); region and project settings are not among them. */
|
|
49
|
+
const PI_CREDENTIAL_VARS = PI_AUTH_VARS.filter((v) => !PI_NON_SECRET_VARS.has(v));
|
|
48
50
|
/**
|
|
49
51
|
* Oldest pi this adapter is written against. The docs and source were read at
|
|
50
52
|
* v0.99.2; older releases are unverified, so they are refused.
|
|
@@ -323,10 +325,12 @@ export class PiHarness {
|
|
|
323
325
|
defaultMcpServers;
|
|
324
326
|
envPassthrough;
|
|
325
327
|
envScope;
|
|
328
|
+
envOverlay;
|
|
326
329
|
sandboxMode;
|
|
327
330
|
sandboxWrapper;
|
|
328
331
|
policyMode;
|
|
329
332
|
piCommand;
|
|
333
|
+
resolvedCommand;
|
|
330
334
|
modelsFile;
|
|
331
335
|
toolBridgeShim;
|
|
332
336
|
abortGraceMs;
|
|
@@ -346,6 +350,7 @@ export class PiHarness {
|
|
|
346
350
|
this.defaultMcpServers = opts.defaultMcpServers ?? {};
|
|
347
351
|
this.envPassthrough = opts.envPassthrough;
|
|
348
352
|
this.envScope = opts.envScope;
|
|
353
|
+
this.envOverlay = agentEnvOverlay(opts);
|
|
349
354
|
this.sandboxMode = opts.sandbox;
|
|
350
355
|
this.sandboxWrapper = opts.sandboxWrapper;
|
|
351
356
|
this.policyMode = opts.policy ?? resolveHarnessPolicy(process.env, undefined, this.logger);
|
|
@@ -359,11 +364,19 @@ export class PiHarness {
|
|
|
359
364
|
info() {
|
|
360
365
|
return { id: this.id, version: this.version };
|
|
361
366
|
}
|
|
367
|
+
/** The pi executable to spawn: the one the version check resolved and ran. */
|
|
368
|
+
cliPath() {
|
|
369
|
+
return this.resolvedCommand ?? this.piCommand.command;
|
|
370
|
+
}
|
|
362
371
|
/** `pi --version`, checked against {@link MIN_PI_VERSION}. Cached per instance. */
|
|
363
372
|
preflight() {
|
|
364
373
|
this.preflightResult ??= new Promise((resolve) => {
|
|
365
374
|
const { command, args } = this.piCommand;
|
|
366
|
-
|
|
375
|
+
// Probe the file the spawn will run: resolved on the run's PATH and cwd, then reused at spawn.
|
|
376
|
+
const hostEnv = overlaidProcessEnv(this.envOverlay, PI_CREDENTIAL_VARS);
|
|
377
|
+
this.resolvedCommand = resolveExecutable(command, hostEnv, this.cwd);
|
|
378
|
+
const opts = { timeout: 30_000, cwd: this.cwd, env: hostEnv };
|
|
379
|
+
execFile(this.resolvedCommand ?? command, [...args, "--version"], opts, (err, stdout, stderr) => {
|
|
367
380
|
if (err) {
|
|
368
381
|
const code = err.code;
|
|
369
382
|
resolve({
|
|
@@ -390,16 +403,18 @@ export class PiHarness {
|
|
|
390
403
|
});
|
|
391
404
|
return this.preflightResult;
|
|
392
405
|
}
|
|
393
|
-
/** Which provider's credential this call gets ({@link resolvePiProvider}). */
|
|
406
|
+
/** Which provider's credential this call gets ({@link resolvePiProvider}), over the env with the overlay applied. */
|
|
394
407
|
resolveProvider(model) {
|
|
395
|
-
return resolvePiProvider(
|
|
408
|
+
return resolvePiProvider(overlaidProcessEnv(this.envOverlay, PI_CREDENTIAL_VARS), model, this.provider);
|
|
396
409
|
}
|
|
397
410
|
/**
|
|
398
411
|
* Env for the pi process: scoped like Claude Code's, with ONE provider's
|
|
399
412
|
* credentials instead of Anthropic's (`providerVars`, from
|
|
400
413
|
* {@link resolvePiProvider}). Every other provider key is dropped, scoped or
|
|
401
|
-
* not, since pi's bash tool inherits this env.
|
|
402
|
-
*
|
|
414
|
+
* not, since pi's bash tool inherits this env. The caller's `envRemove` /
|
|
415
|
+
* `env` are applied after scoping ({@link applyEnvOverlay}), so the same rules
|
|
416
|
+
* hold for overlaid names. Skill credentials are dropped too, overlaid ones
|
|
417
|
+
* included ({@link finishAgentEnv}), unless the node granted them with `agent_env`.
|
|
403
418
|
*/
|
|
404
419
|
buildEnv(access, providerVars) {
|
|
405
420
|
const full = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
|
|
@@ -416,6 +431,7 @@ export class PiHarness {
|
|
|
416
431
|
reportWithheldEnv(scoped.withheld, this.logger);
|
|
417
432
|
env = scoped.env;
|
|
418
433
|
}
|
|
434
|
+
env = applyEnvOverlay(env, this.envOverlay, PI_CREDENTIAL_VARS);
|
|
419
435
|
// Other providers' keys never reach pi, even when scoping is off, passed through or granted.
|
|
420
436
|
const otherProviders = PI_AUTH_VARS.filter((v) => !providerVars.includes(v));
|
|
421
437
|
env = Object.fromEntries(Object.entries(env).filter(([k]) => !otherProviders.includes(k)));
|
|
@@ -860,7 +876,7 @@ export class PiHarness {
|
|
|
860
876
|
const prep = await prepareAgentSpawn({
|
|
861
877
|
caps: this.capabilities,
|
|
862
878
|
policy,
|
|
863
|
-
spawn: { command: this.
|
|
879
|
+
spawn: { command: this.cliPath(), args, env, cwd: this.cwd },
|
|
864
880
|
wrapper: wrapper ?? null,
|
|
865
881
|
harnessEgress,
|
|
866
882
|
env: process.env,
|
|
@@ -979,7 +995,7 @@ export class PiHarness {
|
|
|
979
995
|
const env = { ...this.buildEnv(undefined, provider.vars), PI_CODING_AGENT_DIR: agentDir };
|
|
980
996
|
const out = await this.exec({
|
|
981
997
|
spawn: {
|
|
982
|
-
command: this.
|
|
998
|
+
command: this.cliPath(),
|
|
983
999
|
args: [...this.baseArgs(model, false, provider.explicit ? provider.provider : undefined), "--no-tools"],
|
|
984
1000
|
env,
|
|
985
1001
|
cwd: this.cwd,
|
|
@@ -990,6 +1006,8 @@ export class PiHarness {
|
|
|
990
1006
|
// Classification calls use no tools; any tool call is over budget.
|
|
991
1007
|
maxToolCalls: 0,
|
|
992
1008
|
});
|
|
1009
|
+
// A completion that failed was still billed: report before the outcome.
|
|
1010
|
+
reportCompletionUsage(req.onUsage, out.usage, this.logger);
|
|
993
1011
|
if (out.spawnError)
|
|
994
1012
|
return failClosed(`pi could not start: ${out.spawnError}`);
|
|
995
1013
|
if (out.abortReason === "timeout")
|