@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
package/dist/agent-env.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export declare const PI_ENV_PREFIXES: readonly string[];
|
|
|
77
77
|
* name is in {@link PI_AUTH_VARS} (`meta` to `META_API_KEY`).
|
|
78
78
|
*/
|
|
79
79
|
export declare const PI_PROVIDER_VARS: Readonly<Record<string, readonly string[]>>;
|
|
80
|
+
/** pi settings that are not secrets: their presence alone never selects a provider. */
|
|
81
|
+
export declare const PI_NON_SECRET_VARS: ReadonlySet<string>;
|
|
80
82
|
/** The {@link PI_AUTH_VARS} names for one pi provider id (empty for a custom `models.json` provider). */
|
|
81
83
|
export declare function piProviderVars(provider: string): string[];
|
|
82
84
|
export interface PiProviderResolution {
|
|
@@ -115,6 +117,12 @@ export interface BuildAgentEnvOpts {
|
|
|
115
117
|
prefixes?: readonly string[];
|
|
116
118
|
/** Operator passthrough list (`env-passthrough`). `"*"` inherits everything. */
|
|
117
119
|
passthrough?: readonly string[];
|
|
120
|
+
/**
|
|
121
|
+
* Where `CLAUDE_CODE_USE_BEDROCK` / `CLAUDE_CODE_USE_VERTEX` are read when
|
|
122
|
+
* deciding the cloud vars. Default: `source`. A harness `env` overlay passes
|
|
123
|
+
* the overlaid env, so an overlay that turns Bedrock on keeps the `AWS_*` vars.
|
|
124
|
+
*/
|
|
125
|
+
routing?: Record<string, string | undefined>;
|
|
118
126
|
logger?: Pick<Logger, "warn">;
|
|
119
127
|
}
|
|
120
128
|
/**
|
|
@@ -221,7 +229,8 @@ export declare function noPushGitConfig(dir: string): Array<[string, string]>;
|
|
|
221
229
|
* scoping is on. Withholds {@link PUSH_TOKEN_VARS} (the ssh agent socket and
|
|
222
230
|
* askpass programs among them) and every {@link isAgentCredential} name, even
|
|
223
231
|
* one a node granted with `agent_env`, appends
|
|
224
|
-
* {@link noPushGitConfig} after any `GIT_CONFIG_*` entries already present
|
|
232
|
+
* {@link noPushGitConfig} after any `GIT_CONFIG_*` entries already present
|
|
233
|
+
* (and drops `GIT_CONFIG_PARAMETERS`, which git would apply after them),
|
|
225
234
|
* sets `GIT_ASKPASS` to a refusing program, turns off git's terminal prompt,
|
|
226
235
|
* routes git's ssh through a wrapper that refuses `git-receive-pack` (so an
|
|
227
236
|
* ssh push fails even to a remote with an explicit `pushurl` under
|
|
@@ -306,6 +315,72 @@ export declare function grantedAgentEnv(agentEnv: readonly string[] | undefined,
|
|
|
306
315
|
grant: string[];
|
|
307
316
|
dropped?: string;
|
|
308
317
|
};
|
|
318
|
+
/**
|
|
319
|
+
* The `env` and `envRemove` options every harness takes. They hold per
|
|
320
|
+
* harness instance: every node and every judge and route call (`complete()`)
|
|
321
|
+
* on that instance gets them, so per-run isolation means one harness per
|
|
322
|
+
* `execute()`.
|
|
323
|
+
*/
|
|
324
|
+
export interface HarnessEnvOptions {
|
|
325
|
+
/**
|
|
326
|
+
* Values laid over the agent env after scoping and passthrough, then put
|
|
327
|
+
* through the same credential rule as the inherited env: a skill credential
|
|
328
|
+
* (`GITHUB_TOKEN`, `SLACK_*`, a custom skill's config name) is withheld
|
|
329
|
+
* unless the node grants it with `agent_env`, and the harness's own auth
|
|
330
|
+
* vars always pass. When it names one of the harness's own credentials, the
|
|
331
|
+
* inherited ones are dropped first, so the overlay's login replaces the
|
|
332
|
+
* parent's instead of sitting beside it. In a staged or dry run the push
|
|
333
|
+
* block lands on top: it sets `GIT_ASKPASS`, `GH_CONFIG_DIR`, the ssh
|
|
334
|
+
* command and its git config entries over any overlaid value, drops
|
|
335
|
+
* `GIT_CONFIG_PARAMETERS` and every push token. `SWENY_*` names here only
|
|
336
|
+
* reach the agent; sweny itself reads its settings from `process.env`.
|
|
337
|
+
* Use it to hand a run its own `HOME` or `CLAUDE_CONFIG_DIR` without
|
|
338
|
+
* touching `process.env`, which is never modified. `HOME`,
|
|
339
|
+
* `CLAUDE_CONFIG_DIR` and `CODEX_HOME` must be absolute paths: the sandbox
|
|
340
|
+
* denies the login files under them.
|
|
341
|
+
*/
|
|
342
|
+
env?: Readonly<Record<string, string>>;
|
|
343
|
+
/**
|
|
344
|
+
* Names removed from the agent env after scoping and passthrough, before
|
|
345
|
+
* `env` is applied (e.g. a parent `ANTHROPIC_API_KEY` when the run should
|
|
346
|
+
* use a login under `CLAUDE_CONFIG_DIR`).
|
|
347
|
+
*/
|
|
348
|
+
envRemove?: readonly string[];
|
|
349
|
+
}
|
|
350
|
+
/** A validated, copied {@link HarnessEnvOptions}. */
|
|
351
|
+
export interface AgentEnvOverlay {
|
|
352
|
+
readonly env: Readonly<Record<string, string>>;
|
|
353
|
+
readonly remove: readonly string[];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The executable a spawn of `command` with this env and cwd would run: a path
|
|
357
|
+
* with a separator resolves against `cwd`, a bare name is looked up on
|
|
358
|
+
* `env.PATH`. Undefined when nothing executable is found. Used so a version
|
|
359
|
+
* check probes the same file the spawn later runs (a harness `env` overlay may
|
|
360
|
+
* set its own `PATH`).
|
|
361
|
+
*/
|
|
362
|
+
export declare function resolveExecutable(command: string, env: Record<string, string | undefined>, cwd?: string): string | undefined;
|
|
363
|
+
/**
|
|
364
|
+
* Validate and copy a harness's `env` / `envRemove` options. Throws on a
|
|
365
|
+
* non-string value or an invalid name, so a bad overlay fails at construction
|
|
366
|
+
* instead of at spawn. Returns undefined when neither option is set. The copy
|
|
367
|
+
* means a later change to the caller's objects has no effect.
|
|
368
|
+
*/
|
|
369
|
+
export declare function agentEnvOverlay(opts: HarnessEnvOptions): AgentEnvOverlay | undefined;
|
|
370
|
+
/**
|
|
371
|
+
* Apply a caller overlay to a scoped agent env, before {@link finishAgentEnv}
|
|
372
|
+
* (which then withholds any overlaid skill credential the node did not grant).
|
|
373
|
+
* Drops the `remove` names, then, when the overlay names any of `credentials`
|
|
374
|
+
* (the harness's own login vars), drops the inherited ones, then lays the
|
|
375
|
+
* overlay values on top. Pure: returns a copy.
|
|
376
|
+
*/
|
|
377
|
+
export declare function applyEnvOverlay(env: Record<string, string>, overlay: AgentEnvOverlay | undefined, credentials: readonly string[]): Record<string, string>;
|
|
378
|
+
/**
|
|
379
|
+
* `process.env` as the agent will see it for auth and command lookups (a
|
|
380
|
+
* harness's `preflight()` probe): the overlay applied by {@link applyEnvOverlay},
|
|
381
|
+
* except `SWENY_*` names, which stay as sweny reads them from `process.env`.
|
|
382
|
+
*/
|
|
383
|
+
export declare function overlaidProcessEnv(overlay: AgentEnvOverlay | undefined, credentials: readonly string[]): Record<string, string>;
|
|
309
384
|
/** What one node's agent call may see: env var names and network hosts. */
|
|
310
385
|
export interface AgentAccess {
|
|
311
386
|
/**
|
|
@@ -428,14 +503,31 @@ export declare function checkSandboxSupport(platform?: NodeJS.Platform, pathEnv?
|
|
|
428
503
|
* - `network.allowedDomains` + `strictAllowlist`: non-listed hosts are denied
|
|
429
504
|
* deterministically rather than prompted (no prompt exists headless).
|
|
430
505
|
* - `credentials`: the agent's own Anthropic credentials are unset for
|
|
431
|
-
* sandboxed commands, and the Claude Code credentials file is unreadable
|
|
506
|
+
* sandboxed commands, and the Claude Code credentials file is unreadable:
|
|
507
|
+
* the host's, and the one the agent env selects ({@link claudeCredentialFiles},
|
|
508
|
+
* `agentEnv` carrying a harness `env` overlay's `HOME` / `CLAUDE_CONFIG_DIR`).
|
|
432
509
|
*/
|
|
433
510
|
export declare function buildSandboxSettings(domains: readonly string[], opts?: {
|
|
434
511
|
failIfUnavailable?: boolean;
|
|
435
512
|
home?: string;
|
|
513
|
+
agentEnv?: Record<string, string | undefined>;
|
|
514
|
+
/** The agent's working directory, for a relative `HOME` / `CLAUDE_CONFIG_DIR`. */
|
|
515
|
+
cwd?: string;
|
|
436
516
|
}): SandboxSettings;
|
|
517
|
+
/**
|
|
518
|
+
* Claude Code login files to deny to sandboxed commands: the host's
|
|
519
|
+
* (`<hostHome>/.claude/.credentials.json`), plus the one `env` selects when it
|
|
520
|
+
* points elsewhere: `$CLAUDE_CONFIG_DIR/.credentials.json`, and
|
|
521
|
+
* `$HOME/.claude/.credentials.json`. A relative value resolves against the
|
|
522
|
+
* agent's `cwd`, where the agent would resolve it. Deduplicated, host file first.
|
|
523
|
+
*/
|
|
524
|
+
export declare function claudeCredentialFiles(env: Record<string, string | undefined>, hostHome: string, cwd?: string): string[];
|
|
437
525
|
export interface ResolveAgentSandboxOpts {
|
|
438
526
|
env: Record<string, string | undefined>;
|
|
527
|
+
/** The agent's env when it differs from `env` (a harness `env` overlay): its login files are denied too. */
|
|
528
|
+
agentEnv?: Record<string, string | undefined>;
|
|
529
|
+
/** The agent's working directory (resolves a relative path in `agentEnv`). */
|
|
530
|
+
cwd?: string;
|
|
439
531
|
/** Explicit mode (client option); falls back to `env.SWENY_SANDBOX`. */
|
|
440
532
|
mode?: SandboxMode;
|
|
441
533
|
/** Explicit extra hosts (client option); falls back to `env.SWENY_SANDBOX_ALLOWED_DOMAINS`. */
|
package/dist/agent-env.js
CHANGED
|
@@ -262,7 +262,7 @@ export const PI_PROVIDER_VARS = {
|
|
|
262
262
|
"xiaomi-token-plan-sgp": ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
|
|
263
263
|
};
|
|
264
264
|
/** pi settings that are not secrets: their presence alone never selects a provider. */
|
|
265
|
-
const PI_NON_SECRET_VARS = new Set([
|
|
265
|
+
export const PI_NON_SECRET_VARS = new Set([
|
|
266
266
|
"AZURE_OPENAI_BASE_URL",
|
|
267
267
|
"AZURE_OPENAI_RESOURCE_NAME",
|
|
268
268
|
"CLOUDFLARE_ACCOUNT_ID",
|
|
@@ -338,7 +338,8 @@ export function resolvePiProvider(env, model, explicit) {
|
|
|
338
338
|
}
|
|
339
339
|
/** A stored Codex login (`codex login`): `auth.json` under `CODEX_HOME`, default `~/.codex`. */
|
|
340
340
|
export function hasCodexLogin(env = process.env, exists = existsSync) {
|
|
341
|
-
|
|
341
|
+
// The env's HOME, not the process's: a harness `env` overlay may give the run its own.
|
|
342
|
+
const home = env.CODEX_HOME || path.join(env.HOME || homedir(), ".codex");
|
|
342
343
|
return exists(path.join(home, "auth.json"));
|
|
343
344
|
}
|
|
344
345
|
/**
|
|
@@ -535,9 +536,10 @@ export function buildAgentEnv(source, opts = {}) {
|
|
|
535
536
|
const prefixes = [...(opts.prefixes ?? AGENT_ENV_PREFIXES)];
|
|
536
537
|
// Bedrock / Vertex routing is a Claude Code setting; only its default prefixes carry it.
|
|
537
538
|
if (!opts.prefixes) {
|
|
538
|
-
|
|
539
|
+
const routing = opts.routing ?? source;
|
|
540
|
+
if (truthy(routing.CLAUDE_CODE_USE_BEDROCK))
|
|
539
541
|
prefixes.push(...BEDROCK_PREFIXES);
|
|
540
|
-
if (truthy(
|
|
542
|
+
if (truthy(routing.CLAUDE_CODE_USE_VERTEX))
|
|
541
543
|
for (const v of VERTEX_VARS)
|
|
542
544
|
exact.add(v);
|
|
543
545
|
}
|
|
@@ -636,7 +638,8 @@ export function noPushGitConfig(dir) {
|
|
|
636
638
|
* scoping is on. Withholds {@link PUSH_TOKEN_VARS} (the ssh agent socket and
|
|
637
639
|
* askpass programs among them) and every {@link isAgentCredential} name, even
|
|
638
640
|
* one a node granted with `agent_env`, appends
|
|
639
|
-
* {@link noPushGitConfig} after any `GIT_CONFIG_*` entries already present
|
|
641
|
+
* {@link noPushGitConfig} after any `GIT_CONFIG_*` entries already present
|
|
642
|
+
* (and drops `GIT_CONFIG_PARAMETERS`, which git would apply after them),
|
|
640
643
|
* sets `GIT_ASKPASS` to a refusing program, turns off git's terminal prompt,
|
|
641
644
|
* routes git's ssh through a wrapper that refuses `git-receive-pack` (so an
|
|
642
645
|
* ssh push fails even to a remote with an explicit `pushurl` under
|
|
@@ -662,6 +665,10 @@ export function withPushBlocked(env, enabled) {
|
|
|
662
665
|
// Every skill credential too, even one a node granted with `agent_env`.
|
|
663
666
|
if (PUSH_TOKEN_VARS.includes(k) || isAgentCredential(k))
|
|
664
667
|
continue;
|
|
668
|
+
// git reads GIT_CONFIG_PARAMETERS after the GIT_CONFIG_COUNT entries, so a value there would
|
|
669
|
+
// override the block (push.default, core.hooksPath, credential.helper).
|
|
670
|
+
if (k === "GIT_CONFIG_PARAMETERS")
|
|
671
|
+
continue;
|
|
665
672
|
out[k] = v;
|
|
666
673
|
}
|
|
667
674
|
const base = Number.parseInt(out.GIT_CONFIG_COUNT ?? "0", 10);
|
|
@@ -814,6 +821,116 @@ export function grantedAgentEnv(agentEnv, opts) {
|
|
|
814
821
|
return { grant: [], dropped: `agent_env withheld in a staged or dry run: ${names.join(", ")}` };
|
|
815
822
|
return { grant: names };
|
|
816
823
|
}
|
|
824
|
+
/** POSIX leaves env names loosely defined; the rules here: not empty, no `=`, no NUL, no whitespace. */
|
|
825
|
+
function checkEnvName(option, name) {
|
|
826
|
+
if (typeof name !== "string" || name === "" || /[=\0\s]/.test(name)) {
|
|
827
|
+
throw new Error(`${option}: invalid env var name ${JSON.stringify(name)} (must be a non-empty string without "=", NUL or whitespace)`);
|
|
828
|
+
}
|
|
829
|
+
return name;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* The executable a spawn of `command` with this env and cwd would run: a path
|
|
833
|
+
* with a separator resolves against `cwd`, a bare name is looked up on
|
|
834
|
+
* `env.PATH`. Undefined when nothing executable is found. Used so a version
|
|
835
|
+
* check probes the same file the spawn later runs (a harness `env` overlay may
|
|
836
|
+
* set its own `PATH`).
|
|
837
|
+
*/
|
|
838
|
+
export function resolveExecutable(command, env, cwd = process.cwd()) {
|
|
839
|
+
const ok = (p) => {
|
|
840
|
+
try {
|
|
841
|
+
accessSync(p, fsConstants.X_OK);
|
|
842
|
+
return statSync(p).isFile();
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
if (command.includes(path.sep) || command.includes("/")) {
|
|
849
|
+
const p = path.resolve(cwd, command);
|
|
850
|
+
return ok(p) ? p : undefined;
|
|
851
|
+
}
|
|
852
|
+
for (const dir of (env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
853
|
+
const p = path.resolve(cwd, dir, command);
|
|
854
|
+
if (ok(p))
|
|
855
|
+
return p;
|
|
856
|
+
}
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
/** Overlay names whose value is a directory the sandbox derives credential-file denies from. */
|
|
860
|
+
const OVERLAY_PATH_VARS = ["HOME", "CLAUDE_CONFIG_DIR", "CODEX_HOME"];
|
|
861
|
+
function isPlainObject(v) {
|
|
862
|
+
if (v === null || typeof v !== "object")
|
|
863
|
+
return false;
|
|
864
|
+
const proto = Object.getPrototypeOf(v);
|
|
865
|
+
return proto === Object.prototype || proto === null;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Validate and copy a harness's `env` / `envRemove` options. Throws on a
|
|
869
|
+
* non-string value or an invalid name, so a bad overlay fails at construction
|
|
870
|
+
* instead of at spawn. Returns undefined when neither option is set. The copy
|
|
871
|
+
* means a later change to the caller's objects has no effect.
|
|
872
|
+
*/
|
|
873
|
+
export function agentEnvOverlay(opts) {
|
|
874
|
+
if (opts.env === undefined && opts.envRemove === undefined)
|
|
875
|
+
return undefined;
|
|
876
|
+
const env = {};
|
|
877
|
+
if (opts.env !== undefined) {
|
|
878
|
+
if (!isPlainObject(opts.env)) {
|
|
879
|
+
throw new Error("env: must be a plain object of env var names to string values (not a Map or class instance)");
|
|
880
|
+
}
|
|
881
|
+
for (const [k, v] of Object.entries(opts.env)) {
|
|
882
|
+
checkEnvName("env", k);
|
|
883
|
+
if (typeof v !== "string") {
|
|
884
|
+
throw new Error(`env: value for ${k} must be a string, got ${typeof v}` +
|
|
885
|
+
(v === undefined ? " (leave the key out, or name it in envRemove to unset it)" : ""));
|
|
886
|
+
}
|
|
887
|
+
if (v.includes("\0"))
|
|
888
|
+
throw new Error(`env: value for ${k} contains NUL`);
|
|
889
|
+
if (OVERLAY_PATH_VARS.includes(k) && !path.isAbsolute(v)) {
|
|
890
|
+
throw new Error(`env: ${k} must be an absolute path, got ${JSON.stringify(v)}`);
|
|
891
|
+
}
|
|
892
|
+
env[k] = v;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (opts.envRemove !== undefined && !Array.isArray(opts.envRemove)) {
|
|
896
|
+
throw new Error("envRemove: must be an array of env var names");
|
|
897
|
+
}
|
|
898
|
+
const remove = (opts.envRemove ?? []).map((n) => checkEnvName("envRemove", n));
|
|
899
|
+
return Object.freeze({ env: Object.freeze(env), remove: Object.freeze(remove) });
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Apply a caller overlay to a scoped agent env, before {@link finishAgentEnv}
|
|
903
|
+
* (which then withholds any overlaid skill credential the node did not grant).
|
|
904
|
+
* Drops the `remove` names, then, when the overlay names any of `credentials`
|
|
905
|
+
* (the harness's own login vars), drops the inherited ones, then lays the
|
|
906
|
+
* overlay values on top. Pure: returns a copy.
|
|
907
|
+
*/
|
|
908
|
+
export function applyEnvOverlay(env, overlay, credentials) {
|
|
909
|
+
if (!overlay)
|
|
910
|
+
return env;
|
|
911
|
+
const out = { ...env };
|
|
912
|
+
for (const name of overlay.remove)
|
|
913
|
+
delete out[name];
|
|
914
|
+
if (credentials.some((c) => Object.hasOwn(overlay.env, c)))
|
|
915
|
+
for (const c of credentials)
|
|
916
|
+
delete out[c];
|
|
917
|
+
return Object.assign(out, overlay.env);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* `process.env` as the agent will see it for auth and command lookups (a
|
|
921
|
+
* harness's `preflight()` probe): the overlay applied by {@link applyEnvOverlay},
|
|
922
|
+
* except `SWENY_*` names, which stay as sweny reads them from `process.env`.
|
|
923
|
+
*/
|
|
924
|
+
export function overlaidProcessEnv(overlay, credentials) {
|
|
925
|
+
const full = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
|
|
926
|
+
if (!overlay)
|
|
927
|
+
return full;
|
|
928
|
+
const own = (n) => !n.startsWith("SWENY_");
|
|
929
|
+
return applyEnvOverlay(full, {
|
|
930
|
+
env: Object.fromEntries(Object.entries(overlay.env).filter(([k]) => own(k))),
|
|
931
|
+
remove: overlay.remove.filter(own),
|
|
932
|
+
}, credentials);
|
|
933
|
+
}
|
|
817
934
|
/** True when a run request may load no MCP server but sweny's own bridge (read-only or staged). */
|
|
818
935
|
export function mcpWithheld(readOnly, access) {
|
|
819
936
|
return readOnly || access?.noMcp === true;
|
|
@@ -1031,9 +1148,12 @@ function defaultProbe() {
|
|
|
1031
1148
|
* - `network.allowedDomains` + `strictAllowlist`: non-listed hosts are denied
|
|
1032
1149
|
* deterministically rather than prompted (no prompt exists headless).
|
|
1033
1150
|
* - `credentials`: the agent's own Anthropic credentials are unset for
|
|
1034
|
-
* sandboxed commands, and the Claude Code credentials file is unreadable
|
|
1151
|
+
* sandboxed commands, and the Claude Code credentials file is unreadable:
|
|
1152
|
+
* the host's, and the one the agent env selects ({@link claudeCredentialFiles},
|
|
1153
|
+
* `agentEnv` carrying a harness `env` overlay's `HOME` / `CLAUDE_CONFIG_DIR`).
|
|
1035
1154
|
*/
|
|
1036
1155
|
export function buildSandboxSettings(domains, opts = {}) {
|
|
1156
|
+
const loginFiles = claudeCredentialFiles(opts.agentEnv ?? {}, opts.home ?? homedir(), opts.cwd);
|
|
1037
1157
|
return {
|
|
1038
1158
|
enabled: true,
|
|
1039
1159
|
failIfUnavailable: opts.failIfUnavailable ?? true,
|
|
@@ -1046,7 +1166,7 @@ export function buildSandboxSettings(domains, opts = {}) {
|
|
|
1046
1166
|
credentials: {
|
|
1047
1167
|
envVars: AGENT_AUTH_VARS.map((name) => ({ name, mode: "deny" })),
|
|
1048
1168
|
files: [
|
|
1049
|
-
|
|
1169
|
+
...loginFiles.map((p) => ({ path: p, mode: "deny" })),
|
|
1050
1170
|
// Run journals and their keys: an agent that could read a key could forge records.
|
|
1051
1171
|
{ path: runStateRoot(), mode: "deny" },
|
|
1052
1172
|
],
|
|
@@ -1055,6 +1175,21 @@ export function buildSandboxSettings(domains, opts = {}) {
|
|
|
1055
1175
|
filesystem: { denyRead: [runStateRoot()], denyWrite: [runStateRoot()] },
|
|
1056
1176
|
};
|
|
1057
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Claude Code login files to deny to sandboxed commands: the host's
|
|
1180
|
+
* (`<hostHome>/.claude/.credentials.json`), plus the one `env` selects when it
|
|
1181
|
+
* points elsewhere: `$CLAUDE_CONFIG_DIR/.credentials.json`, and
|
|
1182
|
+
* `$HOME/.claude/.credentials.json`. A relative value resolves against the
|
|
1183
|
+
* agent's `cwd`, where the agent would resolve it. Deduplicated, host file first.
|
|
1184
|
+
*/
|
|
1185
|
+
export function claudeCredentialFiles(env, hostHome, cwd = process.cwd()) {
|
|
1186
|
+
const files = [path.join(hostHome, ".claude", ".credentials.json")];
|
|
1187
|
+
if (env.CLAUDE_CONFIG_DIR)
|
|
1188
|
+
files.push(path.resolve(cwd, env.CLAUDE_CONFIG_DIR, ".credentials.json"));
|
|
1189
|
+
if (env.HOME)
|
|
1190
|
+
files.push(path.resolve(cwd, env.HOME, ".claude", ".credentials.json"));
|
|
1191
|
+
return [...new Set(files)];
|
|
1192
|
+
}
|
|
1058
1193
|
/**
|
|
1059
1194
|
* Resolve the `sandbox` query option for one agent call.
|
|
1060
1195
|
*
|
|
@@ -1091,6 +1226,8 @@ export function resolveAgentSandbox(opts) {
|
|
|
1091
1226
|
settings: buildSandboxSettings([...DEFAULT_SANDBOX_DOMAINS, ...(opts.nodeDomains ?? []), ...extra], {
|
|
1092
1227
|
// Preflight passed: from here the sandbox is enforced or the node fails.
|
|
1093
1228
|
failIfUnavailable: true,
|
|
1229
|
+
agentEnv: opts.agentEnv,
|
|
1230
|
+
cwd: opts.cwd,
|
|
1094
1231
|
}),
|
|
1095
1232
|
};
|
|
1096
1233
|
}
|
package/dist/budget.d.ts
CHANGED
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Tokens are input + output tokens as the harness reports them (cache reads and
|
|
10
10
|
* writes are not counted, matching the run receipt). Cost is the harness's own
|
|
11
|
-
* reported USD, never estimated.
|
|
12
|
-
*
|
|
11
|
+
* reported USD, never estimated. Every model completion the executor itself
|
|
12
|
+
* makes is counted too, under its own `kind` (see {@link SpendKind}): judge
|
|
13
|
+
* scoring, route evaluation, retry reflection and the safe-output screen. A
|
|
14
|
+
* completion counts when its harness reports usage for it; one that reports
|
|
15
|
+
* none adds nothing (the same as a node run without usage). The run totals
|
|
16
|
+
* and the stop are over all kinds together, across a resume too; the split is
|
|
17
|
+
* `trace.spend`. Once a ceiling is reached no further executor completion
|
|
18
|
+
* starts. The decision model (`route_by: decider`) is not metered.
|
|
13
19
|
*
|
|
14
20
|
* Enforcement, per unit the harness can report:
|
|
15
21
|
* - live: the harness calls `onUsage` during the node; crossing a limit aborts
|
|
@@ -17,7 +23,9 @@
|
|
|
17
23
|
* - between nodes: usage on the finished result is checked; a crossing fails the
|
|
18
24
|
* node, and the run halts before the next node starts.
|
|
19
25
|
* - across a resume: the run journal records each attempt's spend (live and
|
|
20
|
-
* final), and
|
|
26
|
+
* final), and each metered completion (judge, route, reflection, screen)
|
|
27
|
+
* as its own record; `sweny workflow resume` seeds the run total with all
|
|
28
|
+
* of it, split by kind.
|
|
21
29
|
*/
|
|
22
30
|
import type { NodeUsage } from "./types.js";
|
|
23
31
|
/** The `budget` field of a node or workflow. */
|
|
@@ -36,6 +44,20 @@ export interface Spend {
|
|
|
36
44
|
tokens: number;
|
|
37
45
|
costUsd: number;
|
|
38
46
|
}
|
|
47
|
+
/** Spend restored on a resume: the total, and its split by kind when the journal has one. */
|
|
48
|
+
export interface SeedSpend extends Spend {
|
|
49
|
+
byKind?: Partial<Record<SpendKind, Spend>>;
|
|
50
|
+
}
|
|
51
|
+
/** What a unit of spend paid for. `node` is an agent node run; the rest are the executor's own completions. */
|
|
52
|
+
export type SpendKind = "node" | "judge" | "route" | "reflect" | "screen";
|
|
53
|
+
/** The order kinds are listed in, wherever the split is shown. */
|
|
54
|
+
export declare const SPEND_KINDS: readonly SpendKind[];
|
|
55
|
+
/** Spend of one {@link SpendKind}. `calls` counts the completions that reported usage. */
|
|
56
|
+
export interface KindSpend extends Spend {
|
|
57
|
+
calls: number;
|
|
58
|
+
}
|
|
59
|
+
/** Spend by kind. A kind with no reported usage is absent. */
|
|
60
|
+
export type SpendBreakdown = Partial<Record<SpendKind, KindSpend>>;
|
|
39
61
|
export type BudgetUnit = "tokens" | "cost_usd";
|
|
40
62
|
/** Recorded on the node whose spend crossed a budget (`NodeResult.budget`). */
|
|
41
63
|
export interface BudgetOverrun {
|
|
@@ -45,6 +67,18 @@ export interface BudgetOverrun {
|
|
|
45
67
|
limit: number;
|
|
46
68
|
/** Spend at the moment of the stop (live) or at the end of the node. */
|
|
47
69
|
spent: number;
|
|
70
|
+
/** The executor completion whose spend crossed it (judge, route, ...). Absent for a node run. */
|
|
71
|
+
kind?: SpendKind;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Thrown into an executor completion (judge, route, reflection, screen) the
|
|
75
|
+
* budget refuses: a ceiling was already reached, so the call is not made.
|
|
76
|
+
* Callers that swallow a failed completion (a judge's parse retry) must let
|
|
77
|
+
* this one through, so a refusal is never mistaken for a model answer.
|
|
78
|
+
*/
|
|
79
|
+
export declare class BudgetRefusal extends Error {
|
|
80
|
+
readonly breach: BudgetOverrun;
|
|
81
|
+
constructor(breach: BudgetOverrun);
|
|
48
82
|
}
|
|
49
83
|
export declare function toLimits(b: Budget | undefined): SpendLimits;
|
|
50
84
|
/** The tighter of two limit sets, per unit. */
|
|
@@ -64,21 +98,43 @@ export declare function describeOverrun(o: BudgetOverrun, nodeId?: string): stri
|
|
|
64
98
|
export declare class BudgetGuard {
|
|
65
99
|
readonly runLimits: SpendLimits;
|
|
66
100
|
private spent;
|
|
101
|
+
private kinds;
|
|
67
102
|
constructor(runLimits: SpendLimits);
|
|
68
103
|
get totals(): Spend;
|
|
104
|
+
/**
|
|
105
|
+
* Spend by kind, in {@link SPEND_KINDS} order. Resumed spend is restored
|
|
106
|
+
* under its kind; a journal without meter records seeds `node` only.
|
|
107
|
+
*/
|
|
108
|
+
breakdown(): SpendBreakdown;
|
|
109
|
+
/** True once any completion other than a node run reported usage. */
|
|
110
|
+
get hasMeteredCalls(): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Count one executor completion (judge, route, ...) against the run. Usage
|
|
113
|
+
* with no tokens and no cost counts nothing. Returns the breach this report
|
|
114
|
+
* caused, judged against the totals before it, like a live node report.
|
|
115
|
+
*/
|
|
116
|
+
meter(kind: SpendKind, usage: NodeUsage | undefined, nodeBudget?: NodeBudget): BudgetOverrun | undefined;
|
|
69
117
|
/** True when any limit exists at all, so the executor can skip the machinery. */
|
|
70
118
|
active(nodeLimits: SpendLimits): boolean;
|
|
71
119
|
/** The run ceiling is already reached: no further attempt may start. */
|
|
72
120
|
runExhausted(): BudgetOverrun | undefined;
|
|
73
121
|
node(nodeLimits: SpendLimits): NodeBudget;
|
|
74
122
|
/**
|
|
75
|
-
* Spend the run made before this process (a resumed run's journaled usage
|
|
76
|
-
* Counted against the run ceiling like any other
|
|
77
|
-
* resets the whole-run budget.
|
|
123
|
+
* Spend the run made before this process (a resumed run's journaled usage
|
|
124
|
+
* and metered completions). Counted against the run ceiling like any other
|
|
125
|
+
* spend, so a crash never resets the whole-run budget. Restored under its
|
|
126
|
+
* kind when the journal has the split (a journal written before meter
|
|
127
|
+
* records existed has node spend only). Seeded spend is not a call: `calls`
|
|
128
|
+
* is unchanged.
|
|
78
129
|
*/
|
|
79
|
-
seed(s:
|
|
80
|
-
/**
|
|
81
|
-
|
|
130
|
+
seed(s: SeedSpend): void;
|
|
131
|
+
/**
|
|
132
|
+
* @internal `counted`: this commit is one completion that reported usage, so
|
|
133
|
+
* `calls` goes up. A commit that is not (seeded spend, an attempt that
|
|
134
|
+
* reported nothing) adds its spend only, and a zero one leaves no trace, so
|
|
135
|
+
* the split is the same with and without a limit.
|
|
136
|
+
*/
|
|
137
|
+
commit(s: Spend, kind: SpendKind, counted: boolean): void;
|
|
82
138
|
}
|
|
83
139
|
export declare class NodeBudget {
|
|
84
140
|
private readonly guard;
|
|
@@ -88,10 +144,18 @@ export declare class NodeBudget {
|
|
|
88
144
|
/** A limit is already reached before the next attempt (a retry after a spend-heavy attempt, or a spent run). */
|
|
89
145
|
exhausted(): BudgetOverrun | undefined;
|
|
90
146
|
attempt(outer?: AbortSignal): BudgetAttempt;
|
|
147
|
+
/**
|
|
148
|
+
* Spend this visit made before a resume (a checkpointed visit's journaled
|
|
149
|
+
* spend). It counts against the node's own limit only: the run total
|
|
150
|
+
* already holds it from the seed.
|
|
151
|
+
*/
|
|
152
|
+
restore(s: Spend): void;
|
|
91
153
|
/** @internal */
|
|
92
154
|
get committed(): Spend;
|
|
93
|
-
/**
|
|
94
|
-
|
|
155
|
+
/** Count one executor completion made during this visit (judge, retry reflection, screen). */
|
|
156
|
+
meter(kind: SpendKind, usage: NodeUsage | undefined): BudgetOverrun | undefined;
|
|
157
|
+
/** @internal See {@link BudgetGuard.commit}. */
|
|
158
|
+
commit(s: Spend, kind: SpendKind, counted: boolean): void;
|
|
95
159
|
}
|
|
96
160
|
/** One agent run. Its `signal` is the caller's signal plus a budget stop. */
|
|
97
161
|
export declare class BudgetAttempt {
|
|
@@ -101,6 +165,8 @@ export declare class BudgetAttempt {
|
|
|
101
165
|
private readonly controller;
|
|
102
166
|
private live;
|
|
103
167
|
private liveUsage;
|
|
168
|
+
/** A live report carried tokens or cost. */
|
|
169
|
+
private liveReported;
|
|
104
170
|
private stopped;
|
|
105
171
|
private settled;
|
|
106
172
|
private readonly onOuterAbort;
|