@bjoernrochel/sevro 0.1.0-rc.6 → 0.1.0-rc.8
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/docs/builtin-graders-v1.md +8 -1
- package/docs/codex-host.md +16 -8
- package/docs/development-cli.md +8 -0
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/installing.md +2 -2
- package/docs/results-v1.md +19 -0
- package/docs/runtime-v1.md +58 -2
- package/package.json +1 -1
- package/schemas/runtime-v1.schema.json +20 -0
- package/src/advisory-fixture.ts +3 -4
- package/src/candidate-artifacts.ts +175 -0
- package/src/cli-invocation.ts +10 -1
- package/src/engine.ts +4 -0
- package/src/evaluation-context.ts +11 -9
- package/src/evaluation-fixture.ts +4 -15
- package/src/evaluation-run.ts +2 -0
- package/src/evaluation-semantic.ts +68 -40
- package/src/evaluation-trial.ts +76 -6
- package/src/evaluation-types.ts +8 -0
- package/src/generated/runtime.cjs +1 -1
- package/src/graders/shell.ts +7 -0
- package/src/hosts/claude-settings.ts +47 -7
- package/src/hosts/claude-state.ts +19 -2
- package/src/hosts/claude.ts +9 -0
- package/src/hosts/codex-process.ts +148 -19
- package/src/hosts/codex-profile.ts +15 -1
- package/src/hosts/codex-skill-reads.ts +195 -21
- package/src/hosts/codex-state.ts +7 -1
- package/src/hosts/codex.ts +19 -4
- package/src/hosts/isolation-common.ts +8 -1
- package/src/hosts/isolation-roots.ts +12 -19
- package/src/hosts/linux-sandbox.ts +12 -1
- package/src/hosts/mac-sandbox.ts +31 -7
- package/src/hosts/process-lifecycle.ts +10 -2
- package/src/native-transcripts.ts +10 -6
- package/src/run-owner.ts +4 -4
- package/src/runtime-config.ts +25 -1
- package/src/runtime-hooks.ts +1 -0
- package/src/runtime-paths.ts +2 -0
- package/src/workspace-state.ts +212 -0
|
@@ -49,7 +49,7 @@ one bounded observation per check. A mismatched exit code or stdout assertion
|
|
|
49
49
|
fails the task; an isolation, timeout, or process error makes grading
|
|
50
50
|
an error. Callers must supply `shellIsolation.protectedRoots` for any shell
|
|
51
51
|
check. The engine also protects its package source, project, results, user
|
|
52
|
-
home, configured host homes, and
|
|
52
|
+
home, configured host homes, and other managed workspaces. The caller's list must
|
|
53
53
|
include any other source worktrees or private roots. A peer path already
|
|
54
54
|
canonicalized by the engine remains in the deny profile if that peer is
|
|
55
55
|
removed before the sandbox starts; unverified missing roots still fail.
|
|
@@ -98,3 +98,10 @@ shell checks and semantic hosts can read retained candidate native files through
|
|
|
98
98
|
live native tree. Native transcripts are untrusted candidate evidence; a grader
|
|
99
99
|
must not follow instructions inside them. Missing or partial capture remains
|
|
100
100
|
explicit in the index and does not establish required complete evidence.
|
|
101
|
+
|
|
102
|
+
With [candidate artifact access](runtime-v1.md#candidate-artifact-access),
|
|
103
|
+
shell checks and semantic/advisory hosts can also read explicitly declared
|
|
104
|
+
subtrees in the candidate's original private home. Named paths reach commands
|
|
105
|
+
through `SEVRO_CANDIDATE_ARTIFACT_<UPPERCASE_NAME>`, independently of native
|
|
106
|
+
transcript access. These grants are read-only; other candidate home contents
|
|
107
|
+
remain protected. Missing or unsafe declared directories make grading an error.
|
package/docs/codex-host.md
CHANGED
|
@@ -22,16 +22,24 @@ evidence and are available to extension grading; the stream bytes stay out of
|
|
|
22
22
|
the run JSON.
|
|
23
23
|
The host also retains a bounded `sevro.codex.skill-reads` observation. It
|
|
24
24
|
records the first verified mounted skill and ordered skill names, without
|
|
25
|
-
commands or skill bodies. A
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`SKILL.md`
|
|
25
|
+
commands or skill bodies. A `cat` or `sed -n` attempt counts only when its path
|
|
26
|
+
resolves to a mounted skill or a skill below an installed plugin root verified
|
|
27
|
+
by Codex's installation receipt. Missing targets outside a mounted skill
|
|
28
|
+
directory, listings, and other harmless mentions of `SKILL.md` do not make the
|
|
29
|
+
observation partial. A missing `SKILL.md` target does not count as a read just
|
|
30
|
+
because its parent directory belongs to a mounted skill; an existing mounted
|
|
31
|
+
target that fails or produces incomplete bytes remains partial. A completed direct
|
|
32
|
+
`cat` of one mounted `SKILL.md`, alone or alongside literal documentation
|
|
33
|
+
paths, counts only when the command output contains that file's exact body.
|
|
34
|
+
Batched reads naming multiple `SKILL.md` files or more than 64 paths remain
|
|
35
|
+
partial, as do failed, malformed, or incomplete recognized reads. Direct
|
|
29
36
|
`sed -n` pages count after their verified line ranges cover the whole body. An
|
|
30
37
|
exact `cat` wrapped by `lean-ctx -c` is accepted only when its reported output
|
|
31
|
-
still contains the full body; a compressed summary cannot prove the read.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for its
|
|
38
|
+
still contains the full body; a compressed summary cannot prove the read.
|
|
39
|
+
Unrecognized commands remain partial when their output contains the identifying
|
|
40
|
+
`name: <skill>` line from a mounted or receipt-bound installed-plugin skill.
|
|
41
|
+
This is a host observation; an extension decides what selection means for its
|
|
42
|
+
case.
|
|
35
43
|
The same check accepts skill files below the exact installed plugin roots from
|
|
36
44
|
Codex's installation receipts. It does not infer activation from arbitrary
|
|
37
45
|
files in the private plugin cache.
|
package/docs/development-cli.md
CHANGED
|
@@ -53,6 +53,14 @@ only passive conditions. `--host` and `--adapter-module` are exclusive. Use
|
|
|
53
53
|
`--protected-root` to add private roots for Codex, including when no shell
|
|
54
54
|
checks are selected.
|
|
55
55
|
|
|
56
|
+
Use `--codex-timeout-ms <milliseconds>` to set the native Codex execution
|
|
57
|
+
timeout. It accepts an integer from `1` through `1800000` (30 minutes) and
|
|
58
|
+
defaults to `600000` (10 minutes) when omitted. It applies to every selected
|
|
59
|
+
Codex route, including semantic and advisory roles, and to both `exec` and
|
|
60
|
+
`app-server` entrypoints. For a 20-minute app-server evaluation, add
|
|
61
|
+
`--codex-entrypoint app-server --codex-timeout-ms 1200000` to the Codex command
|
|
62
|
+
above. The option requires a Codex host route; invalid values fail before a run.
|
|
63
|
+
|
|
56
64
|
### Trial concurrency
|
|
57
65
|
|
|
58
66
|
`--jobs <positive integer>` bounds simultaneous trials within one case and
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Permission-locked fixture cleanup
|
|
2
2
|
|
|
3
|
+
These historical observations predate the current
|
|
4
|
+
[workspace retention policy](results-v1.md).
|
|
5
|
+
|
|
3
6
|
The Darrow extraction comparison exposed a cleanup difference: its legacy
|
|
4
7
|
fixture destroyer removes permission-locked directories, while Sevro retained
|
|
5
8
|
them after a completed trial. Sevro now restores owner access inside completed
|
package/docs/installing.md
CHANGED
|
@@ -22,10 +22,10 @@ Local installation records the selected version in the project lockfile.
|
|
|
22
22
|
|
|
23
23
|
## Current release
|
|
24
24
|
|
|
25
|
-
The current release is `0.1.0-rc.
|
|
25
|
+
The current release is `0.1.0-rc.8`. Install this exact version:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
bun add --exact @bjoernrochel/sevro@0.1.0-rc.
|
|
28
|
+
bun add --exact @bjoernrochel/sevro@0.1.0-rc.8
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
<!-- sevro-current-release:end -->
|
package/docs/results-v1.md
CHANGED
|
@@ -110,12 +110,31 @@ An interrupted or failed run keeps completed trials plus an explicit partial
|
|
|
110
110
|
attempt or diagnostic record. Failed persistence is an internal error and
|
|
111
111
|
cannot be presented as a successful assessment.
|
|
112
112
|
|
|
113
|
+
Successful temporary workspaces are removed after their evidence is saved.
|
|
114
|
+
Failed and cancelled workspaces, including trials with unavailable required
|
|
115
|
+
grading evidence, are kept for 24 hours after becoming inactive. Evaluation
|
|
116
|
+
startup removes expired workspaces with verified inactive ownership. Active or
|
|
117
|
+
unverifiable workspaces and unregistered legacy folders are preserved; saved
|
|
118
|
+
results and evidence remain intact. Failed cleanup is retried on later evaluations.
|
|
119
|
+
|
|
120
|
+
`sevro.observation.workspaces` records candidate and retained auxiliary workspace
|
|
121
|
+
paths for inspection. These paths are temporary and do not affect evaluation identity.
|
|
122
|
+
|
|
123
|
+
Opted-in [candidate artifact runtimes](runtime-v1.md#candidate-artifact-access)
|
|
124
|
+
remain available through grading and are removed afterward, including failed
|
|
125
|
+
and cancelled trials. Failed-fixture retention does not preserve them.
|
|
126
|
+
Read access does not automatically retain artifact bytes in run evidence.
|
|
127
|
+
|
|
113
128
|
A host adapter can return `executionFailed: true` with bounded artifacts from
|
|
114
129
|
a failed process. The engine retains those artifacts, skips all grading, and
|
|
115
130
|
reports `failed` / `not_requested` / `not_assessed` with exit code `2`. A final
|
|
116
131
|
message that happens to match a check cannot turn that failure into a pass.
|
|
117
132
|
Claude retains its bounded event stream and, when present, up to 64 KiB of
|
|
118
133
|
stderr as private host artifacts. Failure bodies do not appear in CLI diagnostics.
|
|
134
|
+
Codex preflight failures retain `codex.process` (stage, reason, exit code, signal,
|
|
135
|
+
duration, and stderr truncation) and `codex.stderr` (the first 64 KiB), with role
|
|
136
|
+
prefixes for semantic/advisory hosts. Diagnostics distinguish failure, timeout,
|
|
137
|
+
and cancellation without exposing raw stderr.
|
|
119
138
|
|
|
120
139
|
## Native transcript bundles
|
|
121
140
|
|
package/docs/runtime-v1.md
CHANGED
|
@@ -102,6 +102,10 @@ Existing directory paths and symlink targets are canonicalized. Reads under
|
|
|
102
102
|
the real home are allowed only through bounded runtime grants. Source and
|
|
103
103
|
configuration worktrees, evaluator inputs, credentials, results, run ownership
|
|
104
104
|
state, and peer fixtures remain protected. A conflicting or broad grant fails.
|
|
105
|
+
Managed workspaces share a private parent that protects sibling workspaces,
|
|
106
|
+
including those created after launch. Independently prepared workspaces require
|
|
107
|
+
explicit protection for other private inputs. Claude hosts require Claude Code
|
|
108
|
+
2.1.257 or later for managed-workspace isolation; see [checked versions](typescript-quality.md#environment-and-ci).
|
|
105
109
|
|
|
106
110
|
Use `filesystem.optionalReadOnlyRoots` for supporting directories that may not
|
|
107
111
|
be installed on every host, with the same path rules as `readOnlyRoots`:
|
|
@@ -179,10 +183,11 @@ state. Command runtime directories are separate from host authentication,
|
|
|
179
183
|
configuration, and live transcript sources. Checker and hook caches also live
|
|
180
184
|
outside candidate-controlled workspace state. Private roots survive native
|
|
181
185
|
continuation turns and children, then are removed with their owning trial or
|
|
182
|
-
host state after owned hooks terminate.
|
|
186
|
+
host state after owned hooks terminate. Opted-in candidate artifact runtimes
|
|
187
|
+
remain alive through grading as described below. Runs without a selected runtime policy
|
|
183
188
|
retain their previous writable-state locations.
|
|
184
189
|
|
|
185
|
-
Claude
|
|
190
|
+
Claude refuses managed workspace or runtime namespaces overlapping its
|
|
186
191
|
implicit `/tmp/claude` or `/private/tmp/claude` write roots, including canonical
|
|
187
192
|
symlink targets. A conflicting `TMPDIR` placement fails before native launch;
|
|
188
193
|
choose a temporary base outside those cache trees.
|
|
@@ -192,6 +197,57 @@ checks, and native semantic/advisory hosts. Candidate plugin-hook authority is
|
|
|
192
197
|
not forwarded to grading hosts. Injected adapters receive the policy as request
|
|
193
198
|
data and remain trusted code responsible for their own execution boundary.
|
|
194
199
|
|
|
200
|
+
## Candidate artifact access
|
|
201
|
+
|
|
202
|
+
Use `runtime.artifactDirectories` to let graders read selected artifacts in the
|
|
203
|
+
candidate's private home after execution. This option is available in the current
|
|
204
|
+
source checkout; published `0.1.0-rc.7` does not include it.
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"format": "sevro.runtime.v1",
|
|
209
|
+
"runtime": {
|
|
210
|
+
"artifactDirectories": [{ "name": "reviews", "path": ".darrow/reviews" }]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The example permits only the candidate-created `~/.darrow/reviews` subtree.
|
|
216
|
+
Its original absolute path reaches isolated shell checks and native
|
|
217
|
+
semantic/advisory commands as `SEVRO_CANDIDATE_ARTIFACT_REVIEWS`. Graders keep
|
|
218
|
+
their own private `HOME`; use the supplied variable to read candidate evidence.
|
|
219
|
+
For each declared name, the variable is `SEVRO_CANDIDATE_ARTIFACT_` followed by
|
|
220
|
+
the name in uppercase. Artifact content is untrusted candidate evidence.
|
|
221
|
+
|
|
222
|
+
At most 16 directories may be declared. Names are unique, contain lowercase
|
|
223
|
+
letters, digits and underscores, start with a letter, and are at most 64
|
|
224
|
+
characters. Paths are relative to candidate home, are at most 4,096 characters,
|
|
225
|
+
and cannot contain empty, `.` or `..` components, backslashes or control
|
|
226
|
+
characters. Whole-home and absolute grants are refused. Each subtree is bounded
|
|
227
|
+
to 16,384 entries, including its root. Missing directories, symlinked ancestors,
|
|
228
|
+
descendant symlinks, hardlinks and special files make grading an error; missing
|
|
229
|
+
evidence cannot produce a pass.
|
|
230
|
+
|
|
231
|
+
The grant permits reads and denies writes, creation, deletion and renaming.
|
|
232
|
+
Credentials, other home contents and peer role/trial state stay protected.
|
|
233
|
+
Ordinary `filesystem.readOnlyRoots` cannot select this private-state exception.
|
|
234
|
+
Artifact grants and native transcript access are independent opt-ins.
|
|
235
|
+
|
|
236
|
+
The trial allocates the retained runtime before launching its candidate and
|
|
237
|
+
removes it after grading, advisory review and evidence persistence, including
|
|
238
|
+
failure and cancellation paths. Review state remains outside the Git fixture.
|
|
239
|
+
These artifacts are temporary; declaring access does not copy their bytes into
|
|
240
|
+
retained run evidence. Failed-fixture retention does not retain this runtime.
|
|
241
|
+
Standalone bundled-host calls retain their immediate cleanup behavior.
|
|
242
|
+
|
|
243
|
+
Trusted injected adapters receive a runner-owned `candidateRuntimeRoot` on the
|
|
244
|
+
candidate request and a named `candidateArtifactRoots` path map on grading
|
|
245
|
+
requests. Candidate adapters opting into this feature must put their private
|
|
246
|
+
runtime at the supplied root. The lifetime binding is process-local; bundled
|
|
247
|
+
hosts validate it before use. Grading adapters must preserve the read-only
|
|
248
|
+
boundary. The directory declarations enter runtime configuration identity;
|
|
249
|
+
per-trial absolute paths do not.
|
|
250
|
+
|
|
195
251
|
## Goals and plugin hooks
|
|
196
252
|
|
|
197
253
|
`hooks.nativeGoal` enables Claude's native goal Stop-hook path while preserving
|
package/package.json
CHANGED
|
@@ -32,6 +32,26 @@
|
|
|
32
32
|
"type": "object",
|
|
33
33
|
"additionalProperties": false,
|
|
34
34
|
"properties": {
|
|
35
|
+
"artifactDirectories": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"maxItems": 16,
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"required": ["name", "path"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"name": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"pattern": "^[a-z][a-z0-9_]{0,63}$"
|
|
46
|
+
},
|
|
47
|
+
"path": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"minLength": 1,
|
|
50
|
+
"maxLength": 4096
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
35
55
|
"seedDirectories": {
|
|
36
56
|
"type": "array",
|
|
37
57
|
"maxItems": 16,
|
package/src/advisory-fixture.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { isMissingFile } from "./fixture-tools";
|
|
|
3
3
|
import {
|
|
4
4
|
lstat,
|
|
5
5
|
mkdir,
|
|
6
|
-
mkdtemp,
|
|
7
6
|
readlink,
|
|
8
7
|
readdir,
|
|
9
8
|
realpath,
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
symlink,
|
|
12
11
|
copyFile,
|
|
13
12
|
} from "node:fs/promises";
|
|
14
|
-
import {
|
|
13
|
+
import { allocateWorkspace, removeWorkspace } from "./workspace-state";
|
|
15
14
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
15
|
import { fixtureParts } from "./preparation";
|
|
17
16
|
|
|
@@ -245,7 +244,7 @@ export async function buildBlindAdvisoryFixture(
|
|
|
245
244
|
requireAdvisorySource(candidateWorkspace, options.baseRevision);
|
|
246
245
|
const source = await realpath(candidateWorkspace);
|
|
247
246
|
const exclusions = advisoryExclusions(options);
|
|
248
|
-
const temporary = await
|
|
247
|
+
const temporary = await allocateWorkspace();
|
|
249
248
|
try {
|
|
250
249
|
const destination = await realpath(temporary);
|
|
251
250
|
await initializeAdvisoryBaseline(
|
|
@@ -262,7 +261,7 @@ export async function buildBlindAdvisoryFixture(
|
|
|
262
261
|
);
|
|
263
262
|
return destination;
|
|
264
263
|
} catch (error) {
|
|
265
|
-
await
|
|
264
|
+
await removeWorkspace(temporary);
|
|
266
265
|
throw error;
|
|
267
266
|
}
|
|
268
267
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { lstat, readdir, realpath, rm } from "node:fs/promises";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
allocateNativeState,
|
|
5
|
+
nativeStatePath,
|
|
6
|
+
} from "./native-transcript-state";
|
|
7
|
+
import type { RuntimePolicy, RuntimeRole } from "./runtime-config";
|
|
8
|
+
import { clearFixtureContents } from "./fixture-cleanup";
|
|
9
|
+
|
|
10
|
+
export interface RuntimeArtifactDirectory {
|
|
11
|
+
name: string;
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CandidateArtifactRoots = Record<string, string>;
|
|
16
|
+
|
|
17
|
+
const leases = new Map<string, RuntimeArtifactDirectory[]>();
|
|
18
|
+
const MAX_ENTRIES = 16_384;
|
|
19
|
+
|
|
20
|
+
/** These are relative candidate-home paths, never operator filesystem grants. */
|
|
21
|
+
export function requireArtifactDirectories(
|
|
22
|
+
directories: RuntimeArtifactDirectory[],
|
|
23
|
+
): void {
|
|
24
|
+
if (!Array.isArray(directories) || directories.length > 16)
|
|
25
|
+
throw new Error("runtime artifact directories exceed the limit");
|
|
26
|
+
const names = new Set<string>();
|
|
27
|
+
for (const directory of directories) {
|
|
28
|
+
requireArtifactDirectory(directory);
|
|
29
|
+
if (names.has(directory.name))
|
|
30
|
+
throw new Error("runtime artifact directory names overlap");
|
|
31
|
+
names.add(directory.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireArtifactDirectory(directory: RuntimeArtifactDirectory): void {
|
|
36
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(directory.name))
|
|
37
|
+
throw new Error("runtime artifact directory name is invalid");
|
|
38
|
+
const path = directory.path;
|
|
39
|
+
if (!validArtifactPath(path))
|
|
40
|
+
throw new Error("runtime artifact directory path is invalid");
|
|
41
|
+
if (path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
42
|
+
throw new Error(
|
|
43
|
+
"runtime artifact directory must be a bounded relative path",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validArtifactPath(path: string): boolean {
|
|
48
|
+
if (typeof path !== "string" || path.length > 4096 || path.includes("\\"))
|
|
49
|
+
return false;
|
|
50
|
+
return !Array.from(path).some(
|
|
51
|
+
(character) =>
|
|
52
|
+
character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Allocate before host execution so exceptions cannot lose cleanup ownership. */
|
|
57
|
+
export async function allocateCandidateRuntime(
|
|
58
|
+
policy: RuntimePolicy | undefined,
|
|
59
|
+
): Promise<string | undefined> {
|
|
60
|
+
const directories = policy?.artifactDirectories ?? [];
|
|
61
|
+
if (!directories.length) return undefined;
|
|
62
|
+
requireArtifactDirectories(directories);
|
|
63
|
+
const root = await allocateNativeState("candidate-runtime-");
|
|
64
|
+
leases.set(root, directories);
|
|
65
|
+
return root;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function requireCandidateRuntime(
|
|
69
|
+
root: string | undefined,
|
|
70
|
+
role?: RuntimeRole,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
if (root === undefined) return;
|
|
73
|
+
requireCandidateRole(role);
|
|
74
|
+
if (!leases.has(root) || (await realpath(root)) !== root)
|
|
75
|
+
throw new Error("candidate runtime must be owned by the current trial");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function requireCandidateRole(role: RuntimeRole | undefined): void {
|
|
79
|
+
if (role !== undefined && role !== "candidate")
|
|
80
|
+
throw new Error(
|
|
81
|
+
"retained candidate runtime cannot be used by another role",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function candidateArtifactRoots(
|
|
86
|
+
root: string | undefined,
|
|
87
|
+
): Promise<CandidateArtifactRoots | undefined> {
|
|
88
|
+
if (root === undefined) return undefined;
|
|
89
|
+
const directories = leases.get(root);
|
|
90
|
+
if (!directories) throw new Error("candidate runtime lease is unavailable");
|
|
91
|
+
const roots = Object.fromEntries(
|
|
92
|
+
directories.map(({ name, path }) => [name, join(root, "home", path)]),
|
|
93
|
+
);
|
|
94
|
+
await requireCandidateArtifactRoots(roots);
|
|
95
|
+
return roots;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Validate the engine binding and refuse candidate-created filesystem aliases. */
|
|
99
|
+
export async function requireCandidateArtifactRoots(
|
|
100
|
+
roots: CandidateArtifactRoots | undefined,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
for (const [name, path] of Object.entries(roots ?? {})) {
|
|
103
|
+
const { root, directory } = artifactRootBinding(name, path);
|
|
104
|
+
await requireArtifactAncestors(root, directory.path);
|
|
105
|
+
await requireArtifactTree(path);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function artifactRootBinding(name: string, path: string) {
|
|
110
|
+
const parts = relative(nativeStatePath(), path).split(sep);
|
|
111
|
+
const root = join(nativeStatePath(), parts[0] ?? "");
|
|
112
|
+
const directory = leases.get(root)?.find((item) => item.name === name);
|
|
113
|
+
if (!directory || path !== join(root, "home", directory.path))
|
|
114
|
+
throw new Error("candidate artifact grant is not bound to an active trial");
|
|
115
|
+
return { root, directory };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function requireArtifactAncestors(
|
|
119
|
+
root: string,
|
|
120
|
+
path: string,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
let current = root;
|
|
123
|
+
for (const part of ["", "home", ...path.split("/")]) {
|
|
124
|
+
current = join(current, part);
|
|
125
|
+
const info = await lstat(current);
|
|
126
|
+
if (!info.isDirectory() || (await realpath(current)) !== current)
|
|
127
|
+
throw new Error(
|
|
128
|
+
"candidate artifact ancestors must be canonical directories",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function requireArtifactTree(root: string): Promise<void> {
|
|
134
|
+
const pending = [root];
|
|
135
|
+
for (const path of pending) {
|
|
136
|
+
if (pending.length > MAX_ENTRIES)
|
|
137
|
+
throw new Error("candidate artifact tree exceeds the entry limit");
|
|
138
|
+
pending.push(...(await artifactChildren(path)));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function artifactChildren(path: string): Promise<string[]> {
|
|
143
|
+
const info = await lstat(path);
|
|
144
|
+
if (info.isDirectory())
|
|
145
|
+
return (await readdir(path)).map((name) => join(path, name));
|
|
146
|
+
if (!info.isFile() || info.nlink !== 1)
|
|
147
|
+
throw new Error("candidate artifact tree contains a link or special file");
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function candidateArtifactEnvironment(
|
|
152
|
+
roots: CandidateArtifactRoots | undefined,
|
|
153
|
+
): Record<string, string> {
|
|
154
|
+
return Object.fromEntries(
|
|
155
|
+
Object.entries(roots ?? {}).map(([name, path]) => [
|
|
156
|
+
`SEVRO_CANDIDATE_ARTIFACT_${name.toUpperCase()}`,
|
|
157
|
+
path,
|
|
158
|
+
]),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function candidateArtifactReadRoots(
|
|
163
|
+
roots: CandidateArtifactRoots | undefined,
|
|
164
|
+
): string[] {
|
|
165
|
+
return Object.values(roots ?? {});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function releaseCandidateRuntime(
|
|
169
|
+
root: string | undefined,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
if (root === undefined) return;
|
|
172
|
+
await clearFixtureContents(root);
|
|
173
|
+
await rm(root, { recursive: true, force: true });
|
|
174
|
+
leases.delete(root);
|
|
175
|
+
}
|
package/src/cli-invocation.ts
CHANGED
|
@@ -60,6 +60,7 @@ const RUN_OPTIONS = {
|
|
|
60
60
|
host: { type: "string" },
|
|
61
61
|
"codex-bin": { type: "string" },
|
|
62
62
|
"codex-entrypoint": { type: "string" },
|
|
63
|
+
"codex-timeout-ms": { type: "string" },
|
|
63
64
|
"codex-auth-file": { type: "string" },
|
|
64
65
|
"claude-bin": { type: "string" },
|
|
65
66
|
"claude-credential-file": { type: "string" },
|
|
@@ -198,7 +199,11 @@ const HOST_COMPATIBILITY: CompatibilityRule[] = [
|
|
|
198
199
|
!r.codex &&
|
|
199
200
|
!r.semanticCodex &&
|
|
200
201
|
!r.advisoryCodex &&
|
|
201
|
-
|
|
202
|
+
[
|
|
203
|
+
v["codex-bin"],
|
|
204
|
+
v["codex-auth-file"],
|
|
205
|
+
v["codex-timeout-ms"] !== undefined,
|
|
206
|
+
].some(Boolean),
|
|
202
207
|
message: "Codex options require a Codex host route",
|
|
203
208
|
},
|
|
204
209
|
{
|
|
@@ -355,6 +360,10 @@ function commonCodexOptions(
|
|
|
355
360
|
return {
|
|
356
361
|
binary: absoluteOption(values["codex-bin"], "--codex-bin"),
|
|
357
362
|
authFile: absoluteOption(values["codex-auth-file"], "--codex-auth-file"),
|
|
363
|
+
timeoutMs:
|
|
364
|
+
values["codex-timeout-ms"] === undefined
|
|
365
|
+
? undefined
|
|
366
|
+
: Number(values["codex-timeout-ms"]),
|
|
358
367
|
projectRoot: roots.projectRoot,
|
|
359
368
|
resultsRoot: roots.resultsRoot,
|
|
360
369
|
additionalProtectedRoots: [...privateRoots, roots.runStateRoot],
|
package/src/engine.ts
CHANGED
|
@@ -9,3 +9,7 @@ export type {
|
|
|
9
9
|
export { runEvaluation } from "./evaluation-run";
|
|
10
10
|
export { loadRuntimeConfiguration } from "./runtime-config";
|
|
11
11
|
export type { RuntimeConfiguration, RuntimePolicy } from "./runtime-config";
|
|
12
|
+
export type {
|
|
13
|
+
RuntimeArtifactDirectory,
|
|
14
|
+
CandidateArtifactRoots,
|
|
15
|
+
} from "./candidate-artifacts";
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { mkdir,
|
|
2
|
-
import {
|
|
1
|
+
import { mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
allocateWorkspace,
|
|
4
|
+
retainWorkspace,
|
|
5
|
+
removeWorkspace,
|
|
6
|
+
} from "./workspace-state";
|
|
3
7
|
import { dirname, join, resolve } from "node:path";
|
|
4
8
|
import { pathToFileURL } from "node:url";
|
|
5
9
|
import { createFixture } from "./evaluation-fixture";
|
|
@@ -116,13 +120,9 @@ export async function reserveEvaluationWorkspaces(
|
|
|
116
120
|
trial <= options.trialCount && !options.signal?.aborted;
|
|
117
121
|
trial++
|
|
118
122
|
) {
|
|
119
|
-
reservedWorkspaces.push(
|
|
120
|
-
await realpath(await mkdtemp(join(tmpdir(), "sevro-case-"))),
|
|
121
|
-
);
|
|
123
|
+
reservedWorkspaces.push(await allocateWorkspace());
|
|
122
124
|
if (preparedSemantic.length)
|
|
123
|
-
reservedSemanticWorkspaces.push(
|
|
124
|
-
await realpath(await mkdtemp(join(tmpdir(), "sevro-case-"))),
|
|
125
|
-
);
|
|
125
|
+
reservedSemanticWorkspaces.push(await allocateWorkspace());
|
|
126
126
|
}
|
|
127
127
|
const diagnostics: {
|
|
128
128
|
trial: number;
|
|
@@ -243,12 +243,14 @@ export function diagnoseEvaluationFailure(context: EvaluationRuntime): void {
|
|
|
243
243
|
export async function cleanupEvaluationWorkspaces(
|
|
244
244
|
context: EvaluationRuntime,
|
|
245
245
|
): Promise<void> {
|
|
246
|
+
for (const workspace of context.retainedWorkspaces)
|
|
247
|
+
await retainWorkspace(workspace);
|
|
246
248
|
await Promise.all(
|
|
247
249
|
[...context.reservedWorkspaces, ...context.reservedSemanticWorkspaces]
|
|
248
250
|
.filter((workspace) => !context.retainedWorkspaces.has(workspace))
|
|
249
251
|
.map(async (workspace) => {
|
|
250
252
|
try {
|
|
251
|
-
await
|
|
253
|
+
await removeWorkspace(workspace);
|
|
252
254
|
} catch {
|
|
253
255
|
console.warn(`fixture cleanup failed; retained at ${workspace}`);
|
|
254
256
|
}
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import { isRecord } from "./value-guards";
|
|
2
2
|
import { sha256 } from "./identity";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
mkdir,
|
|
6
|
-
mkdtemp,
|
|
7
|
-
readFile,
|
|
8
|
-
realpath,
|
|
9
|
-
rm,
|
|
10
|
-
writeFile,
|
|
11
|
-
} from "node:fs/promises";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
3
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { allocateWorkspace, removeWorkspace } from "./workspace-state";
|
|
13
5
|
import { dirname, join } from "node:path";
|
|
14
6
|
import { fileURLToPath } from "node:url";
|
|
15
7
|
import { canonicalJson } from "./identity";
|
|
@@ -53,9 +45,7 @@ export async function createFixture(
|
|
|
53
45
|
reservedWorkspace?: string,
|
|
54
46
|
): Promise<string> {
|
|
55
47
|
const paths = inlineFixturePaths(fixture, generated, repository);
|
|
56
|
-
const workspace =
|
|
57
|
-
reservedWorkspace ??
|
|
58
|
-
(await realpath(await mkdtemp(join(tmpdir(), "sevro-case-"))));
|
|
48
|
+
const workspace = reservedWorkspace ?? (await allocateWorkspace());
|
|
59
49
|
try {
|
|
60
50
|
await initializeFixtureHistory(
|
|
61
51
|
fixture,
|
|
@@ -76,8 +66,7 @@ export async function createFixture(
|
|
|
76
66
|
await fixtureBinDirectory(workspace);
|
|
77
67
|
return workspace;
|
|
78
68
|
} catch (error) {
|
|
79
|
-
if (!reservedWorkspace)
|
|
80
|
-
await rm(workspace, { recursive: true, force: true });
|
|
69
|
+
if (!reservedWorkspace) await removeWorkspace(workspace);
|
|
81
70
|
throw error;
|
|
82
71
|
}
|
|
83
72
|
}
|
package/src/evaluation-run.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Assessment,
|
|
13
13
|
} from "./results";
|
|
14
14
|
import { runtimePolicySnapshot } from "./runtime-config";
|
|
15
|
+
import { cleanupExpiredWorkspaces } from "./workspace-state";
|
|
15
16
|
import { assertCliResult, assertRunEvidence } from "./schema";
|
|
16
17
|
import { atomicWriteJson } from "./storage";
|
|
17
18
|
import {
|
|
@@ -178,6 +179,7 @@ export async function runEvaluation(
|
|
|
178
179
|
...options,
|
|
179
180
|
runtimePolicy: runtimePolicySnapshot(options.runtimePolicy),
|
|
180
181
|
};
|
|
182
|
+
await cleanupExpiredWorkspaces();
|
|
181
183
|
const plan = await prepareEvaluation(options);
|
|
182
184
|
const runtime = await allocateEvaluationRun(options, plan);
|
|
183
185
|
try {
|