@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.
- package/LICENSE +159 -0
- package/README.md +71 -0
- package/docs/advisory-fixture.md +23 -0
- package/docs/builtin-graders-v1.md +77 -0
- package/docs/cache-ownership-validation.md +49 -0
- package/docs/claude-continuation-validation.md +47 -0
- package/docs/codex-host.md +134 -0
- package/docs/concurrency-validation.md +86 -0
- package/docs/configuration-root-validation.md +70 -0
- package/docs/development-cli.md +327 -0
- package/docs/engine-slice.md +114 -0
- package/docs/extension-protocol-v1.md +292 -0
- package/docs/fixture-cleanup-validation.md +70 -0
- package/docs/identity-v1.md +65 -0
- package/docs/native-controls.md +29 -0
- package/docs/releases.md +88 -0
- package/docs/report-v1.md +32 -0
- package/docs/results-v1.md +118 -0
- package/examples/basic/graded.json +13 -0
- package/examples/basic/host.ts +8 -0
- package/examples/basic/prompt-only.json +7 -0
- package/package.json +45 -0
- package/schemas/cli-result-v1.schema.json +184 -0
- package/schemas/extension-v1.schema.json +659 -0
- package/schemas/report-v1.schema.json +164 -0
- package/schemas/run-evidence-v1.schema.json +510 -0
- package/src/advisory-fixture.ts +282 -0
- package/src/advisory.ts +105 -0
- package/src/cli.ts +837 -0
- package/src/engine.ts +2341 -0
- package/src/extension-client.ts +257 -0
- package/src/extension-session.ts +335 -0
- package/src/fixture-bin.ts +87 -0
- package/src/fixture-cleanup.ts +38 -0
- package/src/fixture-setup.ts +163 -0
- package/src/generated-fixture.ts +220 -0
- package/src/git-hooks.ts +63 -0
- package/src/graders/git-head.ts +176 -0
- package/src/graders/output.ts +359 -0
- package/src/graders/semantic.ts +120 -0
- package/src/graders/shell.ts +277 -0
- package/src/hosts/claude-continuation.ts +94 -0
- package/src/hosts/claude-credential.ts +73 -0
- package/src/hosts/claude-events.ts +128 -0
- package/src/hosts/claude-nested-skills.ts +218 -0
- package/src/hosts/claude-repository-invocation.ts +239 -0
- package/src/hosts/claude-settings.ts +50 -0
- package/src/hosts/claude-tool-calls.ts +159 -0
- package/src/hosts/claude.ts +511 -0
- package/src/hosts/codex-configuration.ts +85 -0
- package/src/hosts/codex-events.ts +102 -0
- package/src/hosts/codex-native-calls.ts +702 -0
- package/src/hosts/codex-profile.ts +78 -0
- package/src/hosts/codex-skill-reads.ts +345 -0
- package/src/hosts/codex.ts +769 -0
- package/src/hosts/isolation-roots.ts +60 -0
- package/src/hosts/mac-sandbox.ts +108 -0
- package/src/hosts/native-controls.ts +124 -0
- package/src/hosts/protected-worktrees.ts +56 -0
- package/src/hosts/workspace-fingerprint.ts +55 -0
- package/src/identity.ts +156 -0
- package/src/instrumentation.ts +106 -0
- package/src/preparation.ts +211 -0
- package/src/provenance.ts +244 -0
- package/src/report.ts +351 -0
- package/src/repository-fixture.ts +286 -0
- package/src/results.ts +160 -0
- package/src/run-owner.ts +276 -0
- package/src/schema.ts +28 -0
- package/src/storage.ts +23 -0
- 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
|
+
}
|