@bjoernrochel/sevro 0.1.0-rc.1

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.
Files changed (71) hide show
  1. package/LICENSE +159 -0
  2. package/README.md +71 -0
  3. package/docs/advisory-fixture.md +23 -0
  4. package/docs/builtin-graders-v1.md +77 -0
  5. package/docs/cache-ownership-validation.md +49 -0
  6. package/docs/claude-continuation-validation.md +47 -0
  7. package/docs/codex-host.md +134 -0
  8. package/docs/concurrency-validation.md +86 -0
  9. package/docs/configuration-root-validation.md +70 -0
  10. package/docs/development-cli.md +327 -0
  11. package/docs/engine-slice.md +114 -0
  12. package/docs/extension-protocol-v1.md +292 -0
  13. package/docs/fixture-cleanup-validation.md +70 -0
  14. package/docs/identity-v1.md +65 -0
  15. package/docs/native-controls.md +29 -0
  16. package/docs/releases.md +88 -0
  17. package/docs/report-v1.md +32 -0
  18. package/docs/results-v1.md +118 -0
  19. package/examples/basic/graded.json +13 -0
  20. package/examples/basic/host.ts +8 -0
  21. package/examples/basic/prompt-only.json +7 -0
  22. package/package.json +45 -0
  23. package/schemas/cli-result-v1.schema.json +184 -0
  24. package/schemas/extension-v1.schema.json +659 -0
  25. package/schemas/report-v1.schema.json +164 -0
  26. package/schemas/run-evidence-v1.schema.json +510 -0
  27. package/src/advisory-fixture.ts +282 -0
  28. package/src/advisory.ts +105 -0
  29. package/src/cli.ts +837 -0
  30. package/src/engine.ts +2341 -0
  31. package/src/extension-client.ts +257 -0
  32. package/src/extension-session.ts +335 -0
  33. package/src/fixture-bin.ts +87 -0
  34. package/src/fixture-cleanup.ts +38 -0
  35. package/src/fixture-setup.ts +163 -0
  36. package/src/generated-fixture.ts +220 -0
  37. package/src/git-hooks.ts +63 -0
  38. package/src/graders/git-head.ts +176 -0
  39. package/src/graders/output.ts +359 -0
  40. package/src/graders/semantic.ts +120 -0
  41. package/src/graders/shell.ts +277 -0
  42. package/src/hosts/claude-continuation.ts +94 -0
  43. package/src/hosts/claude-credential.ts +73 -0
  44. package/src/hosts/claude-events.ts +128 -0
  45. package/src/hosts/claude-nested-skills.ts +218 -0
  46. package/src/hosts/claude-repository-invocation.ts +239 -0
  47. package/src/hosts/claude-settings.ts +50 -0
  48. package/src/hosts/claude-tool-calls.ts +159 -0
  49. package/src/hosts/claude.ts +511 -0
  50. package/src/hosts/codex-configuration.ts +85 -0
  51. package/src/hosts/codex-events.ts +102 -0
  52. package/src/hosts/codex-native-calls.ts +702 -0
  53. package/src/hosts/codex-profile.ts +78 -0
  54. package/src/hosts/codex-skill-reads.ts +345 -0
  55. package/src/hosts/codex.ts +769 -0
  56. package/src/hosts/isolation-roots.ts +60 -0
  57. package/src/hosts/mac-sandbox.ts +108 -0
  58. package/src/hosts/native-controls.ts +124 -0
  59. package/src/hosts/protected-worktrees.ts +56 -0
  60. package/src/hosts/workspace-fingerprint.ts +55 -0
  61. package/src/identity.ts +156 -0
  62. package/src/instrumentation.ts +106 -0
  63. package/src/preparation.ts +211 -0
  64. package/src/provenance.ts +244 -0
  65. package/src/report.ts +351 -0
  66. package/src/repository-fixture.ts +286 -0
  67. package/src/results.ts +160 -0
  68. package/src/run-owner.ts +276 -0
  69. package/src/schema.ts +28 -0
  70. package/src/storage.ts +23 -0
  71. package/src/trial-scheduler.ts +27 -0
@@ -0,0 +1,769 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import {
4
+ lstat,
5
+ mkdir,
6
+ mkdtemp,
7
+ readFile,
8
+ readdir,
9
+ realpath,
10
+ rm,
11
+ stat,
12
+ writeFile,
13
+ } from "node:fs/promises";
14
+ import { tmpdir } from "node:os";
15
+ import { delimiter, dirname, isAbsolute, join, relative, sep } from "node:path";
16
+ import type { HostAdapter } from "../engine";
17
+ import { codexNativeControls } from "./native-controls";
18
+ import { fixtureParts } from "../preparation";
19
+ import { summarizeCodexEvents } from "./codex-events";
20
+ import {
21
+ codexNativeCallObservation,
22
+ codexNativeSessionLastOrdinal,
23
+ } from "./codex-native-calls";
24
+ import { codexSkillReadObservation } from "./codex-skill-reads";
25
+ import { codexPermissionProfile } from "./codex-profile";
26
+ import { evaluationProtectedRoots } from "./isolation-roots";
27
+ import { workspaceFingerprint } from "./workspace-fingerprint";
28
+
29
+ const MAX_AUTH_BYTES = 1024 * 1024;
30
+ const MAX_EVENT_BYTES = 8 * 1024 * 1024;
31
+ const DEFAULT_TIMEOUT_MS = 10 * 60_000;
32
+ const MAX_TIMEOUT_MS = 30 * 60_000;
33
+
34
+ async function verifyCodexInvocation(
35
+ request: Parameters<HostAdapter["run"]>[0],
36
+ ): Promise<void> {
37
+ const selected = request.explicitSkillInvocation;
38
+ if (!selected) return;
39
+ const { skillName, token } = selected;
40
+ const tokenCount =
41
+ request.prompt.split(token).length -
42
+ 1 +
43
+ (request.followUpPrompt?.split(token).length ?? 1) -
44
+ 1;
45
+ if (
46
+ !/^[A-Za-z0-9._-]+$/.test(skillName) ||
47
+ skillName === "." ||
48
+ skillName === ".." ||
49
+ tokenCount !== 1
50
+ )
51
+ throw new Error("invalid Codex explicit skill invocation");
52
+ if (selected.scope === "repository") {
53
+ if (token !== `$${skillName}`)
54
+ throw new Error("invalid Codex explicit skill invocation");
55
+ let path = request.workspace;
56
+ for (const [index, part] of [
57
+ ".agents",
58
+ "skills",
59
+ skillName,
60
+ "SKILL.md",
61
+ ].entries()) {
62
+ path = join(path, part);
63
+ const entry = await lstat(path).catch(() => null);
64
+ if (
65
+ !entry ||
66
+ entry.isSymbolicLink() ||
67
+ (index === 3
68
+ ? !entry.isFile() || entry.size > 1024 * 1024
69
+ : !entry.isDirectory())
70
+ )
71
+ throw new Error(
72
+ "invoked Codex repository skill is unavailable or unsafe",
73
+ );
74
+ }
75
+ return;
76
+ }
77
+ const { pluginName } = selected;
78
+ if (
79
+ !request.codexMarketplace?.pluginNames.includes(pluginName) ||
80
+ !/^[a-z][a-z0-9-]*$/.test(pluginName) ||
81
+ token !== `$${pluginName}:${skillName}`
82
+ )
83
+ throw new Error("invalid Codex explicit skill invocation");
84
+ }
85
+
86
+ export interface CodexHostOptions {
87
+ binary: string;
88
+ authFile: string;
89
+ model: string;
90
+ effort: string;
91
+ projectRoot: string;
92
+ resultsRoot: string;
93
+ additionalProtectedRoots: string[];
94
+ agentConcurrencyLimit?: number | null;
95
+ timeoutMs?: number;
96
+ /** Integration-test seam; production uses the same CLI for execution and preflight. */
97
+ sandboxBinary?: string;
98
+ }
99
+
100
+ function stopProcess(proc: Bun.Subprocess): void {
101
+ if (process.platform !== "win32") {
102
+ try {
103
+ process.kill(-proc.pid, "SIGKILL");
104
+ return;
105
+ } catch {
106
+ // The group may have exited before cancellation.
107
+ }
108
+ }
109
+ try {
110
+ proc.kill("SIGKILL");
111
+ } catch {
112
+ // The process already exited.
113
+ }
114
+ }
115
+
116
+ function shellQuote(value: string): string {
117
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
118
+ }
119
+
120
+ async function boundedText(
121
+ stream: ReadableStream<Uint8Array>,
122
+ limit: number,
123
+ ): Promise<string> {
124
+ const reader = stream.getReader();
125
+ const chunks: Uint8Array[] = [];
126
+ let size = 0;
127
+ while (true) {
128
+ const { value, done } = await reader.read();
129
+ if (done) break;
130
+ size += value.byteLength;
131
+ if (size > limit)
132
+ throw new Error("Codex event stream exceeds the size limit");
133
+ chunks.push(value);
134
+ }
135
+ return new TextDecoder("utf-8", { fatal: true }).decode(
136
+ Buffer.concat(chunks),
137
+ );
138
+ }
139
+
140
+ async function runProcess(options: {
141
+ argv: string[];
142
+ cwd: string;
143
+ env: Record<string, string>;
144
+ input?: string;
145
+ timeoutMs: number;
146
+ signal?: AbortSignal;
147
+ }): Promise<{ code: number; out: string }> {
148
+ if (options.signal?.aborted) throw new Error("Codex run cancelled");
149
+ const proc = Bun.spawn(options.argv, {
150
+ cwd: options.cwd,
151
+ env: options.env,
152
+ detached: process.platform !== "win32",
153
+ stdin: options.input === undefined ? "ignore" : "pipe",
154
+ stdout: "pipe",
155
+ stderr: "ignore",
156
+ });
157
+ let timer: ReturnType<typeof setTimeout> | undefined;
158
+ let cancel: (() => void) | undefined;
159
+ try {
160
+ const written = (async () => {
161
+ if (options.input === undefined) return;
162
+ if (!proc.stdin || typeof proc.stdin === "number")
163
+ throw new Error("Codex input pipe unavailable");
164
+ await proc.stdin.write(options.input);
165
+ await proc.stdin.end();
166
+ })();
167
+ const completed = Promise.all([
168
+ boundedText(proc.stdout, MAX_EVENT_BYTES),
169
+ proc.exited,
170
+ written,
171
+ ]).then(([out, code]) => ({ out, code }));
172
+ const timeout = new Promise<never>((_resolve, reject) => {
173
+ timer = setTimeout(
174
+ () => reject(new Error("Codex run timed out")),
175
+ options.timeoutMs,
176
+ );
177
+ });
178
+ const aborted = new Promise<never>((_resolve, reject) => {
179
+ if (!options.signal) return;
180
+ cancel = () => reject(new Error("Codex run cancelled"));
181
+ if (options.signal.aborted) cancel();
182
+ else options.signal.addEventListener("abort", cancel, { once: true });
183
+ });
184
+ return await Promise.race([completed, timeout, aborted]);
185
+ } catch (error) {
186
+ stopProcess(proc);
187
+ await proc.exited;
188
+ throw error;
189
+ } finally {
190
+ if (timer) clearTimeout(timer);
191
+ if (cancel) options.signal?.removeEventListener("abort", cancel);
192
+ }
193
+ }
194
+
195
+ async function copyAuth(source: string, target: string): Promise<void> {
196
+ let bytes: Buffer;
197
+ try {
198
+ if ((await stat(source)).size > MAX_AUTH_BYTES)
199
+ throw new Error("Codex auth file exceeds the size limit");
200
+ bytes = await readFile(source);
201
+ } catch {
202
+ throw new Error("Codex auth file is unreadable or oversized");
203
+ }
204
+ if (bytes.byteLength > MAX_AUTH_BYTES)
205
+ throw new Error("Codex auth file exceeds the size limit");
206
+ await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
207
+ }
208
+
209
+ function inside(root: string, path: string): boolean {
210
+ const child = relative(root, path);
211
+ return (
212
+ child === "" ||
213
+ (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
214
+ );
215
+ }
216
+
217
+ async function marketplaceRoot(
218
+ workspace: string,
219
+ declaration: NonNullable<
220
+ Parameters<HostAdapter["run"]>[0]["codexMarketplace"]
221
+ >,
222
+ ): Promise<string> {
223
+ const { artifactRoot, marketplaceName, pluginNames } = declaration;
224
+ const expectedPaths = new Set(declaration.artifactPaths);
225
+ if (
226
+ !/^[a-z][a-z0-9-]*$/.test(marketplaceName) ||
227
+ !pluginNames.length ||
228
+ pluginNames.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) ||
229
+ new Set(pluginNames).size !== pluginNames.length ||
230
+ !declaration.artifactPaths.length ||
231
+ expectedPaths.size !== declaration.artifactPaths.length ||
232
+ declaration.artifactPaths.some(
233
+ (path) =>
234
+ !path.startsWith(`${artifactRoot}/`) ||
235
+ fixtureParts(path).join("/") !== path,
236
+ )
237
+ )
238
+ throw new Error("invalid Codex marketplace declaration");
239
+ const root = join(workspace, ...fixtureParts(artifactRoot));
240
+ const actualWorkspace = await realpath(workspace);
241
+ const actualRoot = await realpath(root);
242
+ if (!inside(actualWorkspace, actualRoot) || actualRoot === actualWorkspace)
243
+ throw new Error("Codex marketplace escapes the workspace");
244
+ let manifest: unknown;
245
+ try {
246
+ manifest = JSON.parse(
247
+ await readFile(
248
+ join(actualRoot, ".claude-plugin", "marketplace.json"),
249
+ "utf8",
250
+ ),
251
+ );
252
+ } catch {
253
+ throw new Error("Codex marketplace manifest is unreadable");
254
+ }
255
+ if (!manifest || typeof manifest !== "object")
256
+ throw new Error("invalid Codex marketplace manifest");
257
+ const document = manifest as Record<string, unknown>;
258
+ const plugins = document.plugins;
259
+ if (
260
+ document.name !== marketplaceName ||
261
+ !Array.isArray(plugins) ||
262
+ plugins.length !== pluginNames.length
263
+ )
264
+ throw new Error("Codex marketplace manifest does not match declaration");
265
+ const found = new Set<string>();
266
+ for (const entry of plugins) {
267
+ if (!entry || typeof entry !== "object")
268
+ throw new Error("invalid Codex marketplace plugin");
269
+ const plugin = entry as Record<string, unknown>;
270
+ if (
271
+ typeof plugin.name !== "string" ||
272
+ !pluginNames.includes(plugin.name) ||
273
+ found.has(plugin.name) ||
274
+ typeof plugin.source !== "string" ||
275
+ !plugin.source.startsWith("./")
276
+ )
277
+ throw new Error(
278
+ "Codex marketplace plugin is not a declared local source",
279
+ );
280
+ found.add(plugin.name);
281
+ const source = join(actualRoot, ...fixtureParts(plugin.source.slice(2)));
282
+ if (!inside(actualRoot, await realpath(source)))
283
+ throw new Error("Codex marketplace plugin escapes its artifact root");
284
+ }
285
+ // The package is materialized from declared artifacts; links can otherwise
286
+ // pull files from outside that snapshot during the local CLI installation.
287
+ const pending = [actualRoot];
288
+ const seen = new Set<string>();
289
+ while (pending.length) {
290
+ const directory = pending.pop()!;
291
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
292
+ if (entry.isSymbolicLink())
293
+ throw new Error("Codex marketplace package contains a symlink");
294
+ if (entry.isDirectory()) pending.push(join(directory, entry.name));
295
+ else if (!entry.isFile())
296
+ throw new Error("Codex marketplace package contains a special file");
297
+ else {
298
+ const path = `${artifactRoot}/${relative(actualRoot, join(directory, entry.name)).split(sep).join("/")}`;
299
+ if (!expectedPaths.has(path))
300
+ throw new Error(
301
+ "Codex marketplace package contains undeclared files",
302
+ );
303
+ seen.add(path);
304
+ }
305
+ }
306
+ }
307
+ if (seen.size !== expectedPaths.size)
308
+ throw new Error("Codex marketplace package is missing declared files");
309
+ return actualRoot;
310
+ }
311
+
312
+ /** Construct one Codex route without inheriting user settings or credentials. */
313
+ export function createCodexHost(options: CodexHostOptions): HostAdapter {
314
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
315
+ const agentConcurrencyLimit = options.agentConcurrencyLimit ?? null;
316
+ if (
317
+ !isAbsolute(options.binary) ||
318
+ !isAbsolute(options.authFile) ||
319
+ !isAbsolute(options.projectRoot) ||
320
+ !isAbsolute(options.resultsRoot) ||
321
+ options.additionalProtectedRoots.some((root) => !isAbsolute(root)) ||
322
+ (options.sandboxBinary !== undefined &&
323
+ !isAbsolute(options.sandboxBinary)) ||
324
+ !options.model ||
325
+ !options.effort ||
326
+ (agentConcurrencyLimit !== null &&
327
+ (!Number.isSafeInteger(agentConcurrencyLimit) ||
328
+ agentConcurrencyLimit < 1)) ||
329
+ !Number.isSafeInteger(timeoutMs) ||
330
+ timeoutMs < 1 ||
331
+ timeoutMs > MAX_TIMEOUT_MS
332
+ )
333
+ throw new Error("invalid Codex host configuration");
334
+ return {
335
+ id: "sevro.host.codex",
336
+ hostCapabilities: [
337
+ "sevro.host.continuation",
338
+ "sevro.codex.plugin-marketplace",
339
+ "sevro.codex.explicit-invocation",
340
+ "sevro.codex.repository-invocation",
341
+ "sevro.codex.native-calls",
342
+ "sevro.host.native-controls",
343
+ "sevro.codex.initial-skill-reads",
344
+ "sevro.codex.follow-up-skill-reads",
345
+ ],
346
+ model: options.model,
347
+ effort: options.effort,
348
+ configuration: {
349
+ "sevro.codex.agent-concurrency-limit": agentConcurrencyLimit,
350
+ },
351
+ async run(request) {
352
+ if (
353
+ request.followUpPrompt !== undefined &&
354
+ (typeof request.followUpPrompt !== "string" ||
355
+ !request.followUpPrompt.trim())
356
+ )
357
+ throw new Error("Codex follow-up prompt must be nonempty");
358
+ if (
359
+ request.fixtureBinDir !== undefined &&
360
+ request.fixtureBinDir !== join(request.workspace, ".git", "fixture-bin")
361
+ )
362
+ throw new Error("fixture binary path is outside the workspace");
363
+ if (request.instrumentation?.length)
364
+ throw new Error("Codex instrumentation is unavailable");
365
+ if (request.condition !== "passive")
366
+ throw new Error("Codex enforcement instrumentation is unavailable");
367
+ await verifyCodexInvocation(request);
368
+ if (existsSync(join(request.workspace, ".codex")))
369
+ throw new Error("fixture Codex configuration is unsupported");
370
+ const stateRoot = await mkdtemp(join(tmpdir(), "sevro-codex-state-"));
371
+ try {
372
+ const codexHome = join(stateRoot, "codex-home");
373
+ const parentHome = join(stateRoot, "home");
374
+ const parentTemp = join(stateRoot, "tmp");
375
+ const commandHome = join(stateRoot, "command-home");
376
+ const commandTemp = join(stateRoot, "command-tmp");
377
+ const pluginCacheRoot = join(codexHome, "plugins", "cache");
378
+ await Promise.all(
379
+ [codexHome, parentHome, parentTemp, commandHome, commandTemp].map(
380
+ (path) => mkdir(path, { mode: 0o700 }),
381
+ ),
382
+ );
383
+ await copyAuth(options.authFile, join(codexHome, "auth.json"));
384
+ const protectedRoots = await evaluationProtectedRoots({
385
+ workspace: request.workspace,
386
+ projectRoot: options.projectRoot,
387
+ resultsRoot: options.resultsRoot,
388
+ additionalRoots: [...options.additionalProtectedRoots, stateRoot],
389
+ });
390
+ const sandboxBinary = options.sandboxBinary ?? options.binary;
391
+ const executableReadRoots = await Promise.all(
392
+ [options.binary, sandboxBinary].flatMap((binary) => [
393
+ realpath(dirname(binary)),
394
+ realpath(binary).then(dirname),
395
+ ]),
396
+ );
397
+ if (
398
+ executableReadRoots.some((readRoot) =>
399
+ protectedRoots.some(
400
+ (protectedRoot) =>
401
+ readRoot === protectedRoot ||
402
+ readRoot.startsWith(`${protectedRoot}${sep}`),
403
+ ),
404
+ )
405
+ )
406
+ throw new Error("Codex executable resides inside a protected root");
407
+ const profileId = `sevro_${randomUUID().replaceAll("-", "")}`;
408
+ await writeFile(
409
+ join(codexHome, "config.toml"),
410
+ codexPermissionProfile({
411
+ id: profileId,
412
+ workspace: request.workspace,
413
+ commandHome,
414
+ commandTemp,
415
+ executableReadRoots,
416
+ ...(request.codexMarketplace
417
+ ? { pluginReadRoot: pluginCacheRoot }
418
+ : {}),
419
+ protectedRoots,
420
+ }) +
421
+ (agentConcurrencyLimit === null
422
+ ? ""
423
+ : `\n[agents]\nmax_concurrent_threads_per_session = ${agentConcurrencyLimit}\n`),
424
+ { flag: "wx", mode: 0o600 },
425
+ );
426
+ const shellRoot = join(request.workspace, ".git", "sevro-shell");
427
+ if (request.fixtureBinDir) {
428
+ await mkdir(shellRoot, { mode: 0o700 });
429
+ await writeFile(
430
+ join(shellRoot, ".zprofile"),
431
+ `export PATH=${shellQuote(request.fixtureBinDir)}:"$PATH"\n`,
432
+ { flag: "wx", mode: 0o600 },
433
+ );
434
+ }
435
+ const env: Record<string, string> = {
436
+ PATH: request.fixtureBinDir
437
+ ? `${request.fixtureBinDir}${delimiter}${process.env.PATH ?? "/usr/bin:/bin"}`
438
+ : (process.env.PATH ?? "/usr/bin:/bin"),
439
+ LANG: process.env.LANG ?? "C",
440
+ HOME: parentHome,
441
+ TMPDIR: parentTemp,
442
+ CODEX_HOME: codexHome,
443
+ NO_COLOR: "1",
444
+ ...(request.fixtureBinDir ? { ZDOTDIR: shellRoot } : {}),
445
+ };
446
+ const installedPluginRoots: string[] = [];
447
+ if (request.codexMarketplace) {
448
+ const root = await marketplaceRoot(
449
+ request.workspace,
450
+ request.codexMarketplace,
451
+ );
452
+ const added = await runProcess({
453
+ argv: [
454
+ options.binary,
455
+ "plugin",
456
+ "marketplace",
457
+ "add",
458
+ root,
459
+ "--json",
460
+ ],
461
+ cwd: request.workspace,
462
+ env,
463
+ timeoutMs: 30_000,
464
+ signal: request.signal,
465
+ });
466
+ if (added.code !== 0)
467
+ throw new Error("Codex local marketplace installation failed");
468
+ for (const pluginName of request.codexMarketplace.pluginNames) {
469
+ const installed = await runProcess({
470
+ argv: [
471
+ options.binary,
472
+ "plugin",
473
+ "add",
474
+ `${pluginName}@${request.codexMarketplace.marketplaceName}`,
475
+ "--json",
476
+ ],
477
+ cwd: request.workspace,
478
+ env,
479
+ timeoutMs: 30_000,
480
+ signal: request.signal,
481
+ });
482
+ if (installed.code !== 0)
483
+ throw new Error("Codex local plugin installation failed");
484
+ let receipt: unknown;
485
+ try {
486
+ receipt = JSON.parse(installed.out);
487
+ } catch {
488
+ throw new Error("Codex plugin installation receipt is invalid");
489
+ }
490
+ const entry = receipt as Record<string, unknown>;
491
+ const actualInstalledPath =
492
+ typeof entry?.installedPath === "string"
493
+ ? await realpath(entry.installedPath)
494
+ : null;
495
+ if (
496
+ !entry ||
497
+ entry.name !== pluginName ||
498
+ entry.marketplaceName !==
499
+ request.codexMarketplace.marketplaceName ||
500
+ !actualInstalledPath ||
501
+ !inside(await realpath(pluginCacheRoot), actualInstalledPath)
502
+ )
503
+ throw new Error("Codex plugin installation receipt is invalid");
504
+ installedPluginRoots.push(actualInstalledPath);
505
+ if (
506
+ request.explicitSkillInvocation?.pluginName === pluginName &&
507
+ !(
508
+ await stat(
509
+ join(
510
+ actualInstalledPath,
511
+ "skills",
512
+ request.explicitSkillInvocation.skillName,
513
+ "SKILL.md",
514
+ ),
515
+ ).catch(() => null)
516
+ )?.isFile()
517
+ )
518
+ throw new Error(
519
+ "invoked Codex skill is absent from the installation",
520
+ );
521
+ }
522
+ }
523
+ const probe = join(commandTemp, "isolation-probe");
524
+ await writeFile(probe, "probe\n", { flag: "wx", mode: 0o600 });
525
+ const checked = await runProcess({
526
+ argv: [
527
+ sandboxBinary,
528
+ "sandbox",
529
+ "-P",
530
+ profileId,
531
+ "-C",
532
+ request.workspace,
533
+ "/bin/sh",
534
+ "-c",
535
+ '/bin/cat "$1" >/dev/null && ! /bin/ls "$2" >/dev/null 2>&1 && ! /bin/cat "$3" >/dev/null 2>&1',
536
+ "sevro-probe",
537
+ probe,
538
+ options.projectRoot,
539
+ join(codexHome, "auth.json"),
540
+ ],
541
+ cwd: request.workspace,
542
+ env,
543
+ timeoutMs: 10_000,
544
+ signal: request.signal,
545
+ });
546
+ if (checked.code !== 0)
547
+ throw new Error("Codex isolation preflight failed");
548
+ const executableCheck = await runProcess({
549
+ argv: [
550
+ sandboxBinary,
551
+ "sandbox",
552
+ "-P",
553
+ profileId,
554
+ "-C",
555
+ request.workspace,
556
+ sandboxBinary,
557
+ "--version",
558
+ ],
559
+ cwd: request.workspace,
560
+ env,
561
+ timeoutMs: 10_000,
562
+ signal: request.signal,
563
+ });
564
+ if (executableCheck.code !== 0)
565
+ throw new Error("Codex executable preflight failed");
566
+ await rm(probe);
567
+ const initialWorkspaceFingerprint = request.followUpPrompt
568
+ ? await workspaceFingerprint(request.workspace)
569
+ : null;
570
+ const executionArgs = [
571
+ options.binary,
572
+ "exec",
573
+ "--json",
574
+ "--strict-config",
575
+ "--skip-git-repo-check",
576
+ ...(request.followUpPrompt ? [] : ["--ephemeral"]),
577
+ "--ignore-rules",
578
+ "-C",
579
+ request.workspace,
580
+ "-m",
581
+ options.model,
582
+ "-c",
583
+ `model_reasoning_effort=${JSON.stringify(options.effort)}`,
584
+ "-c",
585
+ `default_permissions=${JSON.stringify(profileId)}`,
586
+ "-c",
587
+ 'approval_policy="never"',
588
+ ];
589
+ const execution = await runProcess({
590
+ argv: [...executionArgs, "-"],
591
+ cwd: request.workspace,
592
+ env,
593
+ input: request.prompt,
594
+ timeoutMs,
595
+ signal: request.signal,
596
+ });
597
+ const summary = summarizeCodexEvents(execution.out, execution.code);
598
+ if (!summary.complete) throw new Error("Codex turn did not complete");
599
+ let followUp: { out: string; code: number } | null = null;
600
+ let followUpSummary: typeof summary | null = null;
601
+ let continuationObservation:
602
+ | {
603
+ id: string;
604
+ completeness: "complete" | "partial";
605
+ data: Record<string, unknown>;
606
+ }
607
+ | undefined;
608
+ if (request.followUpPrompt) {
609
+ const beforeFollowUp = await workspaceFingerprint(request.workspace);
610
+ const nativeAfterOrdinal = await codexNativeSessionLastOrdinal(
611
+ codexHome,
612
+ summary.threadId,
613
+ );
614
+ const measured =
615
+ initialWorkspaceFingerprint !== null && beforeFollowUp !== null;
616
+ continuationObservation = {
617
+ id: "sevro.codex.continuation",
618
+ completeness: measured ? "complete" : "partial",
619
+ data: {
620
+ method: "same_thread_resume",
621
+ threadId: summary.threadId,
622
+ nativeAfterOrdinal,
623
+ preFollowUpWorktreeUnchanged: measured
624
+ ? initialWorkspaceFingerprint === beforeFollowUp
625
+ : null,
626
+ },
627
+ };
628
+ followUp = await runProcess({
629
+ argv: [
630
+ options.binary,
631
+ "exec",
632
+ "resume",
633
+ "--json",
634
+ "--strict-config",
635
+ "--skip-git-repo-check",
636
+ "--ignore-rules",
637
+ "-m",
638
+ options.model,
639
+ "-c",
640
+ `model_reasoning_effort=${JSON.stringify(options.effort)}`,
641
+ "-c",
642
+ `default_permissions=${JSON.stringify(profileId)}`,
643
+ "-c",
644
+ 'approval_policy="never"',
645
+ summary.threadId,
646
+ "-",
647
+ ],
648
+ cwd: request.workspace,
649
+ env,
650
+ input: request.followUpPrompt,
651
+ timeoutMs,
652
+ signal: request.signal,
653
+ });
654
+ followUpSummary = summarizeCodexEvents(followUp.out, followUp.code);
655
+ if (
656
+ !followUpSummary.complete ||
657
+ followUpSummary.threadId !== summary.threadId
658
+ )
659
+ throw new Error(
660
+ "Codex follow-up turn did not complete in the original thread",
661
+ );
662
+ }
663
+ const observedEvents = followUp
664
+ ? `${execution.out.trimEnd()}\n${followUp.out.trimStart()}`
665
+ : execution.out;
666
+ if (Buffer.byteLength(observedEvents, "utf8") > MAX_EVENT_BYTES)
667
+ throw new Error("Codex combined event stream exceeds the size limit");
668
+ const skillReads = await codexSkillReadObservation(
669
+ observedEvents,
670
+ request.workspace,
671
+ installedPluginRoots,
672
+ );
673
+ const initialSkillReads = followUp
674
+ ? {
675
+ ...(await codexSkillReadObservation(
676
+ execution.out,
677
+ request.workspace,
678
+ installedPluginRoots,
679
+ )),
680
+ id: "sevro.codex.initial-skill-reads",
681
+ }
682
+ : null;
683
+ const followUpSkillReads = followUp
684
+ ? {
685
+ ...(await codexSkillReadObservation(
686
+ followUp.out,
687
+ request.workspace,
688
+ installedPluginRoots,
689
+ )),
690
+ id: "sevro.codex.follow-up-skill-reads",
691
+ }
692
+ : null;
693
+ const nativeCalls = await codexNativeCallObservation(
694
+ codexHome,
695
+ summary.threadId,
696
+ {
697
+ workspace: request.workspace,
698
+ installedPluginRoots,
699
+ ...(request.followUpPrompt
700
+ ? { followUpPrompt: request.followUpPrompt }
701
+ : {}),
702
+ },
703
+ );
704
+ const explicit = request.explicitSkillInvocation;
705
+ const explicitReceipt = explicit
706
+ ? {
707
+ id: "sevro.codex.explicit-invocation",
708
+ completeness: skillReads.completeness,
709
+ data: {
710
+ method: "explicit_invocation",
711
+ primarySkill: explicit.skillName,
712
+ observedSkills: [
713
+ explicit.skillName,
714
+ ...skillReads.data.observedSkills.filter(
715
+ (skill) => skill !== explicit.skillName,
716
+ ),
717
+ ],
718
+ },
719
+ }
720
+ : null;
721
+ return {
722
+ finalMessage: (followUpSummary ?? summary).finalMessage,
723
+ complete: (followUpSummary ?? summary).finalMessage !== null,
724
+ observations: [
725
+ skillReads,
726
+ ...(initialSkillReads ? [initialSkillReads] : []),
727
+ ...(followUpSkillReads ? [followUpSkillReads] : []),
728
+ nativeCalls,
729
+ codexNativeControls(nativeCalls),
730
+ ...(continuationObservation ? [continuationObservation] : []),
731
+ ...(explicitReceipt ? [explicitReceipt] : []),
732
+ ],
733
+ artifacts: [
734
+ {
735
+ id: "sevro.codex.events",
736
+ bytes: Buffer.from(execution.out, "utf8"),
737
+ },
738
+ ...(followUp
739
+ ? [
740
+ {
741
+ id: "sevro.codex.follow-up-events",
742
+ bytes: Buffer.from(followUp.out, "utf8"),
743
+ },
744
+ ]
745
+ : []),
746
+ ],
747
+ actualCondition: "passive" as const,
748
+ inputTokens: followUpSummary
749
+ ? summary.inputTokens !== null &&
750
+ followUpSummary.inputTokens !== null
751
+ ? summary.inputTokens + followUpSummary.inputTokens
752
+ : null
753
+ : summary.inputTokens,
754
+ outputTokens: followUpSummary
755
+ ? summary.outputTokens !== null &&
756
+ followUpSummary.outputTokens !== null
757
+ ? summary.outputTokens + followUpSummary.outputTokens
758
+ : null
759
+ : summary.outputTokens,
760
+ usageComplete:
761
+ summary.usageComplete && (followUpSummary?.usageComplete ?? true),
762
+ costUsd: null,
763
+ };
764
+ } finally {
765
+ await rm(stateRoot, { recursive: true, force: true });
766
+ }
767
+ },
768
+ };
769
+ }