@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21
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/CONTRIBUTING.md +170 -0
- package/LICENSE +63 -136
- package/README.md +83 -66
- package/docs/README.md +44 -0
- package/docs/acknowledgements.md +14 -0
- package/docs/architecture.md +146 -0
- package/docs/assets/README.md +27 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +1 -0
- package/docs/assets/sevro-wordmark.svg +1 -0
- package/docs/builtin-graders-v1.md +41 -13
- package/docs/codex-host.md +184 -52
- package/docs/contracts.md +19 -0
- package/docs/creating-cases.md +139 -0
- package/docs/development-cli.md +190 -30
- package/docs/documentation-quality.md +61 -0
- package/docs/evaluation-workflow.md +110 -0
- package/docs/evidence/code-mode-controls-71.md +64 -0
- package/docs/evidence/code-mode-programs-85.md +64 -0
- package/docs/evidence/codex-state-73/README.md +204 -0
- package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
- package/docs/evidence/codex-state-73/results.json +661 -0
- package/docs/evidence/local-http-74.md +104 -0
- package/docs/evidence/runtime-startup-30/README.md +185 -0
- package/docs/evidence/runtime-startup-30/after.json +319 -0
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
- package/docs/evidence/runtime-startup-30/before.json +403 -0
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
- package/docs/evidence/skill-reads-70/results.json +823 -0
- package/docs/evidence.md +21 -0
- package/docs/extension-protocol-v1.md +50 -16
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/getting-started.md +102 -0
- package/docs/guide-evaluation.md +89 -0
- package/docs/guide-verification.json +239 -0
- package/docs/identity-v1.md +35 -1
- package/docs/installing.md +88 -0
- package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
- package/docs/licensing.md +42 -0
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +228 -0
- package/docs/reading-results.md +141 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +81 -40
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +180 -0
- package/docs/run-settings-v1.md +147 -0
- package/docs/running-evaluations.md +31 -0
- package/docs/runtime-v1.md +614 -0
- package/docs/specs/repository-guide.md +58 -0
- package/docs/troubleshooting.md +30 -0
- package/docs/typescript-quality.md +234 -0
- package/docs/usability-study.md +114 -0
- package/examples/basic/host.ts +2 -2
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +39 -6
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +193 -0
- package/src/advisory.ts +10 -5
- package/src/candidate-artifacts.ts +208 -0
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +105 -0
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +81 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +59 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +222 -0
- package/src/cli/hosts.ts +59 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/cli/invocation.ts +627 -0
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +163 -0
- package/src/cli/presentation.ts +91 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +133 -0
- package/src/cli/run-options.ts +101 -0
- package/src/cli/run-overrides.ts +83 -0
- package/src/cli/settings.ts +160 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -843
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +19 -2373
- package/src/evaluation/advisory.ts +150 -0
- package/src/evaluation/check-plan.ts +510 -0
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/evaluation/grading-host.ts +39 -0
- package/src/evaluation/grading.ts +258 -0
- package/src/evaluation/invocation-plan.ts +548 -0
- package/src/evaluation/plan.ts +736 -0
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
- package/src/evaluation/semantic.ts +312 -0
- package/src/evaluation/trial.ts +866 -0
- package/src/evaluation/types.ts +207 -0
- package/src/evidence/host-results.ts +92 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +228 -148
- package/src/extension-session.ts +326 -195
- package/src/fixtures/advisory.ts +338 -0
- package/src/fixtures/bin.ts +57 -0
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +187 -0
- package/src/fixtures/generated.ts +300 -0
- package/src/fixtures/git-hooks.ts +24 -0
- package/src/fixtures/preparation.ts +270 -0
- package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
- package/src/fixtures/setup.ts +224 -0
- package/src/fixtures/tools.ts +88 -0
- package/src/fixtures/workspace-state.ts +212 -0
- package/src/generated/cli-result.cjs +2 -0
- package/src/generated/cli-result.d.cts +3 -0
- package/src/generated/extension.cjs +2 -0
- package/src/generated/extension.d.cts +3 -0
- package/src/generated/report.cjs +2 -0
- package/src/generated/report.d.cts +3 -0
- package/src/generated/run-evidence.cjs +2 -0
- package/src/generated/run-evidence.d.cts +3 -0
- package/src/generated/runtime.cjs +2 -0
- package/src/generated/runtime.d.cts +3 -0
- package/src/graders/artifact.ts +56 -23
- package/src/graders/git-head.ts +79 -75
- package/src/graders/output.ts +222 -140
- package/src/graders/semantic.ts +102 -43
- package/src/graders/shell.ts +379 -207
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +319 -0
- package/src/hosts/claude-continuation.ts +63 -18
- package/src/hosts/claude-credential.ts +43 -62
- package/src/hosts/claude-events.ts +86 -51
- package/src/hosts/claude-keychain.ts +59 -0
- package/src/hosts/claude-native-goal.ts +57 -33
- package/src/hosts/claude-process.ts +111 -0
- package/src/hosts/claude-repository-invocation.ts +208 -117
- package/src/hosts/claude-settings.ts +240 -21
- package/src/hosts/claude-skill-activations.ts +166 -0
- package/src/hosts/claude-state.ts +403 -0
- package/src/hosts/claude-tool-calls.ts +171 -106
- package/src/hosts/claude.ts +561 -451
- package/src/hosts/codex-app-server.ts +326 -178
- package/src/hosts/codex-configuration.ts +44 -18
- package/src/hosts/codex-events.ts +97 -60
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-invocation.ts +70 -0
- package/src/hosts/codex-marketplace.ts +283 -0
- package/src/hosts/codex-native-calls.ts +546 -500
- package/src/hosts/codex-process.ts +202 -0
- package/src/hosts/codex-profile.ts +257 -28
- package/src/hosts/codex-session-files.ts +106 -0
- package/src/hosts/codex-state.ts +492 -0
- package/src/hosts/codex.ts +783 -832
- package/src/hosts/isolation-common.ts +94 -0
- package/src/hosts/isolation-roots.ts +54 -27
- package/src/hosts/isolation.ts +10 -0
- package/src/hosts/linux-sandbox.ts +159 -0
- package/src/hosts/mac-sandbox.ts +187 -61
- package/src/hosts/native-controls.ts +152 -62
- package/src/hosts/process-lifecycle.ts +37 -0
- package/src/hosts/protected-worktrees.ts +51 -19
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +580 -0
- package/src/hosts/workspace-fingerprint.ts +55 -35
- package/src/identity.ts +102 -48
- package/src/instrumentation.ts +109 -48
- package/src/native-transcript-state.ts +131 -0
- package/src/native-transcript-view.ts +90 -0
- package/src/native-transcripts.ts +286 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +161 -81
- package/src/prune.ts +215 -0
- package/src/report.ts +172 -140
- package/src/resolved-case.ts +33 -0
- package/src/results-lock.ts +29 -0
- package/src/results.ts +133 -86
- package/src/run-owner.ts +154 -63
- package/src/runtime/config.ts +456 -0
- package/src/runtime/discovery-apple.ts +90 -0
- package/src/runtime/discovery-providers.ts +63 -0
- package/src/runtime/discovery.ts +70 -0
- package/src/runtime/hook-processes.ts +97 -0
- package/src/runtime/hooks.ts +476 -0
- package/src/runtime/paths.ts +313 -0
- package/src/runtime/query.ts +85 -0
- package/src/runtime/seeds.ts +134 -0
- package/src/runtime/shared-cache.ts +143 -0
- package/src/runtime/state.ts +143 -0
- package/src/schema-types.ts +198 -0
- package/src/schema.ts +15 -19
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +20 -0
- package/src/advisory-fixture.ts +0 -282
- package/src/fixture-bin.ts +0 -87
- package/src/fixture-setup.ts +0 -163
- package/src/generated-fixture.ts +0 -220
- package/src/git-hooks.ts +0 -63
- package/src/hosts/claude-nested-skills.ts +0 -218
- package/src/hosts/codex-command-output.ts +0 -399
- package/src/hosts/codex-executor-command.ts +0 -195
- package/src/hosts/codex-skill-reads.ts +0 -391
- package/src/preparation.ts +0 -211
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { lstat, readdir, realpath, rm } from "node:fs/promises";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
allocateCodexStateLease,
|
|
5
|
+
candidateCodexArtifactBinding,
|
|
6
|
+
candidateCodexArtifactRoots,
|
|
7
|
+
codexStateEnabled,
|
|
8
|
+
releaseCandidateCodexState,
|
|
9
|
+
requireUnreservedArtifactName,
|
|
10
|
+
} from "./candidate-codex-state";
|
|
11
|
+
import {
|
|
12
|
+
allocateNativeState,
|
|
13
|
+
nativeStatePath,
|
|
14
|
+
} from "./native-transcript-state";
|
|
15
|
+
import type { RuntimePolicy, RuntimeRole } from "./runtime/config";
|
|
16
|
+
import { clearFixtureContents } from "./fixtures/cleanup";
|
|
17
|
+
|
|
18
|
+
export interface RuntimeArtifactDirectory {
|
|
19
|
+
name: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CandidateArtifactRoots = Record<string, string>;
|
|
24
|
+
|
|
25
|
+
const leases = new Map<string, RuntimeArtifactDirectory[]>();
|
|
26
|
+
const MAX_ENTRIES = 16_384;
|
|
27
|
+
|
|
28
|
+
/** These are relative candidate-home paths, never operator filesystem grants. */
|
|
29
|
+
export function requireArtifactDirectories(
|
|
30
|
+
directories: RuntimeArtifactDirectory[],
|
|
31
|
+
): void {
|
|
32
|
+
if (!Array.isArray(directories) || directories.length > 16)
|
|
33
|
+
throw new Error("runtime artifact directories exceed the limit");
|
|
34
|
+
const names = new Set<string>();
|
|
35
|
+
for (const directory of directories) {
|
|
36
|
+
requireArtifactDirectory(directory);
|
|
37
|
+
if (names.has(directory.name))
|
|
38
|
+
throw new Error("runtime artifact directory names overlap");
|
|
39
|
+
names.add(directory.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireArtifactDirectory(directory: RuntimeArtifactDirectory): void {
|
|
44
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(directory.name))
|
|
45
|
+
throw new Error("runtime artifact directory name is invalid");
|
|
46
|
+
requireUnreservedArtifactName(directory.name);
|
|
47
|
+
const path = directory.path;
|
|
48
|
+
if (!validArtifactPath(path))
|
|
49
|
+
throw new Error("runtime artifact directory path is invalid");
|
|
50
|
+
if (path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
51
|
+
throw new Error(
|
|
52
|
+
"runtime artifact directory must be a bounded relative path",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validArtifactPath(path: string): boolean {
|
|
57
|
+
if (typeof path !== "string" || path.length > 4096 || path.includes("\\"))
|
|
58
|
+
return false;
|
|
59
|
+
return !Array.from(path).some(
|
|
60
|
+
(character) =>
|
|
61
|
+
character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Allocate before host execution so exceptions cannot lose cleanup ownership. */
|
|
66
|
+
export async function allocateCandidateRuntime(
|
|
67
|
+
policy: RuntimePolicy | undefined,
|
|
68
|
+
retainHome = false,
|
|
69
|
+
): Promise<string | undefined> {
|
|
70
|
+
const directories = policy?.artifactDirectories ?? [];
|
|
71
|
+
const codexState = codexStateEnabled(policy);
|
|
72
|
+
if (!candidateRuntimeRequired(directories, codexState, retainHome))
|
|
73
|
+
return undefined;
|
|
74
|
+
requireArtifactDirectories(directories);
|
|
75
|
+
const root = await allocateNativeState("candidate-runtime-");
|
|
76
|
+
leases.set(root, directories);
|
|
77
|
+
allocateCodexStateLease(root, codexState);
|
|
78
|
+
return root;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function candidateRuntimeRequired(
|
|
82
|
+
directories: RuntimeArtifactDirectory[],
|
|
83
|
+
codexState: boolean,
|
|
84
|
+
retainHome: boolean,
|
|
85
|
+
): boolean {
|
|
86
|
+
return directories.length > 0 || codexState || retainHome;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function requireCandidateRuntime(
|
|
90
|
+
root: string | undefined,
|
|
91
|
+
role?: RuntimeRole,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
if (root === undefined) return;
|
|
94
|
+
requireCandidateRole(role);
|
|
95
|
+
if (!leases.has(root) || (await realpath(root)) !== root)
|
|
96
|
+
throw new Error("candidate runtime must be owned by the current trial");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requireCandidateRole(role: RuntimeRole | undefined): void {
|
|
100
|
+
if (role !== undefined && role !== "candidate")
|
|
101
|
+
throw new Error(
|
|
102
|
+
"retained candidate runtime cannot be used by another role",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function candidateArtifactRoots(
|
|
107
|
+
root: string | undefined,
|
|
108
|
+
): Promise<CandidateArtifactRoots | undefined> {
|
|
109
|
+
if (root === undefined) return undefined;
|
|
110
|
+
const directories = leases.get(root);
|
|
111
|
+
if (!directories) throw new Error("candidate runtime lease is unavailable");
|
|
112
|
+
const roots = Object.fromEntries(
|
|
113
|
+
directories.map(({ name, path }) => [name, join(root, "home", path)]),
|
|
114
|
+
);
|
|
115
|
+
Object.assign(roots, candidateCodexArtifactRoots(root));
|
|
116
|
+
await requireCandidateArtifactRoots(roots);
|
|
117
|
+
return roots;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Validate the engine binding and refuse candidate-created filesystem aliases. */
|
|
121
|
+
export async function requireCandidateArtifactRoots(
|
|
122
|
+
roots: CandidateArtifactRoots | undefined,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
for (const [name, path] of Object.entries(roots ?? {})) {
|
|
125
|
+
const binding = artifactRootBinding(name, path);
|
|
126
|
+
await requireArtifactAncestors(binding.root, binding.directoryPath);
|
|
127
|
+
if (binding.file && !(await lstat(path)).isFile())
|
|
128
|
+
throw new Error(
|
|
129
|
+
"candidate Codex configuration must remain a regular file",
|
|
130
|
+
);
|
|
131
|
+
await requireArtifactTree(path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function artifactRootBinding(name: string, path: string) {
|
|
136
|
+
const codex = candidateCodexArtifactBinding(name, path);
|
|
137
|
+
if (codex) return codex;
|
|
138
|
+
return homeArtifactBinding(name, path);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function homeArtifactBinding(name: string, path: string) {
|
|
142
|
+
const parts = relative(nativeStatePath(), path).split(sep);
|
|
143
|
+
const root = join(nativeStatePath(), parts[0] ?? "");
|
|
144
|
+
const directory = leases.get(root)?.find((item) => item.name === name);
|
|
145
|
+
if (!directory || path !== join(root, "home", directory.path))
|
|
146
|
+
throw new Error("candidate artifact grant is not bound to an active trial");
|
|
147
|
+
return { root, directoryPath: join("home", directory.path), file: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function requireArtifactAncestors(
|
|
151
|
+
root: string,
|
|
152
|
+
path: string,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
let current = root;
|
|
155
|
+
for (const part of ["", ...path.split(sep)]) {
|
|
156
|
+
current = join(current, part);
|
|
157
|
+
const info = await lstat(current);
|
|
158
|
+
if (!info.isDirectory() || (await realpath(current)) !== current)
|
|
159
|
+
throw new Error(
|
|
160
|
+
"candidate artifact ancestors must be canonical directories",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function requireArtifactTree(root: string): Promise<void> {
|
|
166
|
+
const pending = [root];
|
|
167
|
+
for (const path of pending) {
|
|
168
|
+
if (pending.length > MAX_ENTRIES)
|
|
169
|
+
throw new Error("candidate artifact tree exceeds the entry limit");
|
|
170
|
+
pending.push(...(await artifactChildren(path)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function artifactChildren(path: string): Promise<string[]> {
|
|
175
|
+
const info = await lstat(path);
|
|
176
|
+
if (info.isDirectory())
|
|
177
|
+
return (await readdir(path)).map((name) => join(path, name));
|
|
178
|
+
if (!info.isFile() || info.nlink !== 1)
|
|
179
|
+
throw new Error("candidate artifact tree contains a link or special file");
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function candidateArtifactEnvironment(
|
|
184
|
+
roots: CandidateArtifactRoots | undefined,
|
|
185
|
+
): Record<string, string> {
|
|
186
|
+
return Object.fromEntries(
|
|
187
|
+
Object.entries(roots ?? {}).map(([name, path]) => [
|
|
188
|
+
`SEVRO_CANDIDATE_ARTIFACT_${name.toUpperCase()}`,
|
|
189
|
+
path,
|
|
190
|
+
]),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function candidateArtifactReadRoots(
|
|
195
|
+
roots: CandidateArtifactRoots | undefined,
|
|
196
|
+
): string[] {
|
|
197
|
+
return Object.values(roots ?? {});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function releaseCandidateRuntime(
|
|
201
|
+
root: string | undefined,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
if (root === undefined) return;
|
|
204
|
+
await releaseCandidateCodexState(root);
|
|
205
|
+
await clearFixtureContents(root);
|
|
206
|
+
await rm(root, { recursive: true, force: true });
|
|
207
|
+
leases.delete(root);
|
|
208
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { lstat, realpath, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
4
|
+
import { nativeStatePath } from "./native-transcript-state";
|
|
5
|
+
import { clearFixtureContents } from "./fixtures/cleanup";
|
|
6
|
+
import type { RuntimePolicy } from "./runtime/config";
|
|
7
|
+
|
|
8
|
+
interface CodexPaths {
|
|
9
|
+
codexHome: string;
|
|
10
|
+
parentHome: string;
|
|
11
|
+
commandHome: string;
|
|
12
|
+
}
|
|
13
|
+
interface ArtifactBinding {
|
|
14
|
+
root: string;
|
|
15
|
+
path: string;
|
|
16
|
+
directoryPath: string;
|
|
17
|
+
file?: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface CodexLease {
|
|
20
|
+
state?: { stateRoot: string; artifacts: Record<string, ArtifactBinding> };
|
|
21
|
+
}
|
|
22
|
+
const leases = new Map<string, CodexLease>();
|
|
23
|
+
const ARTIFACT_NAMES = [
|
|
24
|
+
"codex_config",
|
|
25
|
+
"codex_skills",
|
|
26
|
+
"agent_skills",
|
|
27
|
+
"command_agent_skills",
|
|
28
|
+
"command_codex_skills",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function codexStateEnabled(policy: RuntimePolicy | undefined): boolean {
|
|
32
|
+
return policy?.codexState === true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function requireUnreservedArtifactName(name: string): void {
|
|
36
|
+
if (ARTIFACT_NAMES.includes(name))
|
|
37
|
+
throw new Error(
|
|
38
|
+
"runtime artifact directory name is reserved for Codex state",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function allocateCodexStateLease(root: string, enabled: boolean): void {
|
|
43
|
+
if (enabled) leases.set(root, {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Bind the actual host state to one trial before execution; never substitute a grader home. */
|
|
47
|
+
export async function retainCandidateCodexState(
|
|
48
|
+
runtimeRoot: string | undefined,
|
|
49
|
+
stateRoot: string,
|
|
50
|
+
paths: CodexPaths,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
if (runtimeRoot === undefined) return;
|
|
53
|
+
const lease = leases.get(runtimeRoot);
|
|
54
|
+
if (!lease) return;
|
|
55
|
+
if (lease.state)
|
|
56
|
+
throw new Error(
|
|
57
|
+
"candidate Codex state is already bound to a native request",
|
|
58
|
+
);
|
|
59
|
+
requireCodexLayout(stateRoot, runtimeRoot, paths);
|
|
60
|
+
await requirePrivateState(stateRoot);
|
|
61
|
+
await requirePrivateState(paths.codexHome);
|
|
62
|
+
lease.state = {
|
|
63
|
+
stateRoot,
|
|
64
|
+
artifacts: codexArtifacts(stateRoot, runtimeRoot, paths),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requireCodexLayout(
|
|
69
|
+
stateRoot: string,
|
|
70
|
+
runtimeRoot: string,
|
|
71
|
+
paths: CodexPaths,
|
|
72
|
+
): void {
|
|
73
|
+
if (
|
|
74
|
+
dirname(stateRoot) !== nativeStatePath() ||
|
|
75
|
+
!basename(stateRoot).startsWith("codex-")
|
|
76
|
+
)
|
|
77
|
+
throw new Error("candidate Codex state must be a native request directory");
|
|
78
|
+
requireCodexHomes(stateRoot, runtimeRoot, paths);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireCodexHomes(
|
|
82
|
+
stateRoot: string,
|
|
83
|
+
runtimeRoot: string,
|
|
84
|
+
paths: CodexPaths,
|
|
85
|
+
): void {
|
|
86
|
+
if (
|
|
87
|
+
paths.codexHome !== join(stateRoot, "codex-home") ||
|
|
88
|
+
paths.parentHome !== join(stateRoot, "home") ||
|
|
89
|
+
paths.commandHome !== join(runtimeRoot, "home")
|
|
90
|
+
)
|
|
91
|
+
throw new Error("candidate Codex homes must belong to the current trial");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function requirePrivateState(root: string): Promise<void> {
|
|
95
|
+
const info = await lstat(root);
|
|
96
|
+
if (
|
|
97
|
+
!info.isDirectory() ||
|
|
98
|
+
info.uid !== userInfo().uid ||
|
|
99
|
+
(info.mode & 0o077) !== 0 ||
|
|
100
|
+
(await realpath(root)) !== root
|
|
101
|
+
)
|
|
102
|
+
throw new Error(
|
|
103
|
+
"candidate Codex state must be owned, private and canonical",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function binding(root: string, path: string, file = false): ArtifactBinding {
|
|
108
|
+
return {
|
|
109
|
+
root,
|
|
110
|
+
path,
|
|
111
|
+
directoryPath: relative(root, file ? dirname(path) : path),
|
|
112
|
+
file,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function codexArtifacts(
|
|
117
|
+
stateRoot: string,
|
|
118
|
+
runtimeRoot: string,
|
|
119
|
+
paths: CodexPaths,
|
|
120
|
+
): Record<string, ArtifactBinding> {
|
|
121
|
+
return {
|
|
122
|
+
codex_config: binding(
|
|
123
|
+
stateRoot,
|
|
124
|
+
join(paths.codexHome, "config.toml"),
|
|
125
|
+
true,
|
|
126
|
+
),
|
|
127
|
+
codex_skills: binding(stateRoot, join(paths.codexHome, "skills")),
|
|
128
|
+
agent_skills: binding(
|
|
129
|
+
stateRoot,
|
|
130
|
+
join(paths.parentHome, ".agents", "skills"),
|
|
131
|
+
),
|
|
132
|
+
command_agent_skills: binding(
|
|
133
|
+
runtimeRoot,
|
|
134
|
+
join(paths.commandHome, ".agents", "skills"),
|
|
135
|
+
),
|
|
136
|
+
command_codex_skills: binding(
|
|
137
|
+
runtimeRoot,
|
|
138
|
+
join(paths.commandHome, ".codex", "skills"),
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function candidateCodexArtifactRoots(
|
|
144
|
+
root: string,
|
|
145
|
+
): Record<string, string> {
|
|
146
|
+
const lease = leases.get(root);
|
|
147
|
+
if (!lease) return {};
|
|
148
|
+
if (!lease.state) throw new Error("candidate Codex state is unavailable");
|
|
149
|
+
return Object.fromEntries(
|
|
150
|
+
Object.entries(lease.state.artifacts).map(([name, item]) => [
|
|
151
|
+
name,
|
|
152
|
+
item.path,
|
|
153
|
+
]),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function candidateCodexArtifactBinding(
|
|
158
|
+
name: string,
|
|
159
|
+
path: string,
|
|
160
|
+
): ArtifactBinding | undefined {
|
|
161
|
+
for (const lease of leases.values()) {
|
|
162
|
+
const artifact = lease.state?.artifacts[name];
|
|
163
|
+
if (artifact?.path === path) return artifact;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function candidateCodexStateRetained(
|
|
169
|
+
runtimeRoot: string | undefined,
|
|
170
|
+
stateRoot: string,
|
|
171
|
+
): boolean {
|
|
172
|
+
return (
|
|
173
|
+
runtimeRoot !== undefined &&
|
|
174
|
+
leases.get(runtimeRoot)?.state?.stateRoot === stateRoot
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function releaseCandidateCodexState(root: string): Promise<void> {
|
|
179
|
+
const state = leases.get(root)?.state;
|
|
180
|
+
if (state) {
|
|
181
|
+
await clearFixtureContents(state.stateRoot);
|
|
182
|
+
await rm(state.stateRoot, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
leases.delete(root);
|
|
185
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type ResolvedCase } from "./engine";
|
|
2
|
+
import { prepareGeneratedFixture } from "./fixtures/generated";
|
|
3
|
+
import { prepareRepositoryFixture } from "./fixtures/repository";
|
|
4
|
+
import { isOptionalNonblankString, isRecord } from "./value-guards";
|
|
5
|
+
|
|
6
|
+
function fixtureDeclarationIssue(
|
|
7
|
+
value: Record<string, unknown>,
|
|
8
|
+
): string | undefined {
|
|
9
|
+
try {
|
|
10
|
+
if (value.kind === "generated") prepareGeneratedFixture(value);
|
|
11
|
+
else prepareRepositoryFixture({ kind: "repository", ...value });
|
|
12
|
+
return undefined;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const detail =
|
|
15
|
+
error instanceof Error ? error.message : "invalid declaration";
|
|
16
|
+
return `fixture: ${detail}; correct the fixture declaration`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validInlineFixture(value: Record<string, unknown>): boolean {
|
|
21
|
+
return (
|
|
22
|
+
!Object.hasOwn(value, "sourceRef") &&
|
|
23
|
+
Object.hasOwn(value, "files") &&
|
|
24
|
+
isRecord(value.files) &&
|
|
25
|
+
Object.values(value.files).every((content) => typeof content === "string")
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function inlineFixtureIssue(
|
|
30
|
+
value: Record<string, unknown>,
|
|
31
|
+
): string | undefined {
|
|
32
|
+
if (!Object.hasOwn(value, "files") || !isRecord(value.files))
|
|
33
|
+
return "fixture.files: expected an object mapping file paths to string contents";
|
|
34
|
+
const entry = Object.entries(value.files).find(
|
|
35
|
+
([, content]) => typeof content !== "string",
|
|
36
|
+
);
|
|
37
|
+
return entry
|
|
38
|
+
? `fixture.files[${JSON.stringify(entry[0])}]: expected string contents`
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fixtureIssue(value: unknown): string | undefined {
|
|
43
|
+
if (!isRecord(value)) return "fixture: expected an object";
|
|
44
|
+
if (value.kind === "generated") return fixtureDeclarationIssue(value);
|
|
45
|
+
if (value.kind !== undefined)
|
|
46
|
+
return "fixture.kind: omit for inline/repository fixtures, or use generated";
|
|
47
|
+
if (Object.hasOwn(value, "sourceRef")) return fixtureDeclarationIssue(value);
|
|
48
|
+
return inlineFixtureIssue(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nonemptyString(value: unknown): value is string {
|
|
52
|
+
return typeof value === "string" && value.length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function caseDetailsIssue(value: Record<string, unknown>): string | undefined {
|
|
56
|
+
for (const field of ["id", "prompt"])
|
|
57
|
+
if (!nonemptyString(value[field]))
|
|
58
|
+
return `${field}: expected a nonempty string`;
|
|
59
|
+
if (!isOptionalNonblankString(value.followUpPrompt))
|
|
60
|
+
return "followUpPrompt: omit or supply a nonblank string";
|
|
61
|
+
return fixtureIssue(value.fixture);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkIssue(value: unknown, location: string): string | undefined {
|
|
65
|
+
if (!isRecord(value)) return `${location}: expected an object`;
|
|
66
|
+
for (const field of ["id", "grader"])
|
|
67
|
+
if (typeof value[field] !== "string")
|
|
68
|
+
return `${location}.${field}: expected a string`;
|
|
69
|
+
if (!isRecord(value.configuration))
|
|
70
|
+
return `${location}.configuration: expected an object`;
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checksIssue(value: unknown): string | undefined {
|
|
75
|
+
if (!Array.isArray(value))
|
|
76
|
+
return "checks: expected an array of check objects";
|
|
77
|
+
for (const [index, check] of value.entries()) {
|
|
78
|
+
const issue = checkIssue(check, `checks[${index}]`);
|
|
79
|
+
if (issue) return issue;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function requiredEvidenceIssue(value: unknown): string | undefined {
|
|
85
|
+
if (!Array.isArray(value))
|
|
86
|
+
return "requiredEvidence: expected an array of strings";
|
|
87
|
+
const index = value.findIndex((entry: unknown) => typeof entry !== "string");
|
|
88
|
+
return index < 0
|
|
89
|
+
? undefined
|
|
90
|
+
: `requiredEvidence[${index}]: expected a string`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Explain the first invalid field using the same checks as the case type guard. */
|
|
94
|
+
export function resolvedCaseIssue(value: unknown): string | undefined {
|
|
95
|
+
if (!isRecord(value)) return "case: expected an object";
|
|
96
|
+
return (
|
|
97
|
+
caseDetailsIssue(value) ??
|
|
98
|
+
checksIssue(value.checks) ??
|
|
99
|
+
requiredEvidenceIssue(value.requiredEvidence)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isResolvedCase(value: unknown): value is ResolvedCase {
|
|
104
|
+
return resolvedCaseIssue(value) === undefined;
|
|
105
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class CommandCancellation {
|
|
2
|
+
private readonly controller = new AbortController();
|
|
3
|
+
private readonly interrupt = () => {
|
|
4
|
+
this.controller.abort("SIGINT");
|
|
5
|
+
};
|
|
6
|
+
private readonly terminate = () => {
|
|
7
|
+
this.controller.abort("SIGTERM");
|
|
8
|
+
};
|
|
9
|
+
get signal(): AbortSignal {
|
|
10
|
+
return this.controller.signal;
|
|
11
|
+
}
|
|
12
|
+
constructor() {
|
|
13
|
+
process.on("SIGINT", this.interrupt);
|
|
14
|
+
process.on("SIGTERM", this.terminate);
|
|
15
|
+
}
|
|
16
|
+
dispose(): void {
|
|
17
|
+
process.off("SIGINT", this.interrupt);
|
|
18
|
+
process.off("SIGTERM", this.terminate);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { prepareRunInvocation, type PreparedInvocation } from "./preparation";
|
|
2
|
+
import { evaluateInvocation } from "./evaluation";
|
|
3
|
+
import { CommandCancellation } from "./cancellation";
|
|
4
|
+
import { display, displayFailure, runnerFailureCategory } from "./presentation";
|
|
5
|
+
import { displayEffectiveSettings } from "./settings";
|
|
6
|
+
import { jsonOutputRequested } from "./run-options";
|
|
7
|
+
import { retainCliResult } from "./completion";
|
|
8
|
+
|
|
9
|
+
type CommandRelease = (() => Promise<void>) | undefined;
|
|
10
|
+
|
|
11
|
+
export async function main(
|
|
12
|
+
argv: string[],
|
|
13
|
+
release?: () => Promise<void>,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const json = jsonOutputRequested(argv);
|
|
16
|
+
let prepared: PreparedInvocation;
|
|
17
|
+
try {
|
|
18
|
+
prepared = await prepareRunInvocation(argv);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
await releaseBeforeDisplay(release, 64);
|
|
21
|
+
displayFailure(error, json, 64, "invalid invocation");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const cancellation = new CommandCancellation();
|
|
25
|
+
try {
|
|
26
|
+
displayEffectiveSettings(prepared);
|
|
27
|
+
const { result } = await evaluateInvocation(prepared, cancellation.signal);
|
|
28
|
+
const completion = await retainCompletion(
|
|
29
|
+
result,
|
|
30
|
+
prepared.invocation.resultsRoot,
|
|
31
|
+
);
|
|
32
|
+
await releaseBeforeDisplay(release, result.exitCode);
|
|
33
|
+
display(result, prepared.invocation.json, completion);
|
|
34
|
+
process.exitCode = result.exitCode;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
await releaseBeforeDisplay(release, 70);
|
|
37
|
+
displayFailure(
|
|
38
|
+
error,
|
|
39
|
+
prepared.invocation.json,
|
|
40
|
+
runnerFailureCategory(error),
|
|
41
|
+
"runner failure",
|
|
42
|
+
);
|
|
43
|
+
} finally {
|
|
44
|
+
cancellation.dispose();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function retainCompletion(
|
|
49
|
+
result: Parameters<typeof retainCliResult>[0],
|
|
50
|
+
resultsRoot: string,
|
|
51
|
+
) {
|
|
52
|
+
try {
|
|
53
|
+
return await retainCliResult(result, resultsRoot);
|
|
54
|
+
} catch (cause) {
|
|
55
|
+
if (result.exitCode === 0)
|
|
56
|
+
throw new Error(
|
|
57
|
+
"CLI result/latest could not be retained; check --results-root permissions",
|
|
58
|
+
{ cause },
|
|
59
|
+
);
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
"sevro: CLI result/latest could not be retained; original run outcome preserved; inspect evidence directly\n",
|
|
62
|
+
);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function releaseBeforeDisplay(
|
|
68
|
+
release: CommandRelease,
|
|
69
|
+
exitCode: number,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
await release?.();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (exitCode === 0) throw error;
|
|
75
|
+
const detail =
|
|
76
|
+
error instanceof Error
|
|
77
|
+
? error.message
|
|
78
|
+
: "temporary project could not be removed";
|
|
79
|
+
process.stderr.write(`sevro: demo cleanup failed: ${detail}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { atomicWriteJson } from "../storage";
|
|
3
|
+
import type { CliResult } from "../engine";
|
|
4
|
+
import { createReport } from "../report";
|
|
5
|
+
import { withResultsLock } from "../results-lock";
|
|
6
|
+
|
|
7
|
+
/** Latest means the last successfully retained CLI result, including dry/failing runs. */
|
|
8
|
+
export async function retainCliResult(result: CliResult, resultsRoot: string) {
|
|
9
|
+
return withResultsLock(resultsRoot, async () => {
|
|
10
|
+
const resultFile = join(dirname(result.evidencePath), "result.json");
|
|
11
|
+
await atomicWriteJson(resultFile, result);
|
|
12
|
+
await atomicWriteJson(join(resultsRoot, "latest.json"), result);
|
|
13
|
+
return { resultFile, report: await createReport([resultFile]) };
|
|
14
|
+
});
|
|
15
|
+
}
|
package/src/cli/demo.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { main } from "./command";
|
|
5
|
+
import { InvocationError } from "./invocation";
|
|
6
|
+
import { displayFailure } from "./presentation";
|
|
7
|
+
|
|
8
|
+
/** Run the packaged deterministic case without inheriting a caller's project policy. */
|
|
9
|
+
export async function demoCommand(argv: string[]): Promise<void> {
|
|
10
|
+
const json = argv.includes("--json");
|
|
11
|
+
if (argv.some((arg) => arg !== "--json")) {
|
|
12
|
+
displayFailure(
|
|
13
|
+
new InvocationError("demo accepts only --json; run sevro demo --help"),
|
|
14
|
+
json,
|
|
15
|
+
64,
|
|
16
|
+
"invalid demo invocation",
|
|
17
|
+
);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let projectRoot: string;
|
|
21
|
+
try {
|
|
22
|
+
projectRoot = await mkdtemp(join(tmpdir(), "sevro-demo-"));
|
|
23
|
+
} catch (cause) {
|
|
24
|
+
displayFailure(
|
|
25
|
+
new Error(
|
|
26
|
+
`demo temporary project could not be created under ${tmpdir()}; set TMPDIR to a writable directory`,
|
|
27
|
+
{ cause },
|
|
28
|
+
),
|
|
29
|
+
json,
|
|
30
|
+
70,
|
|
31
|
+
"demo preparation failed",
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
await main(
|
|
36
|
+
[
|
|
37
|
+
"run",
|
|
38
|
+
...(json ? ["--json"] : []),
|
|
39
|
+
"--case-file",
|
|
40
|
+
join(import.meta.dir, "..", "..", "examples", "basic", "graded.json"),
|
|
41
|
+
"--adapter-module",
|
|
42
|
+
join(import.meta.dir, "..", "..", "examples", "basic", "host.ts"),
|
|
43
|
+
"--project-root",
|
|
44
|
+
projectRoot,
|
|
45
|
+
"--results-root",
|
|
46
|
+
join(process.cwd(), ".sevro", "results"),
|
|
47
|
+
],
|
|
48
|
+
demoCleanup(projectRoot),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function demoCleanup(projectRoot: string): () => Promise<void> {
|
|
53
|
+
let attempted = false;
|
|
54
|
+
return async () => {
|
|
55
|
+
if (attempted) return;
|
|
56
|
+
attempted = true;
|
|
57
|
+
await rm(projectRoot, { recursive: true, force: true });
|
|
58
|
+
};
|
|
59
|
+
}
|