@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.
Files changed (243) hide show
  1. package/CONTRIBUTING.md +170 -0
  2. package/LICENSE +63 -136
  3. package/README.md +83 -66
  4. package/docs/README.md +44 -0
  5. package/docs/acknowledgements.md +14 -0
  6. package/docs/architecture.md +146 -0
  7. package/docs/assets/README.md +27 -0
  8. package/docs/assets/sevro-logo.png +0 -0
  9. package/docs/assets/sevro-symbol.svg +1 -0
  10. package/docs/assets/sevro-wordmark.svg +1 -0
  11. package/docs/builtin-graders-v1.md +41 -13
  12. package/docs/codex-host.md +184 -52
  13. package/docs/contracts.md +19 -0
  14. package/docs/creating-cases.md +139 -0
  15. package/docs/development-cli.md +190 -30
  16. package/docs/documentation-quality.md +61 -0
  17. package/docs/evaluation-workflow.md +110 -0
  18. package/docs/evidence/code-mode-controls-71.md +64 -0
  19. package/docs/evidence/code-mode-programs-85.md +64 -0
  20. package/docs/evidence/codex-state-73/README.md +204 -0
  21. package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
  22. package/docs/evidence/codex-state-73/results.json +661 -0
  23. package/docs/evidence/local-http-74.md +104 -0
  24. package/docs/evidence/runtime-startup-30/README.md +185 -0
  25. package/docs/evidence/runtime-startup-30/after.json +319 -0
  26. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
  27. package/docs/evidence/runtime-startup-30/before.json +403 -0
  28. package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
  29. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
  30. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
  31. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
  32. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
  33. package/docs/evidence/skill-reads-70/results.json +823 -0
  34. package/docs/evidence.md +21 -0
  35. package/docs/extension-protocol-v1.md +50 -16
  36. package/docs/fixture-cleanup-validation.md +3 -0
  37. package/docs/getting-started.md +102 -0
  38. package/docs/guide-evaluation.md +89 -0
  39. package/docs/guide-verification.json +239 -0
  40. package/docs/identity-v1.md +35 -1
  41. package/docs/installing.md +88 -0
  42. package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
  43. package/docs/licensing.md +42 -0
  44. package/docs/native-controls.md +76 -5
  45. package/docs/native-hosts.md +228 -0
  46. package/docs/reading-results.md +141 -0
  47. package/docs/recipes.md +489 -0
  48. package/docs/releases.md +81 -40
  49. package/docs/report-v1.md +29 -1
  50. package/docs/results-v1.md +180 -0
  51. package/docs/run-settings-v1.md +147 -0
  52. package/docs/running-evaluations.md +31 -0
  53. package/docs/runtime-v1.md +614 -0
  54. package/docs/specs/repository-guide.md +58 -0
  55. package/docs/troubleshooting.md +30 -0
  56. package/docs/typescript-quality.md +234 -0
  57. package/docs/usability-study.md +114 -0
  58. package/examples/basic/host.ts +2 -2
  59. package/examples/recipes/candidate.ts +26 -0
  60. package/examples/recipes/cases/coding-change.json +21 -0
  61. package/examples/recipes/cases/host-tools.json +21 -0
  62. package/examples/recipes/cases/mise.json +21 -0
  63. package/examples/recipes/cases/node.json +21 -0
  64. package/examples/recipes/cases/python-uv.json +21 -0
  65. package/examples/recipes/cases/semantic.json +17 -0
  66. package/examples/recipes/coding-change/check.ts +23 -0
  67. package/examples/recipes/extension-artifacts.ts +65 -0
  68. package/examples/recipes/extension-methods.ts +174 -0
  69. package/examples/recipes/extension.ts +37 -0
  70. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  71. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  72. package/examples/recipes/repository-skill/README.md +3 -0
  73. package/examples/recipes/run-inputs.ts +188 -0
  74. package/examples/recipes/run-options.ts +131 -0
  75. package/examples/recipes/run.ts +63 -0
  76. package/examples/recipes/runtime/empty.json +1 -0
  77. package/examples/recipes/runtime/goals.json +1 -0
  78. package/examples/recipes/runtime/mise.json +6 -0
  79. package/examples/recipes/runtime/path.json +1 -0
  80. package/examples/recipes/runtime/plugins.json +4 -0
  81. package/examples/recipes/runtime/python-uv.json +10 -0
  82. package/examples/recipes/semantic-judge.ts +29 -0
  83. package/examples/templates/coding/adapter.ts +17 -0
  84. package/examples/templates/coding/case.json +15 -0
  85. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  86. package/examples/templates/repository-skill/extension-data.ts +64 -0
  87. package/examples/templates/repository-skill/extension.ts +31 -0
  88. package/examples/templates/response/adapter.ts +12 -0
  89. package/examples/templates/response/case.json +13 -0
  90. package/package.json +39 -6
  91. package/schemas/report-v1.schema.json +9 -0
  92. package/schemas/run-evidence-v1.schema.json +2 -1
  93. package/schemas/runtime-v1.schema.json +193 -0
  94. package/src/advisory.ts +10 -5
  95. package/src/candidate-artifacts.ts +208 -0
  96. package/src/candidate-codex-state.ts +185 -0
  97. package/src/case-validation.ts +105 -0
  98. package/src/cli/cancellation.ts +20 -0
  99. package/src/cli/command.ts +81 -0
  100. package/src/cli/completion.ts +15 -0
  101. package/src/cli/demo.ts +59 -0
  102. package/src/cli/evaluation.ts +55 -0
  103. package/src/cli/extension.ts +79 -0
  104. package/src/cli/help.ts +222 -0
  105. package/src/cli/hosts.ts +59 -0
  106. package/src/cli/init-settings.ts +30 -0
  107. package/src/cli/init.ts +111 -0
  108. package/src/cli/invocation.ts +627 -0
  109. package/src/cli/loading.ts +151 -0
  110. package/src/cli/preparation.ts +163 -0
  111. package/src/cli/presentation.ts +91 -0
  112. package/src/cli/progress.ts +6 -0
  113. package/src/cli/run-manifest.ts +133 -0
  114. package/src/cli/run-options.ts +101 -0
  115. package/src/cli/run-overrides.ts +83 -0
  116. package/src/cli/settings.ts +160 -0
  117. package/src/cli/summary.ts +43 -0
  118. package/src/cli.ts +13 -843
  119. package/src/contract-types.ts +94 -0
  120. package/src/engine.ts +19 -2373
  121. package/src/evaluation/advisory.ts +150 -0
  122. package/src/evaluation/check-plan.ts +510 -0
  123. package/src/evaluation/collection.ts +88 -0
  124. package/src/evaluation/context.ts +10 -0
  125. package/src/evaluation/grading-host.ts +39 -0
  126. package/src/evaluation/grading.ts +258 -0
  127. package/src/evaluation/invocation-plan.ts +548 -0
  128. package/src/evaluation/plan.ts +736 -0
  129. package/src/evaluation/progress.ts +19 -0
  130. package/src/evaluation/resources.ts +241 -0
  131. package/src/evaluation/run.ts +206 -0
  132. package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
  133. package/src/evaluation/semantic.ts +312 -0
  134. package/src/evaluation/trial.ts +866 -0
  135. package/src/evaluation/types.ts +207 -0
  136. package/src/evidence/host-results.ts +92 -0
  137. package/src/evidence/retained-artifacts.ts +18 -0
  138. package/src/evidence/trial-writer.ts +203 -0
  139. package/src/evidence/usage.ts +17 -0
  140. package/src/extension-client.ts +228 -148
  141. package/src/extension-session.ts +326 -195
  142. package/src/fixtures/advisory.ts +338 -0
  143. package/src/fixtures/bin.ts +57 -0
  144. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  145. package/src/fixtures/create.ts +187 -0
  146. package/src/fixtures/generated.ts +300 -0
  147. package/src/fixtures/git-hooks.ts +24 -0
  148. package/src/fixtures/preparation.ts +270 -0
  149. package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
  150. package/src/fixtures/setup.ts +224 -0
  151. package/src/fixtures/tools.ts +88 -0
  152. package/src/fixtures/workspace-state.ts +212 -0
  153. package/src/generated/cli-result.cjs +2 -0
  154. package/src/generated/cli-result.d.cts +3 -0
  155. package/src/generated/extension.cjs +2 -0
  156. package/src/generated/extension.d.cts +3 -0
  157. package/src/generated/report.cjs +2 -0
  158. package/src/generated/report.d.cts +3 -0
  159. package/src/generated/run-evidence.cjs +2 -0
  160. package/src/generated/run-evidence.d.cts +3 -0
  161. package/src/generated/runtime.cjs +2 -0
  162. package/src/generated/runtime.d.cts +3 -0
  163. package/src/graders/artifact.ts +56 -23
  164. package/src/graders/git-head.ts +79 -75
  165. package/src/graders/output.ts +222 -140
  166. package/src/graders/semantic.ts +102 -43
  167. package/src/graders/shell.ts +379 -207
  168. package/src/host-diagnostic-redaction.ts +52 -0
  169. package/src/host-diagnostics.ts +319 -0
  170. package/src/hosts/claude-continuation.ts +63 -18
  171. package/src/hosts/claude-credential.ts +43 -62
  172. package/src/hosts/claude-events.ts +86 -51
  173. package/src/hosts/claude-keychain.ts +59 -0
  174. package/src/hosts/claude-native-goal.ts +57 -33
  175. package/src/hosts/claude-process.ts +111 -0
  176. package/src/hosts/claude-repository-invocation.ts +208 -117
  177. package/src/hosts/claude-settings.ts +240 -21
  178. package/src/hosts/claude-skill-activations.ts +166 -0
  179. package/src/hosts/claude-state.ts +403 -0
  180. package/src/hosts/claude-tool-calls.ts +171 -106
  181. package/src/hosts/claude.ts +561 -451
  182. package/src/hosts/codex-app-server.ts +326 -178
  183. package/src/hosts/codex-configuration.ts +44 -18
  184. package/src/hosts/codex-events.ts +97 -60
  185. package/src/hosts/codex-exec-controls.ts +516 -0
  186. package/src/hosts/codex-invocation.ts +70 -0
  187. package/src/hosts/codex-marketplace.ts +283 -0
  188. package/src/hosts/codex-native-calls.ts +546 -500
  189. package/src/hosts/codex-process.ts +202 -0
  190. package/src/hosts/codex-profile.ts +257 -28
  191. package/src/hosts/codex-session-files.ts +106 -0
  192. package/src/hosts/codex-state.ts +492 -0
  193. package/src/hosts/codex.ts +783 -832
  194. package/src/hosts/isolation-common.ts +94 -0
  195. package/src/hosts/isolation-roots.ts +54 -27
  196. package/src/hosts/isolation.ts +10 -0
  197. package/src/hosts/linux-sandbox.ts +159 -0
  198. package/src/hosts/mac-sandbox.ts +187 -61
  199. package/src/hosts/native-controls.ts +152 -62
  200. package/src/hosts/process-lifecycle.ts +37 -0
  201. package/src/hosts/protected-worktrees.ts +51 -19
  202. package/src/hosts/skill-hook-catalog.ts +131 -0
  203. package/src/hosts/skill-hook-handler.ts +374 -0
  204. package/src/hosts/skill-hooks.ts +580 -0
  205. package/src/hosts/workspace-fingerprint.ts +55 -35
  206. package/src/identity.ts +102 -48
  207. package/src/instrumentation.ts +109 -48
  208. package/src/native-transcript-state.ts +131 -0
  209. package/src/native-transcript-view.ts +90 -0
  210. package/src/native-transcripts.ts +286 -0
  211. package/src/process-lifecycle.ts +150 -0
  212. package/src/provenance.ts +161 -81
  213. package/src/prune.ts +215 -0
  214. package/src/report.ts +172 -140
  215. package/src/resolved-case.ts +33 -0
  216. package/src/results-lock.ts +29 -0
  217. package/src/results.ts +133 -86
  218. package/src/run-owner.ts +154 -63
  219. package/src/runtime/config.ts +456 -0
  220. package/src/runtime/discovery-apple.ts +90 -0
  221. package/src/runtime/discovery-providers.ts +63 -0
  222. package/src/runtime/discovery.ts +70 -0
  223. package/src/runtime/hook-processes.ts +97 -0
  224. package/src/runtime/hooks.ts +476 -0
  225. package/src/runtime/paths.ts +313 -0
  226. package/src/runtime/query.ts +85 -0
  227. package/src/runtime/seeds.ts +134 -0
  228. package/src/runtime/shared-cache.ts +143 -0
  229. package/src/runtime/state.ts +143 -0
  230. package/src/schema-types.ts +198 -0
  231. package/src/schema.ts +15 -19
  232. package/src/transcript-retention.ts +146 -0
  233. package/src/value-guards.ts +20 -0
  234. package/src/advisory-fixture.ts +0 -282
  235. package/src/fixture-bin.ts +0 -87
  236. package/src/fixture-setup.ts +0 -163
  237. package/src/generated-fixture.ts +0 -220
  238. package/src/git-hooks.ts +0 -63
  239. package/src/hosts/claude-nested-skills.ts +0 -218
  240. package/src/hosts/codex-command-output.ts +0 -399
  241. package/src/hosts/codex-executor-command.ts +0 -195
  242. package/src/hosts/codex-skill-reads.ts +0 -391
  243. package/src/preparation.ts +0 -211
@@ -1,282 +0,0 @@
1
- import {
2
- lstat,
3
- mkdir,
4
- mkdtemp,
5
- readlink,
6
- readdir,
7
- realpath,
8
- rm,
9
- symlink,
10
- copyFile,
11
- } from "node:fs/promises";
12
- import { tmpdir } from "node:os";
13
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
14
- import { fixtureParts } from "./preparation";
15
-
16
- const MAX_GIT_OUTPUT = 16 * 1024 * 1024;
17
- const MAX_UNTRACKED_BYTES = 32 * 1024 * 1024;
18
- const MAX_UNTRACKED_FILES = 1024;
19
- const HIDDEN_ROOTS = [".agents", ".claude", ".codex", ".git"];
20
-
21
- function contained(root: string, path: string): boolean {
22
- const child = relative(root, path);
23
- return (
24
- child === "" ||
25
- (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
26
- );
27
- }
28
-
29
- function excluded(path: string, roots: string[]): boolean {
30
- return roots.some((root) => path === root || path.startsWith(`${root}/`));
31
- }
32
-
33
- async function bounded(stream: ReadableStream<Uint8Array>): Promise<Buffer> {
34
- const chunks: Uint8Array[] = [];
35
- let size = 0;
36
- for await (const chunk of stream) {
37
- size += chunk.byteLength;
38
- if (size > MAX_GIT_OUTPUT)
39
- throw new Error("advisory fixture Git output is too large");
40
- chunks.push(chunk);
41
- }
42
- return Buffer.concat(chunks);
43
- }
44
-
45
- async function git(
46
- cwd: string,
47
- args: string[],
48
- input?: Uint8Array,
49
- ): Promise<Buffer> {
50
- const proc = Bun.spawn(
51
- [
52
- "git",
53
- "-c",
54
- `core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
55
- ...args,
56
- ],
57
- {
58
- cwd,
59
- stdin: input ? "pipe" : "ignore",
60
- stdout: "pipe",
61
- stderr: "pipe",
62
- env: {
63
- PATH: process.env.PATH ?? "/usr/bin:/bin",
64
- ...(process.env.SystemRoot
65
- ? { SystemRoot: process.env.SystemRoot }
66
- : {}),
67
- GIT_CONFIG_NOSYSTEM: "1",
68
- GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
69
- GIT_TERMINAL_PROMPT: "0",
70
- GIT_AUTHOR_NAME: "Sevro Advisory",
71
- GIT_AUTHOR_EMAIL: "advisory@sevro.invalid",
72
- GIT_COMMITTER_NAME: "Sevro Advisory",
73
- GIT_COMMITTER_EMAIL: "advisory@sevro.invalid",
74
- GIT_AUTHOR_DATE: "2000-01-01T00:00:00+00:00",
75
- GIT_COMMITTER_DATE: "2000-01-01T00:00:00+00:00",
76
- },
77
- },
78
- );
79
- try {
80
- const write = (async () => {
81
- if (!input) return;
82
- if (!proc.stdin || typeof proc.stdin === "number")
83
- throw new Error("advisory fixture Git input unavailable");
84
- await proc.stdin.write(input);
85
- await proc.stdin.end();
86
- })();
87
- const [out, , code] = await Promise.all([
88
- bounded(proc.stdout),
89
- bounded(proc.stderr),
90
- proc.exited,
91
- write,
92
- ]);
93
- if (code !== 0) throw new Error("advisory fixture Git operation failed");
94
- return out;
95
- } catch {
96
- try {
97
- proc.kill("SIGKILL");
98
- } catch {
99
- // The process may already have exited.
100
- }
101
- await proc.exited;
102
- throw new Error("advisory fixture Git operation failed");
103
- }
104
- }
105
-
106
- async function checkSymlinks(root: string, directory = root): Promise<void> {
107
- for (const entry of await readdir(directory, { withFileTypes: true })) {
108
- if (directory === root && entry.name === ".git") continue;
109
- const path = join(directory, entry.name);
110
- if (entry.isDirectory()) await checkSymlinks(root, path);
111
- else if (entry.isSymbolicLink()) {
112
- const target = await readlink(path);
113
- if (
114
- isAbsolute(target) ||
115
- !contained(root, resolve(dirname(path), target))
116
- )
117
- throw new Error("advisory fixture contains an escaping symlink");
118
- const resolved = await realpath(path).catch(() => {
119
- throw new Error("advisory fixture contains a broken symlink");
120
- });
121
- if (!contained(root, resolved))
122
- throw new Error("advisory fixture contains an escaping symlink");
123
- }
124
- }
125
- }
126
-
127
- async function safeParent(root: string, parts: string[]): Promise<void> {
128
- let directory = root;
129
- for (const part of parts.slice(0, -1)) {
130
- directory = join(directory, part);
131
- await mkdir(directory, { mode: 0o700 }).catch((error: unknown) => {
132
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
133
- });
134
- if (!(await lstat(directory)).isDirectory())
135
- throw new Error("advisory fixture path has a non-directory parent");
136
- }
137
- }
138
-
139
- async function copyUntracked(
140
- source: string,
141
- destination: string,
142
- exclusions: string[],
143
- ): Promise<void> {
144
- const output = await git(source, [
145
- "ls-files",
146
- "--others",
147
- "--exclude-standard",
148
- "-z",
149
- ]);
150
- const paths = new TextDecoder("utf-8", { fatal: true })
151
- .decode(output)
152
- .split("\0")
153
- .filter(Boolean)
154
- .filter((path) => !excluded(path, exclusions));
155
- if (paths.length > MAX_UNTRACKED_FILES)
156
- throw new Error("advisory fixture has too many untracked files");
157
- let totalBytes = 0;
158
- for (const path of paths) {
159
- const parts = fixtureParts(path);
160
- const from = join(source, ...parts);
161
- const to = join(destination, ...parts);
162
- if (!contained(source, from) || !contained(destination, to))
163
- throw new Error("advisory fixture path escapes its root");
164
- const info = await lstat(from);
165
- totalBytes += info.size;
166
- if (totalBytes > MAX_UNTRACKED_BYTES)
167
- throw new Error("advisory fixture untracked files exceed the size limit");
168
- await safeParent(destination, parts);
169
- if (
170
- await lstat(to).then(
171
- () => true,
172
- (error: NodeJS.ErrnoException) => {
173
- if (error.code === "ENOENT") return false;
174
- throw error;
175
- },
176
- )
177
- )
178
- throw new Error("advisory fixture untracked path already exists");
179
- if (info.isFile()) await copyFile(from, to);
180
- else if (info.isSymbolicLink()) {
181
- const target = await readlink(from);
182
- if (
183
- isAbsolute(target) ||
184
- !contained(destination, resolve(dirname(to), target))
185
- )
186
- throw new Error("advisory fixture contains an escaping symlink");
187
- await symlink(target, to);
188
- } else
189
- throw new Error("advisory fixture has an unsupported untracked entry");
190
- }
191
- }
192
-
193
- export interface BlindAdvisoryOptions {
194
- /** Fixture HEAD before candidate execution. */
195
- baseRevision: string;
196
- /** Additional root-relative evaluator paths to omit from the judge view. */
197
- excludedPaths?: string[];
198
- }
199
-
200
- /** Capture the candidate fixture's baseline before the host edits it. */
201
- export async function advisoryBaseRevision(workspace: string): Promise<string> {
202
- const revision = (await git(workspace, ["rev-parse", "--verify", "HEAD"]))
203
- .toString("utf8")
204
- .trim();
205
- if (!/^[a-f0-9]{40,64}$/.test(revision))
206
- throw new Error("advisory fixture has no valid Git baseline");
207
- return revision;
208
- }
209
-
210
- /** Rebuild a condition-blind Git view of the complete candidate change. */
211
- export async function buildBlindAdvisoryFixture(
212
- candidateWorkspace: string,
213
- options: BlindAdvisoryOptions,
214
- ): Promise<string> {
215
- if (
216
- !isAbsolute(candidateWorkspace) ||
217
- !/^[a-f0-9]{40,64}$/.test(options.baseRevision)
218
- )
219
- throw new Error("invalid advisory fixture source");
220
- const source = await realpath(candidateWorkspace);
221
- const exclusions = [...HIDDEN_ROOTS, ...(options.excludedPaths ?? [])];
222
- for (const path of options.excludedPaths ?? []) {
223
- fixtureParts(path);
224
- if (path.toLowerCase() === ".git" || path.toLowerCase().startsWith(".git/"))
225
- throw new Error("invalid advisory exclusion");
226
- }
227
- const destination = await mkdtemp(join(tmpdir(), "sevro-advisory-"));
228
- try {
229
- await git(source, [
230
- "clone",
231
- "--local",
232
- "--no-hardlinks",
233
- "--no-checkout",
234
- "--quiet",
235
- source,
236
- destination,
237
- ]);
238
- await git(destination, [
239
- "checkout",
240
- "--detach",
241
- "--quiet",
242
- options.baseRevision,
243
- ]);
244
- for (const root of exclusions)
245
- await rm(join(destination, ...fixtureParts(root)), {
246
- recursive: true,
247
- force: true,
248
- });
249
- await rm(join(destination, ".git"), { recursive: true, force: true });
250
- await git(destination, ["init", "--quiet", "--initial-branch=main"]);
251
- await git(destination, ["add", "-A"]);
252
- await git(destination, [
253
- "commit",
254
- "--quiet",
255
- "--allow-empty",
256
- "-m",
257
- "Advisory baseline",
258
- ]);
259
- const pathspec = exclusions.flatMap((path) => [
260
- `:(exclude)${path}`,
261
- `:(exclude)${path}/**`,
262
- ]);
263
- const patch = await git(source, [
264
- "diff",
265
- "--binary",
266
- "--no-ext-diff",
267
- "--no-textconv",
268
- "--no-renames",
269
- options.baseRevision,
270
- "--",
271
- ".",
272
- ...pathspec,
273
- ]);
274
- if (patch.length) await git(destination, ["apply", "--binary", "-"], patch);
275
- await copyUntracked(source, destination, exclusions);
276
- await checkSymlinks(destination);
277
- return destination;
278
- } catch (error) {
279
- await rm(destination, { recursive: true, force: true });
280
- throw error;
281
- }
282
- }
@@ -1,87 +0,0 @@
1
- import { chmod, lstat, mkdir, realpath, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
-
4
- const MAX_TOOLS = 64;
5
- const MAX_BYTES = 1024 * 1024;
6
-
7
- /** Validate executable fixture tools without allowing path traversal. */
8
- export function prepareFixtureBin(
9
- value: unknown,
10
- ): Record<string, string> | null {
11
- if (value === undefined) return null;
12
- if (!value || typeof value !== "object" || Array.isArray(value))
13
- throw new Error("invalid fixture binaries");
14
- const bin = value as Record<string, unknown>;
15
- const entries = Object.entries(bin);
16
- if (
17
- entries.length > MAX_TOOLS ||
18
- entries.some(
19
- ([name, content]) =>
20
- !/^[A-Za-z][A-Za-z0-9._-]*$/.test(name) ||
21
- name.length > 64 ||
22
- typeof content !== "string" ||
23
- !content ||
24
- content.includes("\0"),
25
- ) ||
26
- entries.reduce(
27
- (size, [name, content]) =>
28
- size +
29
- Buffer.byteLength(name, "utf8") +
30
- Buffer.byteLength(content as string, "utf8"),
31
- 0,
32
- ) > MAX_BYTES
33
- )
34
- throw new Error("invalid fixture binaries");
35
- return bin as Record<string, string>;
36
- }
37
-
38
- /** Install declared tools inside the fixture's Git metadata, after commits. */
39
- export async function installFixtureBin(
40
- bin: Record<string, string> | undefined,
41
- workspace: string,
42
- ): Promise<void> {
43
- if (!bin || Object.keys(bin).length === 0) return;
44
- const directory = join(workspace, ".git", "fixture-bin");
45
- await mkdir(directory, { recursive: true, mode: 0o700 });
46
- const root = await lstat(directory);
47
- if (!root.isDirectory() || root.isSymbolicLink())
48
- throw new Error("fixture binary directory is invalid");
49
- for (const [name, content] of Object.entries(bin)) {
50
- const target = join(directory, name);
51
- try {
52
- const existing = await lstat(target);
53
- if (!existing.isFile() || existing.isSymbolicLink())
54
- throw new Error("fixture binary target is invalid");
55
- } catch (error) {
56
- if (!(
57
- error instanceof Error &&
58
- "code" in error &&
59
- error.code === "ENOENT"
60
- ))
61
- throw error;
62
- }
63
- await writeFile(target, content, { mode: 0o755 });
64
- await chmod(target, 0o755);
65
- }
66
- }
67
-
68
- /** Setup may also install trusted tools, such as a repository ticket stub. */
69
- export async function fixtureBinDirectory(
70
- workspace: string,
71
- ): Promise<string | null> {
72
- const path = join(workspace, ".git", "fixture-bin");
73
- try {
74
- const entry = await lstat(path);
75
- if (!entry.isDirectory() || entry.isSymbolicLink())
76
- throw new Error("fixture binary directory is invalid");
77
- const canonicalWorkspace = await realpath(workspace);
78
- const canonicalBin = await realpath(path);
79
- if (canonicalBin !== join(canonicalWorkspace, ".git", "fixture-bin"))
80
- throw new Error("fixture binary directory escapes the workspace");
81
- return path;
82
- } catch (error) {
83
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
84
- return null;
85
- throw error;
86
- }
87
- }
@@ -1,163 +0,0 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { delimiter, isAbsolute, join } from "node:path";
4
-
5
- const MAX_COMMAND_BYTES = 64 * 1024;
6
- const MAX_ENV_BYTES = 16 * 1024;
7
- const TIMEOUT_MS = 120_000;
8
- const RESERVED_ENV = new Set([
9
- "PATH",
10
- "HOME",
11
- "TMPDIR",
12
- "GIT_CONFIG_GLOBAL",
13
- "GIT_CONFIG_NOSYSTEM",
14
- "GIT_TERMINAL_PROMPT",
15
- ]);
16
-
17
- export interface FixtureSetupDeclaration {
18
- command: string[];
19
- environment?: Record<string, string>;
20
- }
21
-
22
- export interface FixtureSetup extends FixtureSetupDeclaration {
23
- environment: Record<string, string>;
24
- }
25
-
26
- /** Validate a trusted extension's bounded fixture setup command. */
27
- export function prepareFixtureSetup(value: unknown): FixtureSetup | null {
28
- if (value === undefined) return null;
29
- if (!value || typeof value !== "object" || Array.isArray(value))
30
- throw new Error("invalid fixture setup");
31
- const input = value as Record<string, unknown>;
32
- if (
33
- Object.keys(input).some((key) => key !== "command" && key !== "environment")
34
- )
35
- throw new Error("unsupported fixture setup field");
36
- const command = input.command;
37
- if (
38
- !Array.isArray(command) ||
39
- !command.length ||
40
- command.length > 16 ||
41
- command.some(
42
- (arg) => typeof arg !== "string" || !arg.length || arg.includes("\0"),
43
- ) ||
44
- !isAbsolute(command[0]) ||
45
- Buffer.byteLength(command.join("\0"), "utf8") > MAX_COMMAND_BYTES
46
- )
47
- throw new Error("invalid fixture setup command");
48
- const environment = input.environment ?? {};
49
- if (
50
- !environment ||
51
- typeof environment !== "object" ||
52
- Array.isArray(environment)
53
- )
54
- throw new Error("invalid fixture setup environment");
55
- const entries = Object.entries(environment);
56
- if (
57
- entries.length > 32 ||
58
- entries.some(
59
- ([name, value]) =>
60
- !/^[A-Z][A-Z0-9_]*$/.test(name) ||
61
- RESERVED_ENV.has(name) ||
62
- typeof value !== "string" ||
63
- value.includes("\0") ||
64
- /\{\{(?!sevro\.(?:project|workspace)\}\})/.test(value),
65
- ) ||
66
- Buffer.byteLength(JSON.stringify(environment), "utf8") > MAX_ENV_BYTES
67
- )
68
- throw new Error("invalid fixture setup environment");
69
- return { command, environment: environment as Record<string, string> };
70
- }
71
-
72
- function stopProcess(proc: Bun.Subprocess): void {
73
- if (process.platform !== "win32") {
74
- try {
75
- process.kill(-proc.pid, "SIGKILL");
76
- return;
77
- } catch {
78
- // The process group may already be gone.
79
- }
80
- }
81
- try {
82
- proc.kill("SIGKILL");
83
- } catch {
84
- // The process already exited.
85
- }
86
- }
87
-
88
- /** Run setup after fixture history and before evaluator artifacts are mounted. */
89
- export async function runFixtureSetup(
90
- setup: FixtureSetup,
91
- options: {
92
- workspace: string;
93
- projectRoot: string;
94
- fixtureBinDir?: string;
95
- signal?: AbortSignal;
96
- },
97
- ): Promise<void> {
98
- if (options.signal?.aborted) throw new Error("fixture setup cancelled");
99
- const home = await mkdtemp(join(tmpdir(), "sevro-setup-"));
100
- let proc: Bun.Subprocess | undefined;
101
- let timer: ReturnType<typeof setTimeout> | undefined;
102
- let cancel: (() => void) | undefined;
103
- try {
104
- const environment = Object.fromEntries(
105
- Object.entries(setup.environment).map(([name, value]) => [
106
- name,
107
- value
108
- .replaceAll("{{sevro.project}}", options.projectRoot)
109
- .replaceAll("{{sevro.workspace}}", options.workspace),
110
- ]),
111
- );
112
- proc = Bun.spawn(setup.command, {
113
- cwd: options.workspace,
114
- env: {
115
- PATH: options.fixtureBinDir
116
- ? `${options.fixtureBinDir}${delimiter}${process.env.PATH ?? "/usr/bin:/bin"}`
117
- : (process.env.PATH ?? "/usr/bin:/bin"),
118
- HOME: home,
119
- TMPDIR: home,
120
- LANG: process.env.LANG ?? "C",
121
- GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
122
- GIT_CONFIG_NOSYSTEM: "1",
123
- GIT_TERMINAL_PROMPT: "0",
124
- GIT_AUTHOR_NAME: "Sevro Fixture",
125
- GIT_AUTHOR_EMAIL: "fixture@sevro.invalid",
126
- GIT_COMMITTER_NAME: "Sevro Fixture",
127
- GIT_COMMITTER_EMAIL: "fixture@sevro.invalid",
128
- GIT_AUTHOR_DATE: "2000-01-01T00:00:00+00:00",
129
- GIT_COMMITTER_DATE: "2000-01-01T00:00:00+00:00",
130
- ...environment,
131
- },
132
- detached: process.platform !== "win32",
133
- stdin: "ignore",
134
- stdout: "ignore",
135
- stderr: "ignore",
136
- });
137
- const running = proc;
138
- const timeout = new Promise<never>((_resolve, reject) => {
139
- timer = setTimeout(
140
- () => reject(new Error("fixture setup timed out")),
141
- TIMEOUT_MS,
142
- );
143
- });
144
- const aborted = new Promise<never>((_resolve, reject) => {
145
- if (!options.signal) return;
146
- cancel = () => reject(new Error("fixture setup cancelled"));
147
- if (options.signal.aborted) cancel();
148
- else options.signal.addEventListener("abort", cancel, { once: true });
149
- });
150
- const code = await Promise.race([running.exited, timeout, aborted]);
151
- if (code !== 0) throw new Error(`fixture setup failed (${code})`);
152
- } catch (error) {
153
- if (proc) {
154
- stopProcess(proc);
155
- await proc.exited;
156
- }
157
- throw error;
158
- } finally {
159
- if (timer) clearTimeout(timer);
160
- if (cancel) options.signal?.removeEventListener("abort", cancel);
161
- await rm(home, { recursive: true, force: true });
162
- }
163
- }