@bjoernrochel/sevro 0.1.0-rc.10 → 0.1.0-rc.11

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.
@@ -37,7 +37,24 @@ partial, as do failed, malformed, or incomplete recognized reads. Direct
37
37
  exact `cat` wrapped by `lean-ctx -c` is accepted only when its reported output
38
38
  still contains the full body; a compressed summary cannot prove the read.
39
39
  The same exact-body check accepts a literal read followed by one literal `rg`
40
- command, separated by a semicolon or newline. It also accepts a chain of literal
40
+ command, separated by a semicolon or newline. It also accepts a single-file
41
+ literal `cat` of the skill followed by one or more literal `cat` reads of
42
+ non-skill files, then one literal `rg` command, with the same separators. For
43
+ example:
44
+
45
+ ```sh
46
+ cat .agents/skills/darrow-guide/SKILL.md; cat docs/specs/repository-guide.md; rg -n -i 'offline|\bsla\b|service.level|guarantee|deployment' --glob '*.md' --glob '!docs/research/**' --glob '!**/SKILL.md' .
47
+ ```
48
+
49
+ This sequence uses the shared stream and native read check. Quoted separators
50
+ remain part of their literal operand. The initial skill body must match the
51
+ beginning of the retained output, so later documentation or search output cannot
52
+ replace an altered or incomplete initial read. Subsequent reads must not name
53
+ another `SKILL.md` or resolve to a mounted or receipt-bound skill through an alias.
54
+ The 64-operand bound applies across all reads in the sequence. Shell expansions,
55
+ including unquoted glob and tilde operands, are unsupported in this form.
56
+
57
+ The check also accepts a chain of literal
41
58
  `cat` fallbacks with `2>/dev/null` and `||`, ending in `true` followed by a newline
42
59
  and one literal `rg` discovery command. A fallback chain must resolve to one
43
60
  canonical file across all its read operands; absent alternate targets do not
@@ -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.10`. Install this exact version:
25
+ The current release is `0.1.0-rc.11`. Install this exact version:
26
26
 
27
27
  ```sh
28
- bun add --exact @bjoernrochel/sevro@0.1.0-rc.10
28
+ bun add --exact @bjoernrochel/sevro@0.1.0-rc.11
29
29
  ```
30
30
 
31
31
  <!-- sevro-current-release:end -->
@@ -289,6 +289,15 @@ instead of being skipped. The wrapper denies network access and protected
289
289
  roots and owns asynchronous hook cleanup. Native timeout/cancellation stops
290
290
  owned hook descendants before private state is removed.
291
291
 
292
+ Prepared native plugin projections live outside the fixture in the current
293
+ host request's private native state. Curated hook read grants validate the
294
+ canonical projection layout, ownership, private state permissions, and binding
295
+ to that request. Only the selected plugin subtree is readable; it stays
296
+ read-only, and credentials, private siblings, and peer requests remain denied.
297
+ Ordinary filesystem declarations cannot select this exception. Codex still
298
+ validates and filters installed plugins with a runtime policy when hooks are
299
+ disabled. Projections are removed with their owning host request.
300
+
292
301
  Auxiliary `.mcp.json` and `.lsp.json` files and plugin mods are unsupported under
293
302
  hook authority. Manifest MCP/LSP declarations are removed from curated mounts.
294
303
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjoernrochel/sevro",
3
- "version": "0.1.0-rc.10",
3
+ "version": "0.1.0-rc.11",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "A standalone evaluation runner for agentic coding capabilities",
6
6
  "type": "module",
@@ -38,6 +38,7 @@ function shellPayload(command: string): string {
38
38
  interface DirectRead {
39
39
  path: string;
40
40
  fallbackPaths?: string[];
41
+ documentationPaths?: string[];
41
42
  operandCount?: number;
42
43
  firstLine?: number;
43
44
  lastLine?: number;
@@ -68,9 +69,11 @@ function sedRead(payload: string, path: string): DirectRead | null {
68
69
  return { path: capturedReadPath(sed, 3), firstLine, lastLine };
69
70
  }
70
71
  function directRead(command: string): DirectRead | null {
71
- const payload = compoundReadPayload(shellPayload(command).trim());
72
+ const shell = shellPayload(command).trim();
73
+ const payload = compoundReadPayload(shell);
72
74
  const path = "(?:\"([^\"]+)\"|'([^']+)'|([^\\s'\"`$;&|<>]+))";
73
75
  return (
76
+ sequentialRead(shell, path) ??
74
77
  catRead(payload, path) ??
75
78
  sedRead(payload, path) ??
76
79
  fallbackRead(payload, path)
@@ -79,6 +82,65 @@ function directRead(command: string): DirectRead | null {
79
82
 
80
83
  const LITERAL_WORD = "(?:\"[^\"$`\\\\]*\"|'[^']*'|[^\\s\"'\\\\`$;&|<>(){}#]+)";
81
84
  const LITERAL_WORDS = `${LITERAL_WORD}(?:[ \\t]+${LITERAL_WORD})*`;
85
+ const SEQUENTIAL_WORD =
86
+ "(?:\"[^\"$`\\\\]*\"|'[^']*'|[^\\s\"'\\\\`$;&|<>(){}#*?\\[\\]~]+)";
87
+ const SEQUENTIAL_WORDS = `${SEQUENTIAL_WORD}(?:[ \\t]+${SEQUENTIAL_WORD})*`;
88
+
89
+ // Recognize only literal commands; quoted separators remain part of their word.
90
+ function literalCommands(payload: string): string[] | null {
91
+ const clause = new RegExp(
92
+ `[ \\t\\r\\n]*(${SEQUENTIAL_WORDS})[ \\t]*(?:[;\\n]|$)`,
93
+ "gy",
94
+ );
95
+ const commands: string[] = [];
96
+ while (clause.lastIndex < payload.length) {
97
+ const match = clause.exec(payload);
98
+ if (!match || commands.length > MAX_NATIVE_READ_ATTEMPTS) return null;
99
+ commands.push(match.slice(1, 2).join(""));
100
+ }
101
+ return commands;
102
+ }
103
+
104
+ function sequentialRead(payload: string, path: string): DirectRead | null {
105
+ const commands = literalCommands(payload);
106
+ if (!commands || commands.length < 3) return null;
107
+ const search = commands.slice(-1).join("");
108
+ if (!new RegExp(`^(?:/usr/bin/)?rg[ \\t]+${LITERAL_WORDS}$`).test(search))
109
+ return null;
110
+ return sequentialCatRead(commands.slice(0, -1), path);
111
+ }
112
+
113
+ function sequentialCatRead(
114
+ commands: string[],
115
+ path: string,
116
+ ): DirectRead | null {
117
+ const first = singleSkillRead(commands.slice(0, 1).join(""), path);
118
+ if (!first) return null;
119
+ const operands = commands
120
+ .slice(1)
121
+ .map((command) => catOperands(command, path));
122
+ if (!operands.every((paths): paths is string[] => paths !== null))
123
+ return null;
124
+ const paths = operands.flat();
125
+ if (unsupportedDocumentationOperands(paths)) return null;
126
+ return {
127
+ ...first,
128
+ documentationPaths: paths,
129
+ operandCount: paths.length + 1,
130
+ };
131
+ }
132
+
133
+ function singleSkillRead(payload: string, path: string): DirectRead | null {
134
+ const read = catRead(payload, path);
135
+ return read?.operandCount === 1 ? read : null;
136
+ }
137
+
138
+ function unsupportedDocumentationOperands(paths: string[]): boolean {
139
+ return (
140
+ paths.length >= MAX_NATIVE_READ_ATTEMPTS ||
141
+ paths.some((path) => /(?:^|\/)SKILL\.md$/.test(path))
142
+ );
143
+ }
82
144
 
83
145
  function compoundReadPayload(payload: string): string {
84
146
  const sequential = payload.match(
@@ -102,14 +164,11 @@ function fallbackRead(payload: string, path: string): DirectRead | null {
102
164
  ].map((match) => catRead(match.slice(1, 2).join(""), path));
103
165
  if (
104
166
  fallbackOperandCount(reads) > MAX_NATIVE_READ_ATTEMPTS ||
105
- reads.some((read) => read === null)
167
+ !reads.every((read): read is DirectRead => read !== null)
106
168
  )
107
169
  return null;
108
- const paths = reads.flatMap((read) => (read ? [read.path] : []));
109
- const first = paths[0];
110
- return first === undefined
111
- ? null
112
- : { path: first, fallbackPaths: paths.slice(1) };
170
+ const paths = reads.map((read) => read.path);
171
+ return { path: paths.slice(0, 1).join(""), fallbackPaths: paths.slice(1) };
113
172
  }
114
173
 
115
174
  function fallbackOperandCount(reads: Array<DirectRead | null>): number {
@@ -121,7 +180,7 @@ function unsupportedCatOptions(payload: string, paths: string[]): boolean {
121
180
  return paths.some((path) => path.startsWith("-"));
122
181
  }
123
182
 
124
- function catRead(payload: string, path: string): DirectRead | null {
183
+ function catOperands(payload: string, path: string): string[] | null {
125
184
  const cat = payload.match(
126
185
  new RegExp(`^(?:/bin/)?cat\\s+(?:--\\s+)?(${path}(?:\\s+${path})*)$`),
127
186
  );
@@ -129,12 +188,14 @@ function catRead(payload: string, path: string): DirectRead | null {
129
188
  const paths = [
130
189
  ...cat.slice(1, 2).join("").matchAll(new RegExp(path, "g")),
131
190
  ].map((match) => capturedReadPath(match, 1));
191
+ return unsupportedCatOptions(payload, paths) ? null : paths;
192
+ }
193
+
194
+ function catRead(payload: string, path: string): DirectRead | null {
195
+ const paths = catOperands(payload, path);
196
+ if (!paths) return null;
132
197
  const skills = paths.filter((value) => /(?:^|\/)SKILL\.md$/.test(value));
133
- if (
134
- skills.length !== 1 ||
135
- paths.length > MAX_NATIVE_READ_ATTEMPTS ||
136
- unsupportedCatOptions(payload, paths)
137
- )
198
+ if (skills.length !== 1 || paths.length > MAX_NATIVE_READ_ATTEMPTS)
138
199
  return null;
139
200
  return { path: skills.join(""), operandCount: paths.length };
140
201
  }
@@ -197,11 +258,26 @@ async function mountedReadPath(
197
258
  );
198
259
  }
199
260
  function readAttemptPaths(command: string): string[] {
200
- const payload = compoundReadPayload(shellPayload(command).trim());
261
+ const shell = shellPayload(command).trim();
262
+ const payload = compoundReadPayload(shell);
201
263
  const path = "(?:\"([^\"]+)\"|'([^']+)'|([^\\s'\"`$;&|<>]+))";
202
264
  const fallback = fallbackRead(payload, path);
203
265
  if (fallback) return [fallback.path, ...(fallback.fallbackPaths ?? [])];
204
- return literalReadAttemptPaths(payload, path);
266
+ const sequential = sequentialRead(shell, path);
267
+ if (sequential) return [sequential.path];
268
+ return [
269
+ ...literalReadAttemptPaths(payload, path),
270
+ ...sequentialReadAttemptPaths(shell, path),
271
+ ];
272
+ }
273
+
274
+ function sequentialReadAttemptPaths(payload: string, path: string): string[] {
275
+ const first = payload.match(
276
+ new RegExp(`^(${SEQUENTIAL_WORDS})[ \\t]*[;\\n]`),
277
+ );
278
+ if (!first) return [];
279
+ // The suffix may be unsupported. This records an attempt, never read credit.
280
+ return literalReadAttemptPaths(first.slice(1, 2).join(""), path);
205
281
  }
206
282
  function literalReadAttemptPaths(payload: string, path: string): string[] {
207
283
  const cat = payload.match(
@@ -323,8 +399,29 @@ async function resolvedSkillRead(
323
399
  if (skill === null) return null;
324
400
  const text = await verifiedSkillBody(actual, skill);
325
401
  if (text === null) return null;
402
+ if (
403
+ await ambiguousSequentialRead(
404
+ declaration.read,
405
+ commandCwd,
406
+ workspace,
407
+ pluginRoots,
408
+ )
409
+ )
410
+ return null;
326
411
  return skillReadReceipt(actual, skill, declaration, text);
327
412
  }
413
+
414
+ async function ambiguousSequentialRead(
415
+ read: DirectRead,
416
+ commandCwd: string,
417
+ workspace: string,
418
+ pluginRoots: string[],
419
+ ): Promise<boolean> {
420
+ for (const path of read.documentationPaths ?? [])
421
+ if (await mountedReadPath(path, commandCwd, workspace, pluginRoots))
422
+ return true;
423
+ return false;
424
+ }
328
425
  async function resolvedReadPath(
329
426
  read: DirectRead,
330
427
  commandCwd: string,
@@ -344,10 +441,19 @@ function skillReadReceipt(
344
441
  ): VerifiedRead | null {
345
442
  const range = readRange(text, declaration.read);
346
443
  if (range[0] === range[1]) return null;
347
- if (!declaration.output.includes(text.slice(...range))) return null;
444
+ if (!matchesReadOutput(declaration, text.slice(...range))) return null;
348
445
  return { skill, path: actual, range, bodyLength: text.length };
349
446
  }
350
447
 
448
+ function matchesReadOutput(
449
+ declaration: { read: DirectRead; output: string },
450
+ expected: string,
451
+ ): boolean {
452
+ if (declaration.read.documentationPaths !== undefined)
453
+ return declaration.output.startsWith(expected);
454
+ return declaration.output.includes(expected);
455
+ }
456
+
351
457
  function fullCoverage(
352
458
  ranges: Array<[number, number]>,
353
459
  length: number,
@@ -77,7 +77,7 @@ export async function codexHookMounts(
77
77
  if (!request.runtimePolicy) return undefined;
78
78
  const hooks = await prepareHookMounts({
79
79
  workspace: request.workspace,
80
- stateRoot: join(request.workspace, ".git", "sevro-runtime"),
80
+ stateRoot: state.stateRoot,
81
81
  privateRoot: state.codexHome,
82
82
  pluginRoots: installedPluginRoots,
83
83
  protectedRoots: state.protectedRoots,
@@ -380,7 +380,7 @@ export async function prepareCodexState(
380
380
  const shellRoot = await fixtureShellRoot(request, paths.parentHome);
381
381
  const env = codexEnvironment(paths, request, shellRoot);
382
382
  applyCodexRuntime(runtime, env, paths);
383
- return { ...paths, sandboxBinary, profileId, env, protectedRoots };
383
+ return { ...paths, stateRoot, sandboxBinary, profileId, env, protectedRoots };
384
384
  }
385
385
 
386
386
  function requireCodexRuntimeReadRoots(
@@ -54,7 +54,7 @@ export async function prepareLinuxSandboxCommand(
54
54
  requireLinuxSandbox();
55
55
  await requireNativeTranscriptViews(options.transcriptReadRoots);
56
56
  await requireCandidateArtifactRoots(options.candidateArtifactRoots);
57
- await requireNativeHookRoots(options.hookReadRoots);
57
+ await requireNativeHookRoots(options.hookReadRoots, options.privateStateRoot);
58
58
  requireIsolationPaths(options);
59
59
  const workspace = await realpath(options.workspace);
60
60
  const roots = options.protectedRootsCanonical
@@ -2,7 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { mkdir, realpath, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
- import { requireNativeTranscriptViews } from "../native-transcript-state";
5
+ import {
6
+ requireNativeTranscriptViews,
7
+ requireNativeHookRoots,
8
+ } from "../native-transcript-state";
6
9
  import { requireCandidateArtifactRoots } from "../candidate-artifacts";
7
10
  import { workspaceBoundaryException } from "../workspace-state";
8
11
  import {
@@ -148,6 +151,7 @@ export async function prepareMacSandboxCommand(
148
151
  requireSandboxAvailable();
149
152
  await requireNativeTranscriptViews(options.transcriptReadRoots);
150
153
  await requireCandidateArtifactRoots(options.candidateArtifactRoots);
154
+ await requireNativeHookRoots(options.hookReadRoots, options.privateStateRoot);
151
155
  requireIsolationPaths(options);
152
156
  const workspace = await realpath(options.workspace);
153
157
  const roots = options.protectedRootsCanonical
@@ -170,10 +174,7 @@ export async function prepareMacSandboxCommand(
170
174
  options.denyNetwork,
171
175
  readRoots,
172
176
  ownedRoot,
173
- [
174
- ...(options.transcriptReadRoots ?? []),
175
- ...Object.values(options.candidateArtifactRoots ?? {}),
176
- ],
177
+ curatedReadRoots(options),
177
178
  workspace,
178
179
  ),
179
180
  { flag: "wx", mode: 0o600 },
@@ -186,6 +187,14 @@ export async function prepareMacSandboxCommand(
186
187
  };
187
188
  }
188
189
 
190
+ function curatedReadRoots(options: IsolationOptions): string[] {
191
+ return [
192
+ ...(options.transcriptReadRoots ?? []),
193
+ ...(options.hookReadRoots ?? []),
194
+ ...Object.values(options.candidateArtifactRoots ?? {}),
195
+ ];
196
+ }
197
+
189
198
  function requireSandboxAvailable(): void {
190
199
  if (process.platform !== "darwin" || !existsSync(SANDBOX_EXEC))
191
200
  throw new HostIsolationError("macOS sandbox-exec isolation is unavailable");
@@ -84,20 +84,39 @@ export async function requireNativeTranscriptViews(
84
84
  /** Curated hook copies are a separate runner-owned exception, never policy roots. */
85
85
  export async function requireNativeHookRoots(
86
86
  roots: string[] | undefined,
87
+ privateRoot: string,
87
88
  ): Promise<void> {
88
- for (const root of roots ?? []) await requireNativeHookRoot(root);
89
+ for (const root of roots ?? [])
90
+ await requireNativeHookRoot(root, privateRoot);
89
91
  }
90
92
 
91
- async function requireNativeHookRoot(root: string): Promise<void> {
93
+ async function requireNativeHookRoot(
94
+ root: string,
95
+ privateRoot: string,
96
+ ): Promise<void> {
92
97
  const parent = await nativeStateParent();
93
98
  const state = dirname(dirname(root));
94
99
  if (!nativeHookLayout(root, state, parent) || (await realpath(root)) !== root)
95
100
  throw new Error("native hook read root must be a canonical curated mount");
101
+ await requireNativeHookOwner(state, privateRoot);
96
102
  const info = await lstat(root);
97
103
  if (!info.isDirectory() || info.uid !== userInfo().uid)
98
104
  throw new Error("native hook read root must be owned");
99
105
  }
100
106
 
107
+ async function requireNativeHookOwner(
108
+ state: string,
109
+ privateRoot: string,
110
+ ): Promise<void> {
111
+ if (
112
+ dirname(privateRoot) !== state ||
113
+ (await realpath(privateRoot)) !== privateRoot
114
+ )
115
+ throw new Error("native hook read root must belong to the current request");
116
+ requirePrivateNamespace(await lstat(state));
117
+ requirePrivateNamespace(await lstat(privateRoot));
118
+ }
119
+
101
120
  function nativeHookLayout(
102
121
  root: string,
103
122
  state: string,
@@ -105,7 +124,7 @@ function nativeHookLayout(
105
124
  ): boolean {
106
125
  return (
107
126
  dirname(state) === parent &&
108
- basename(state).startsWith("claude-") &&
127
+ /^(?:claude|codex)-/.test(basename(state)) &&
109
128
  basename(dirname(root)) === "runtime-plugins" &&
110
129
  /^plugin-[0-9]+$/.test(basename(root))
111
130
  );
@@ -217,7 +217,7 @@ async function hookReadRoots(
217
217
  ];
218
218
  const curated = trusted ? [root] : [];
219
219
  requireRuntimeReadRoots(ordinary, options.protectedRoots);
220
- await requireNativeHookRoots(curated);
220
+ await requireNativeHookRoots(curated, options.privateRoot);
221
221
  return { ordinary, curated };
222
222
  }
223
223