@bjoernrochel/sevro 0.1.0-rc.15 → 0.1.0-rc.16

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.
@@ -106,8 +106,16 @@ trial in the Git fixture after source history and working-tree files are built,
106
106
  before extension artifacts are mounted or the host starts. The command is never
107
107
  interpreted through a shell by Sevro; an extension may explicitly select a
108
108
  shell executable. `{{sevro.project}}` and `{{sevro.workspace}}` in environment
109
- values resolve to the declared roots at execution time. The symbolic values and
110
- command enter fixture and comparison identity, while retained configuration
109
+ values resolve to the declared roots at execution time. Setup's `HOME` is the
110
+ candidate commands' private trial home, outside the Git fixture. Ordinary
111
+ `$HOME` paths can initialize directories and seed prior state before the candidate
112
+ starts, including without a runtime configuration file. This home is separate
113
+ from host authentication and native configuration, other trials, and grader homes.
114
+ Setup's `TMPDIR` remains disposable and is removed when setup finishes; the shared
115
+ home survives through candidate execution and grading, then is removed even on
116
+ setup failure, cancellation, or dry runs. Trusted injected hosts must use the
117
+ supplied `candidateRuntimeRoot` and runtime policy to preserve this boundary.
118
+ The symbolic values and command enter fixture and comparison identity, while retained configuration
111
119
  records only their digest. Setup has a two-minute limit, inherits no credential
112
120
  environment variables, and its output is discarded. Failure or interruption
113
121
  stops the run before host execution; it cannot become a passing assessment.
@@ -23,10 +23,10 @@ Local installation records the selected version in the project lockfile.
23
23
 
24
24
  ## Current release
25
25
 
26
- The current release is `0.1.0-rc.15`. Install this exact version:
26
+ The current release is `0.1.0-rc.16`. Install this exact version:
27
27
 
28
28
  ```sh
29
- bun add --exact @bjoernrochel/sevro@0.1.0-rc.15
29
+ bun add --exact @bjoernrochel/sevro@0.1.0-rc.16
30
30
  ```
31
31
 
32
32
  <!-- sevro-current-release:end -->
@@ -260,7 +260,10 @@ so zsh here-documents use their existing private temporary storage while shared
260
260
  host temporary directories remain denied. `TMPPREFIX` is runner-owned and cannot
261
261
  be inherited or set through runtime configuration.
262
262
  Sevro supplies private `HOME` and temporary storage and sets `UV_OFFLINE=1`
263
- and `PYTHONDONTWRITEBYTECODE=1`. Candidate, hook, checker, semantic, and advisory
263
+ and `PYTHONDONTWRITEBYTECODE=1`. Fixture setup uses the candidate commands'
264
+ private `HOME`, so `$HOME` paths initialize the same state the candidate will
265
+ use. Its temporary directory stays disposable.
266
+ Candidate, hook, checker, semantic, and advisory
264
267
  state are separate. With a runtime policy, native candidate, semantic, and
265
268
  advisory roots live in a unique runner-owned host directory outside the Git
266
269
  fixture. The root contains `home`, `tmp`, and declared seed slots; plugins can
@@ -270,7 +273,8 @@ configuration, and live transcript sources. Checker and hook caches also live
270
273
  outside candidate-controlled workspace state. Private roots survive native
271
274
  continuation turns and children, then are removed with their owning trial or
272
275
  host state after owned hooks terminate. Opted-in candidate artifact runtimes
273
- remain alive through grading as described below. Runs without a selected runtime policy
276
+ remain alive through grading as described below. Runs with neither fixture setup
277
+ nor a selected runtime policy
274
278
  retain their previous writable-state locations.
275
279
 
276
280
  Claude refuses managed workspace or runtime namespaces overlapping its
@@ -297,7 +301,8 @@ candidate's private home after execution.
297
301
  }
298
302
  ```
299
303
 
300
- The example permits only the candidate-created `~/.darrow/reviews` subtree.
304
+ The example permits only the `~/.darrow/reviews` subtree, which may be initialized
305
+ by trusted fixture setup or created by the candidate.
301
306
  Its original absolute path reaches isolated shell checks and native
302
307
  semantic/advisory commands as `SEVRO_CANDIDATE_ARTIFACT_REVIEWS`. Graders keep
303
308
  their own private `HOME`; use the supplied variable to read candidate evidence.
@@ -313,12 +318,31 @@ to 16,384 entries, including its root. Missing directories, symlinked ancestors,
313
318
  descendant symlinks, hardlinks and special files make grading an error; missing
314
319
  evidence cannot produce a pass.
315
320
 
321
+ Initialize an empty directory or seed prior Review state in the extension's
322
+ trusted `fixtureSetup` command before the candidate starts. For example:
323
+
324
+ ```sh
325
+ mkdir -p "$HOME/.darrow/reviews"
326
+ printf '%s\n' '{"verdict":"passed"}' > "$HOME/.darrow/reviews/prior.json"
327
+ ```
328
+
329
+ Setup and candidate commands share this private trial home, including when no
330
+ artifact grants or runtime file are selected. Graders keep their own homes and
331
+ receive only declared read-only artifact grants. To test missing reports or
332
+ absence of new artifacts, initialize the directory without writing a report;
333
+ an empty safe directory reaches ordinary task checks. Missing required reports
334
+ can then fail those checks, while negative cases can assert the directory is
335
+ empty. Sevro does not automatically create declared artifact directories: if
336
+ setup and candidate both leave one absent, or replace it with an unsafe path,
337
+ grading still errors. Setup state is temporary and is removed with its trial,
338
+ including setup failure, cancellation, and dry runs.
339
+
316
340
  The grant permits reads and denies writes, creation, deletion and renaming.
317
341
  Credentials, other home contents and peer role/trial state stay protected.
318
342
  Ordinary `filesystem.readOnlyRoots` cannot select this private-state exception.
319
343
  Artifact grants and native transcript access are independent opt-ins.
320
344
 
321
- The trial allocates the retained runtime before launching its candidate and
345
+ The trial allocates the retained runtime before fixture setup and
322
346
  removes it after grading, advisory review and evidence persistence, including
323
347
  failure and cancellation paths. Review state remains outside the Git fixture.
324
348
  These artifacts are temporary; declaring access does not copy their bytes into
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjoernrochel/sevro",
3
- "version": "0.1.0-rc.15",
3
+ "version": "0.1.0-rc.16",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "A standalone evaluation runner for agentic coding capabilities",
6
6
  "type": "module",
@@ -65,10 +65,12 @@ function validArtifactPath(path: string): boolean {
65
65
  /** Allocate before host execution so exceptions cannot lose cleanup ownership. */
66
66
  export async function allocateCandidateRuntime(
67
67
  policy: RuntimePolicy | undefined,
68
+ retainHome = false,
68
69
  ): Promise<string | undefined> {
69
70
  const directories = policy?.artifactDirectories ?? [];
70
71
  const codexState = codexStateEnabled(policy);
71
- if (!directories.length && !codexState) return undefined;
72
+ if (!candidateRuntimeRequired(directories, codexState, retainHome))
73
+ return undefined;
72
74
  requireArtifactDirectories(directories);
73
75
  const root = await allocateNativeState("candidate-runtime-");
74
76
  leases.set(root, directories);
@@ -76,6 +78,14 @@ export async function allocateCandidateRuntime(
76
78
  return root;
77
79
  }
78
80
 
81
+ function candidateRuntimeRequired(
82
+ directories: RuntimeArtifactDirectory[],
83
+ codexState: boolean,
84
+ retainHome: boolean,
85
+ ): boolean {
86
+ return directories.length > 0 || codexState || retainHome;
87
+ }
88
+
79
89
  export async function requireCandidateRuntime(
80
90
  root: string | undefined,
81
91
  role?: RuntimeRole,
@@ -181,6 +181,7 @@ export async function prepareTrialWorkspace(
181
181
  invocation: PreparedInvocation,
182
182
  workspaces: RunWorkspaceReservations,
183
183
  trial: number,
184
+ candidateRuntimeRoot?: string,
184
185
  ) {
185
186
  const reservation = workspaces.candidate(trial);
186
187
  workspaces.retain(reservation);
@@ -195,6 +196,7 @@ export async function prepareTrialWorkspace(
195
196
  fixture.projectRoot,
196
197
  options.signal,
197
198
  reservation,
199
+ candidateRuntimeRoot,
198
200
  ).catch((error: unknown) => {
199
201
  if (
200
202
  !options.signal?.aborted ||
@@ -203,12 +205,13 @@ export async function prepareTrialWorkspace(
203
205
  throw error;
204
206
  return null;
205
207
  });
206
- return trialWorkspaceDetails(
208
+ const details = await trialWorkspaceDetails(
207
209
  options.case,
208
210
  invocation,
209
211
  reservation,
210
212
  workspace,
211
213
  );
214
+ return { ...details, candidateRuntimeRoot };
212
215
  }
213
216
 
214
217
  async function trialWorkspaceDetails(
@@ -231,6 +231,7 @@ class EvaluationTrial {
231
231
  private readonly fixture: Awaited<ReturnType<typeof prepareTrialWorkspace>>,
232
232
  ) {
233
233
  this.state = initialTrialState(options.dry);
234
+ this.state.candidateRuntimeRoot = fixture.candidateRuntimeRoot;
234
235
  this.evidence = new TrialEvidenceWriter({
235
236
  runDir: context.resources.runDir,
236
237
  trial,
@@ -296,7 +297,7 @@ class EvaluationTrial {
296
297
  workspace: this.fixture.workspace,
297
298
  condition: this.options.condition,
298
299
  fixtureBinDir: this.fixture.fixtureBinDir,
299
- runtimePolicy: this.options.runtimePolicy,
300
+ runtimePolicy: this.candidateRuntimePolicy(),
300
301
  instrumentation: snapshotInstrumentation(
301
302
  this.context.prepared.requestedInstrumentation,
302
303
  ),
@@ -313,6 +314,16 @@ class EvaluationTrial {
313
314
  };
314
315
  }
315
316
 
317
+ private candidateRuntimePolicy() {
318
+ if (this.options.runtimePolicy) return this.options.runtimePolicy;
319
+ if (!this.context.prepared.fixture.fixtureSetup) return undefined;
320
+ return {
321
+ format: "sevro.runtime.v1" as const,
322
+ environment: {},
323
+ readOnlyRoots: [],
324
+ };
325
+ }
326
+
316
327
  private candidateReservedIds(): Set<string> {
317
328
  return new Set([
318
329
  "sevro.observation.final-message",
@@ -387,9 +398,6 @@ class EvaluationTrial {
387
398
  const started = performance.now();
388
399
  try {
389
400
  ensureNotCancelled(this.options.signal);
390
- this.state.candidateRuntimeRoot = await allocateCandidateRuntime(
391
- this.options.runtimePolicy,
392
- );
393
401
  this.state.hostResult = await this.options.host.run(
394
402
  this.candidateRequest(),
395
403
  );
@@ -781,7 +789,6 @@ class EvaluationTrial {
781
789
  evaluationProgress(this.options, stage, status, this.trial);
782
790
  }
783
791
  private async cleanup(): Promise<void> {
784
- await releaseCandidateRuntime(this.state.candidateRuntimeRoot);
785
792
  if (this.state.candidateTranscriptRoot)
786
793
  await rm(this.state.candidateTranscriptRoot, {
787
794
  recursive: true,
@@ -819,21 +826,30 @@ export async function executeEvaluationTrials(
819
826
  signal: options.signal,
820
827
  },
821
828
  async (trial, stopAdmission) => {
822
- const fixture = await prepareTrialWorkspace(
823
- options,
824
- context.prepared.fixture,
825
- context.prepared.invocation,
826
- context.resources.workspaces,
827
- trial,
829
+ const candidateRuntimeRoot = await allocateCandidateRuntime(
830
+ options.runtimePolicy,
831
+ Boolean(context.prepared.fixture.fixtureSetup),
828
832
  );
829
- evaluationProgress(options, "preparation", "completed", trial);
830
- return new EvaluationTrial(
831
- options,
832
- context,
833
- trial,
834
- stopAdmission,
835
- fixture,
836
- ).run();
833
+ try {
834
+ const fixture = await prepareTrialWorkspace(
835
+ options,
836
+ context.prepared.fixture,
837
+ context.prepared.invocation,
838
+ context.resources.workspaces,
839
+ trial,
840
+ candidateRuntimeRoot,
841
+ );
842
+ evaluationProgress(options, "preparation", "completed", trial);
843
+ return await new EvaluationTrial(
844
+ options,
845
+ context,
846
+ trial,
847
+ stopAdmission,
848
+ fixture,
849
+ ).run();
850
+ } finally {
851
+ await releaseCandidateRuntime(candidateRuntimeRoot);
852
+ }
837
853
  },
838
854
  );
839
855
  }
@@ -36,6 +36,7 @@ export async function createFixture(
36
36
  projectRoot: string,
37
37
  signal?: AbortSignal,
38
38
  reservedWorkspace?: string,
39
+ candidateRuntimeRoot?: string,
39
40
  ): Promise<string> {
40
41
  const paths = inlineFixturePaths(fixture, generated, repository);
41
42
  const workspace = reservedWorkspace ?? (await allocateWorkspace());
@@ -53,7 +54,13 @@ export async function createFixture(
53
54
  workspace,
54
55
  );
55
56
  await writeInlineFixtureFiles(paths, workspace);
56
- await setupFixture(setup, workspace, projectRoot, signal);
57
+ await setupFixture(
58
+ setup,
59
+ workspace,
60
+ projectRoot,
61
+ signal,
62
+ candidateRuntimeRoot,
63
+ );
57
64
  await mountPreparationArtifacts(artifacts, workspace);
58
65
  await excludePreparationArtifacts(artifacts, workspace);
59
66
  await fixtureBinDirectory(workspace);
@@ -131,6 +138,7 @@ async function setupFixture(
131
138
  workspace: string,
132
139
  projectRoot: string,
133
140
  signal?: AbortSignal,
141
+ candidateRuntimeRoot?: string,
134
142
  ): Promise<void> {
135
143
  if (!setup) return;
136
144
  await runFixtureSetup(setup, {
@@ -138,6 +146,7 @@ async function setupFixture(
138
146
  projectRoot,
139
147
  fixtureBinDir: (await fixtureBinDirectory(workspace)) ?? undefined,
140
148
  signal,
149
+ candidateRuntimeRoot,
141
150
  });
142
151
  }
143
152
 
@@ -1,7 +1,8 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { delimiter, isAbsolute, join } from "node:path";
4
4
  import { isRecord } from "../value-guards";
5
+ import { requireCandidateRuntime } from "../candidate-artifacts";
5
6
  import {
6
7
  ProcessDeadline,
7
8
  terminateOwnedProcessGroup,
@@ -112,6 +113,7 @@ interface FixtureSetupOptions {
112
113
  projectRoot: string;
113
114
  fixtureBinDir?: string;
114
115
  signal?: AbortSignal;
116
+ candidateRuntimeRoot?: string;
115
117
  }
116
118
 
117
119
  function setupPath(directory: string | undefined): string {
@@ -134,7 +136,9 @@ function executionEnvironment(
134
136
  );
135
137
  return {
136
138
  PATH: setupPath(options.fixtureBinDir),
137
- HOME: home,
139
+ HOME: options.candidateRuntimeRoot
140
+ ? join(options.candidateRuntimeRoot, "home")
141
+ : home,
138
142
  TMPDIR: home,
139
143
  LANG: process.env.LANG ?? "C",
140
144
  GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
@@ -201,6 +205,12 @@ export async function runFixtureSetup(
201
205
  options: FixtureSetupOptions,
202
206
  ): Promise<void> {
203
207
  if (options.signal?.aborted) throw new Error("fixture setup cancelled");
208
+ await requireCandidateRuntime(options.candidateRuntimeRoot);
209
+ if (options.candidateRuntimeRoot)
210
+ await mkdir(join(options.candidateRuntimeRoot, "home"), {
211
+ recursive: true,
212
+ mode: 0o700,
213
+ });
204
214
  const home = await mkdtemp(join(tmpdir(), "sevro-setup-"));
205
215
  const execution = new FixtureSetupExecution(setup, options, home);
206
216
  try {