@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,6 +1,20 @@
1
+ import { runtimeLocalHttpEnabled, runtimeSeedSources } from "../runtime/config";
2
+ import {
3
+ candidateArtifactEnvironment,
4
+ type CandidateArtifactRoots,
5
+ } from "../candidate-artifacts";
1
6
  import { mkdir, stat } from "node:fs/promises";
2
7
  import { delimiter, join } from "node:path";
3
- import { prepareMacSandboxCommand } from "../hosts/mac-sandbox";
8
+ import { prepareIsolatedCommand } from "../hosts/isolation";
9
+ import type { RuntimePolicy } from "../runtime/config";
10
+ import { prepareRuntimeState } from "../runtime/state";
11
+ import { sharedCacheDenyRoots } from "../runtime/shared-cache";
12
+ import { hashJson } from "../identity";
13
+ import {
14
+ captureBoundedText,
15
+ ProcessDeadline,
16
+ terminateOwnedProcessGroup,
17
+ } from "../process-lifecycle";
4
18
 
5
19
  const DEFAULT_TIMEOUT_MS = 30_000;
6
20
  const MAX_TIMEOUT_MS = 120_000;
@@ -23,91 +37,141 @@ export interface PreparedShellCheck {
23
37
  captureStdout: boolean;
24
38
  }
25
39
 
40
+ function requireShellDeclaration(
41
+ id: string,
42
+ value: unknown,
43
+ ): asserts value is Record<string, unknown> {
44
+ if (!id || !value || typeof value !== "object")
45
+ throw new Error("invalid shell check declaration");
46
+ }
47
+ function requireShellKeys(config: Record<string, unknown>): void {
48
+ const allowed = [
49
+ "run",
50
+ "expectedExitCode",
51
+ "timeoutMs",
52
+ "expectExact",
53
+ "expectRegex",
54
+ "notRegex",
55
+ "flags",
56
+ ];
57
+ if (Object.keys(config).some((key) => !allowed.includes(key)))
58
+ throw new Error("unsupported shell check configuration");
59
+ }
60
+ function requireShellCommand(run: unknown): asserts run is string {
61
+ if (typeof run !== "string" || !run.trim() || run.length > 4096)
62
+ throw new Error("shell check requires a bounded command");
63
+ }
64
+ function requireExpectedExit(code: unknown): asserts code is number {
65
+ if (
66
+ typeof code !== "number" ||
67
+ !Number.isSafeInteger(code) ||
68
+ code < 0 ||
69
+ code > 255
70
+ )
71
+ throw new Error("invalid expected shell exit code");
72
+ }
73
+ function requireShellTimeout(timeout: unknown): asserts timeout is number {
74
+ if (
75
+ typeof timeout !== "number" ||
76
+ !Number.isSafeInteger(timeout) ||
77
+ timeout < 1 ||
78
+ timeout > MAX_TIMEOUT_MS
79
+ )
80
+ throw new Error("invalid shell check timeout");
81
+ }
82
+ function requireExactShellOutput(
83
+ output: unknown,
84
+ ): asserts output is string | undefined {
85
+ if (
86
+ output !== undefined &&
87
+ (typeof output !== "string" ||
88
+ Buffer.byteLength(output, "utf8") > MAX_OUTPUT_BYTES)
89
+ )
90
+ throw new Error("invalid exact shell output expectation");
91
+ }
92
+ function requireShellFlags(flags: unknown): asserts flags is string {
93
+ if (
94
+ flags !== "" &&
95
+ (typeof flags !== "string" ||
96
+ !/^[isu]*$/.test(flags) ||
97
+ new Set(flags).size !== flags.length)
98
+ )
99
+ throw new Error("invalid shell regex flags");
100
+ }
101
+ function requireFlagPattern(
102
+ flags: string,
103
+ expected: unknown,
104
+ forbidden: unknown,
105
+ ): void {
106
+ if (flags && expected === undefined && forbidden === undefined)
107
+ throw new Error("shell regex flags require a pattern");
108
+ }
109
+ function shellOptions(config: Record<string, unknown>) {
110
+ const {
111
+ run,
112
+ expectedExitCode = 0,
113
+ timeoutMs = DEFAULT_TIMEOUT_MS,
114
+ expectExact,
115
+ expectRegex,
116
+ notRegex,
117
+ flags = "",
118
+ } = config;
119
+ requireShellCommand(run);
120
+ requireExpectedExit(expectedExitCode);
121
+ requireShellTimeout(timeoutMs);
122
+ requireExactShellOutput(expectExact);
123
+ requireShellFlags(flags);
124
+ requireFlagPattern(flags, expectRegex, notRegex);
125
+ return {
126
+ run,
127
+ expectedExitCode,
128
+ timeoutMs,
129
+ expectExact,
130
+ expectRegex,
131
+ notRegex,
132
+ flags,
133
+ };
134
+ }
135
+ function compileShellPattern(
136
+ value: unknown,
137
+ flags: string,
138
+ ): RegExp | undefined {
139
+ if (value === undefined) return undefined;
140
+ if (typeof value !== "string" || value.length > 4096)
141
+ throw new Error("invalid shell regex pattern");
142
+ try {
143
+ return new RegExp(value, `m${flags}`);
144
+ } catch (cause) {
145
+ throw new Error("invalid shell regex pattern or flags", { cause });
146
+ }
147
+ }
148
+ function prepareShellCheck({
149
+ id,
150
+ configuration,
151
+ }: ShellCheckDeclaration): PreparedShellCheck {
152
+ requireShellDeclaration(id, configuration);
153
+ requireShellKeys(configuration);
154
+ const config = shellOptions(configuration);
155
+ const expectRegex = compileShellPattern(config.expectRegex, config.flags),
156
+ notRegex = compileShellPattern(config.notRegex, config.flags);
157
+ return {
158
+ id,
159
+ run: config.run,
160
+ expectedExitCode: config.expectedExitCode,
161
+ timeoutMs: config.timeoutMs,
162
+ expectExact: config.expectExact,
163
+ expectRegex,
164
+ notRegex,
165
+ captureStdout:
166
+ config.expectExact !== undefined ||
167
+ expectRegex !== undefined ||
168
+ notRegex !== undefined,
169
+ };
170
+ }
26
171
  export function prepareShellChecks(
27
172
  declarations: ShellCheckDeclaration[],
28
173
  ): PreparedShellCheck[] {
29
- return declarations.map(({ id, configuration }) => {
30
- if (!id || !configuration || typeof configuration !== "object")
31
- throw new Error("invalid shell check declaration");
32
- if (
33
- Object.keys(configuration).some(
34
- (key) =>
35
- ![
36
- "run",
37
- "expectedExitCode",
38
- "timeoutMs",
39
- "expectExact",
40
- "expectRegex",
41
- "notRegex",
42
- "flags",
43
- ].includes(key),
44
- )
45
- )
46
- throw new Error("unsupported shell check configuration");
47
- const {
48
- run,
49
- expectedExitCode = 0,
50
- timeoutMs = DEFAULT_TIMEOUT_MS,
51
- expectExact,
52
- expectRegex,
53
- notRegex,
54
- flags = "",
55
- } = configuration;
56
- if (typeof run !== "string" || !run.trim() || run.length > 4096)
57
- throw new Error("shell check requires a bounded command");
58
- if (
59
- !Number.isSafeInteger(expectedExitCode) ||
60
- (expectedExitCode as number) < 0 ||
61
- (expectedExitCode as number) > 255
62
- )
63
- throw new Error("invalid expected shell exit code");
64
- if (
65
- !Number.isSafeInteger(timeoutMs) ||
66
- (timeoutMs as number) < 1 ||
67
- (timeoutMs as number) > MAX_TIMEOUT_MS
68
- )
69
- throw new Error("invalid shell check timeout");
70
- if (
71
- expectExact !== undefined &&
72
- (typeof expectExact !== "string" ||
73
- Buffer.byteLength(expectExact, "utf8") > MAX_OUTPUT_BYTES)
74
- )
75
- throw new Error("invalid exact shell output expectation");
76
- if (
77
- flags !== "" &&
78
- (typeof flags !== "string" ||
79
- !/^[isu]*$/.test(flags) ||
80
- new Set(flags).size !== flags.length)
81
- )
82
- throw new Error("invalid shell regex flags");
83
- if (flags && expectRegex === undefined && notRegex === undefined)
84
- throw new Error("shell regex flags require a pattern");
85
- const pattern = (value: unknown): RegExp | undefined => {
86
- if (value === undefined) return undefined;
87
- if (typeof value !== "string" || value.length > 4096)
88
- throw new Error("invalid shell regex pattern");
89
- try {
90
- return new RegExp(value, `m${flags}`);
91
- } catch {
92
- throw new Error("invalid shell regex pattern or flags");
93
- }
94
- };
95
- const expectedPattern = pattern(expectRegex);
96
- const forbiddenPattern = pattern(notRegex);
97
- return {
98
- id,
99
- run,
100
- expectedExitCode: expectedExitCode as number,
101
- timeoutMs: timeoutMs as number,
102
- expectExact: expectExact as string | undefined,
103
- expectRegex: expectedPattern,
104
- notRegex: forbiddenPattern,
105
- captureStdout:
106
- expectExact !== undefined ||
107
- expectedPattern !== undefined ||
108
- forbiddenPattern !== undefined,
109
- };
110
- });
174
+ return declarations.map(prepareShellCheck);
111
175
  }
112
176
 
113
177
  export interface ShellCheckResult {
@@ -125,153 +189,261 @@ export function assessShellCheck(
125
189
  passed: false,
126
190
  detail: `exit code ${result.exitCode} (expected ${check.expectedExitCode})`,
127
191
  };
128
- const out = result.stdout;
129
- if (check.captureStdout && out === null)
192
+ if (check.captureStdout && result.stdout === null)
130
193
  throw new Error("shell stdout observation is missing");
131
- if (
132
- check.expectExact !== undefined &&
133
- (out!.endsWith("\n") ? out!.slice(0, -1) : out) !== check.expectExact
134
- )
135
- return { passed: false, detail: "exact shell output did not match" };
136
- if (check.expectRegex && !check.expectRegex.test(out!))
137
- return {
138
- passed: false,
139
- detail: "expected shell output pattern did not match",
140
- };
141
- if (check.notRegex && check.notRegex.test(out!))
142
- return { passed: false, detail: "forbidden shell output pattern matched" };
143
- return { passed: true, detail: `exit code ${result.exitCode}` };
194
+ const detail = shellOutputFailure(check, result.stdout);
195
+ return assessedShellOutput(detail, result.exitCode);
196
+ }
197
+
198
+ function shellOutputFailure(
199
+ check: PreparedShellCheck,
200
+ out: string | null,
201
+ ): string | undefined {
202
+ return (
203
+ exactOutputFailure(check, out) ??
204
+ expectedPatternFailure(check, out) ??
205
+ forbiddenPatternFailure(check, out)
206
+ );
207
+ }
208
+
209
+ function assessedShellOutput(
210
+ detail: string | undefined,
211
+ exitCode: number,
212
+ ): { passed: boolean; detail: string } {
213
+ return detail === undefined
214
+ ? { passed: true, detail: `exit code ${exitCode}` }
215
+ : { passed: false, detail };
216
+ }
217
+ function exactOutputFailure(
218
+ check: PreparedShellCheck,
219
+ out: string | null,
220
+ ): string | undefined {
221
+ if (check.expectExact === undefined) return undefined;
222
+ if (out === null) throw new Error("shell stdout observation is missing");
223
+ const actual = out.endsWith("\n") ? out.slice(0, -1) : out;
224
+ return actual === check.expectExact
225
+ ? undefined
226
+ : "exact shell output did not match";
227
+ }
228
+ function expectedPatternFailure(
229
+ check: PreparedShellCheck,
230
+ out: string | null,
231
+ ): string | undefined {
232
+ return check.expectRegex && !check.expectRegex.test(String(out))
233
+ ? "expected shell output pattern did not match"
234
+ : undefined;
235
+ }
236
+ function forbiddenPatternFailure(
237
+ check: PreparedShellCheck,
238
+ out: string | null,
239
+ ): string | undefined {
240
+ return check.notRegex && check.notRegex.test(String(out))
241
+ ? "forbidden shell output pattern matched"
242
+ : undefined;
144
243
  }
145
244
 
146
245
  async function boundedOutput(
147
246
  stream: ReadableStream<Uint8Array>,
148
247
  ): Promise<string> {
149
- const chunks: Uint8Array[] = [];
150
- let size = 0;
151
- for await (const chunk of stream) {
152
- size += chunk.byteLength;
153
- if (size > MAX_OUTPUT_BYTES) throw new Error("shell stdout exceeds 1 MiB");
154
- chunks.push(chunk);
155
- }
156
- return new TextDecoder("utf-8", { fatal: true }).decode(
157
- Buffer.concat(chunks),
158
- );
248
+ return captureBoundedText(stream, {
249
+ limit: MAX_OUTPUT_BYTES,
250
+ overflow: "reject",
251
+ exceeded: () => new Error("shell stdout exceeds 1 MiB"),
252
+ cancelOnError: true,
253
+ });
159
254
  }
160
255
 
161
- function stopProcess(proc: Bun.Subprocess): void {
162
- if (process.platform !== "win32") {
256
+ interface ShellRunOptions {
257
+ candidateTranscriptRoot?: string;
258
+ candidateArtifactRoots?: CandidateArtifactRoots;
259
+ runtimePolicy?: RuntimePolicy;
260
+ workspace: string;
261
+ fixtureBinDir?: string;
262
+ toolchainBinDir?: string;
263
+ uvRuntimeCache?: boolean;
264
+ protectedRoots: string[];
265
+ protectedRootsCanonical?: boolean;
266
+ privateStateRoot: string;
267
+ signal?: AbortSignal;
268
+ }
269
+ async function shellRuntime(options: ShellRunOptions) {
270
+ if (options.runtimePolicy) {
271
+ const runtime = await prepareRuntimeState(
272
+ options.workspace,
273
+ options.runtimePolicy,
274
+ "checks",
275
+ join(options.privateStateRoot, "runtime", hashJson(options.workspace)),
276
+ );
277
+ return {
278
+ ...runtime,
279
+ uvCache: join(runtime.root, "uv-cache"),
280
+ privateRuntimeRoot: runtime.root,
281
+ };
282
+ }
283
+ const root = join(
284
+ options.privateStateRoot,
285
+ "runtime",
286
+ hashJson(options.workspace),
287
+ ),
288
+ home = join(root, "check-home"),
289
+ temp = join(root, "check-tmp");
290
+ await Promise.all([
291
+ mkdir(home, { recursive: true, mode: 0o700 }),
292
+ mkdir(temp, { recursive: true, mode: 0o700 }),
293
+ ]);
294
+ return {
295
+ root,
296
+ home,
297
+ temp,
298
+ uvCache: join(options.workspace, ".git", "sevro-runtime", "uv-cache"),
299
+ environment: {} as Record<string, string>,
300
+ privateRuntimeRoot: root,
301
+ writableCacheRoots: [],
302
+ };
303
+ }
304
+ type ShellRuntime = Awaited<ReturnType<typeof shellRuntime>>;
305
+ type Isolation = Awaited<ReturnType<typeof prepareIsolatedCommand>>;
306
+ function shellRuntimeEnvironment(
307
+ runtime: ShellRuntime,
308
+ enabled: boolean | undefined,
309
+ ): Record<string, string> {
310
+ return enabled
311
+ ? {
312
+ UV_CACHE_DIR: runtime.uvCache,
313
+ UV_PROJECT_ENVIRONMENT: join(runtime.root, "project-environment"),
314
+ UV_OFFLINE: "1",
315
+ PYTHONDONTWRITEBYTECODE: "1",
316
+ }
317
+ : {};
318
+ }
319
+ function shellEnvironment(
320
+ options: ShellRunOptions,
321
+ runtime: ShellRuntime,
322
+ ): Record<string, string> {
323
+ return {
324
+ ...runtime.environment,
325
+ ...candidateArtifactEnvironment(options.candidateArtifactRoots),
326
+ ...(options.candidateTranscriptRoot
327
+ ? { SEVRO_CANDIDATE_TRANSCRIPTS: options.candidateTranscriptRoot }
328
+ : {}),
329
+ PATH: [
330
+ options.fixtureBinDir,
331
+ options.toolchainBinDir,
332
+ runtime.environment.PATH ?? "/usr/bin:/bin:/usr/sbin:/sbin",
333
+ ]
334
+ .filter(Boolean)
335
+ .join(delimiter),
336
+ HOME: runtime.home,
337
+ TMPDIR: runtime.temp,
338
+ LANG: process.env.LANG ?? "C",
339
+ GIT_CONFIG_GLOBAL: "/dev/null",
340
+ GIT_CONFIG_NOSYSTEM: "1",
341
+ ...shellRuntimeEnvironment(runtime, options.uvRuntimeCache),
342
+ };
343
+ }
344
+ async function requireShellRuntimeCache(
345
+ options: ShellRunOptions,
346
+ runtime: ShellRuntime,
347
+ ): Promise<void> {
348
+ if (options.uvRuntimeCache && !(await stat(runtime.uvCache)).isDirectory())
349
+ throw new Error("isolated UV cache is missing");
350
+ }
351
+ function shellStdout(
352
+ check: PreparedShellCheck,
353
+ output: Bun.Subprocess["stdout"],
354
+ ): ReadableStream<Uint8Array> | null {
355
+ if (!check.captureStdout) return null;
356
+ if (!(output instanceof ReadableStream))
357
+ throw new Error("shell stdout pipe is unavailable");
358
+ return output;
359
+ }
360
+ class ShellExecution {
361
+ private proc?: Bun.Subprocess;
362
+ private readonly deadline: ProcessDeadline;
363
+ constructor(
364
+ private check: PreparedShellCheck,
365
+ private options: ShellRunOptions,
366
+ private isolation: Isolation,
367
+ private runtime: ShellRuntime,
368
+ ) {
369
+ this.deadline = new ProcessDeadline({
370
+ timeoutMs: check.timeoutMs,
371
+ signal: options.signal,
372
+ interruption: (reason) =>
373
+ new Error(
374
+ reason === "timeout"
375
+ ? "shell check timed out"
376
+ : "shell check cancelled",
377
+ ),
378
+ });
379
+ }
380
+ async run(): Promise<ShellCheckResult> {
163
381
  try {
164
- process.kill(-proc.pid, "SIGKILL");
165
- return;
166
- } catch {
167
- // The group may already be gone.
382
+ const running = this.spawn();
383
+ const stream = shellStdout(this.check, running.stdout);
384
+ return await this.deadline.wait(() =>
385
+ Promise.all([
386
+ running.exited,
387
+ stream ? boundedOutput(stream) : Promise.resolve(null),
388
+ ]).then(([exitCode, stdout]) => ({ exitCode, stdout })),
389
+ );
390
+ } catch (error) {
391
+ await this.stop();
392
+ throw error;
393
+ } finally {
394
+ this.deadline.dispose();
395
+ await this.isolation.release();
168
396
  }
169
397
  }
170
- try {
171
- proc.kill("SIGKILL");
172
- } catch {
173
- // The process already exited.
398
+ private spawn() {
399
+ this.proc = Bun.spawn(this.isolation.argv, {
400
+ cwd: this.options.workspace,
401
+ env: shellEnvironment(this.options, this.runtime),
402
+ detached: true,
403
+ stdin: "ignore",
404
+ stdout: this.check.captureStdout ? "pipe" : "ignore",
405
+ stderr: "ignore",
406
+ });
407
+ return this.proc;
408
+ }
409
+ private async stop(): Promise<void> {
410
+ const proc = this.proc;
411
+ if (!proc) return;
412
+ terminateOwnedProcessGroup(proc);
413
+ await proc.exited;
174
414
  }
175
415
  }
176
416
 
177
417
  /** Run a check without inherited credentials or retaining raw process output. */
178
418
  export async function runShellCheck(
179
419
  check: PreparedShellCheck,
180
- options: {
181
- workspace: string;
182
- fixtureBinDir?: string;
183
- toolchainBinDir?: string;
184
- uvRuntimeCache?: boolean;
185
- protectedRoots: string[];
186
- protectedRootsCanonical?: boolean;
187
- privateStateRoot: string;
188
- signal?: AbortSignal;
189
- },
420
+ options: ShellRunOptions,
190
421
  ): Promise<ShellCheckResult> {
191
422
  if (options.signal?.aborted) throw new Error("shell check cancelled");
192
- const runtimeRoot = join(options.workspace, ".git", "sevro-runtime");
193
- const home = join(runtimeRoot, "check-home");
194
- const temp = join(runtimeRoot, "check-tmp");
195
- await Promise.all([
196
- mkdir(home, { recursive: true, mode: 0o700 }),
197
- mkdir(temp, { recursive: true, mode: 0o700 }),
198
- ]);
199
- const isolated = await prepareMacSandboxCommand({
423
+ const runtime = await shellRuntime(options);
424
+ const isolated = await prepareIsolatedCommand({
200
425
  argv: ["/bin/sh", "-e", "-c", check.run],
201
426
  workspace: options.workspace,
202
- protectedRoots: options.protectedRoots,
427
+ protectedRoots: [...options.protectedRoots, ...shellSeedSources(options)],
203
428
  protectedRootsCanonical: options.protectedRootsCanonical,
204
429
  privateStateRoot: options.privateStateRoot,
205
430
  denyNetwork: true,
431
+ localHttp: runtimeLocalHttpEnabled(options.runtimePolicy, "checks"),
432
+ readOnlyRoots: options.runtimePolicy?.readOnlyRoots,
433
+ candidateArtifactRoots: options.candidateArtifactRoots,
434
+ transcriptReadRoots: options.candidateTranscriptRoot
435
+ ? [options.candidateTranscriptRoot]
436
+ : [],
437
+ writableRuntimeRoot: runtime.privateRuntimeRoot,
438
+ writableCacheRoots: runtime.writableCacheRoots,
206
439
  });
207
- const uvCache = join(runtimeRoot, "uv-cache");
208
- if (options.uvRuntimeCache && !(await stat(uvCache)).isDirectory())
209
- throw new Error("isolated UV cache is missing");
210
- let proc: Bun.Subprocess | undefined;
211
- let timer: ReturnType<typeof setTimeout> | undefined;
212
- let cancel: (() => void) | undefined;
213
- try {
214
- proc = Bun.spawn(isolated.argv, {
215
- cwd: options.workspace,
216
- env: {
217
- PATH: [
218
- options.fixtureBinDir,
219
- options.toolchainBinDir,
220
- "/usr/bin:/bin:/usr/sbin:/sbin",
221
- ]
222
- .filter(Boolean)
223
- .join(delimiter),
224
- HOME: home,
225
- TMPDIR: temp,
226
- LANG: process.env.LANG ?? "C",
227
- GIT_CONFIG_GLOBAL: "/dev/null",
228
- GIT_CONFIG_NOSYSTEM: "1",
229
- ...(options.uvRuntimeCache
230
- ? {
231
- UV_CACHE_DIR: uvCache,
232
- UV_PROJECT_ENVIRONMENT: join(runtimeRoot, "project-environment"),
233
- UV_OFFLINE: "1",
234
- PYTHONDONTWRITEBYTECODE: "1",
235
- }
236
- : {}),
237
- },
238
- detached: true,
239
- stdin: "ignore",
240
- stdout: check.captureStdout ? "pipe" : "ignore",
241
- stderr: "ignore",
242
- });
243
- const running = proc;
244
- const outputStream = running.stdout;
245
- if (check.captureStdout && !(outputStream instanceof ReadableStream))
246
- throw new Error("shell stdout pipe is unavailable");
247
- const timeout = new Promise<never>((_resolve, reject) => {
248
- timer = setTimeout(
249
- () => reject(new Error("shell check timed out")),
250
- check.timeoutMs,
251
- );
252
- });
253
- const aborted = new Promise<never>((_resolve, reject) => {
254
- if (!options.signal) return;
255
- cancel = () => reject(new Error("shell check cancelled"));
256
- if (options.signal.aborted) cancel();
257
- else options.signal.addEventListener("abort", cancel, { once: true });
258
- });
259
- const completed = Promise.all([
260
- running.exited,
261
- check.captureStdout
262
- ? boundedOutput(outputStream as ReadableStream<Uint8Array>)
263
- : Promise.resolve(null),
264
- ]).then(([exitCode, stdout]) => ({ exitCode, stdout }));
265
- return await Promise.race([completed, timeout, aborted]);
266
- } catch (error) {
267
- if (proc) {
268
- stopProcess(proc);
269
- await proc.exited;
270
- }
271
- throw error;
272
- } finally {
273
- if (timer) clearTimeout(timer);
274
- if (cancel) options.signal?.removeEventListener("abort", cancel);
275
- await isolated.release();
276
- }
440
+ await requireShellRuntimeCache(options, runtime);
441
+ return new ShellExecution(check, options, isolated, runtime).run();
442
+ }
443
+
444
+ function shellSeedSources(options: ShellRunOptions): string[] {
445
+ return [
446
+ ...runtimeSeedSources(options.runtimePolicy),
447
+ ...sharedCacheDenyRoots(options.runtimePolicy, "checks"),
448
+ ];
277
449
  }
@@ -0,0 +1,52 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { isRecord } from "./value-guards";
3
+
4
+ function credentialStrings(value: unknown): string[] {
5
+ const pending: unknown[] = [value];
6
+ const secrets: string[] = [];
7
+ for (let visited = 0; visited < 256; visited++) {
8
+ const item = pending.pop();
9
+ if (typeof item === "string") {
10
+ if (item.length >= 8) secrets.push(item);
11
+ } else if (isRecord(item)) pending.push(...Object.values(item));
12
+ }
13
+ return secrets;
14
+ }
15
+
16
+ /** Read credentials only for scrubbing; failures belong to the actual host preparation. */
17
+ export async function diagnosticSecrets(path: string): Promise<string[]> {
18
+ try {
19
+ if ((await stat(path)).size > 1024 * 1024) return [];
20
+ const text = await readFile(path, "utf8");
21
+ try {
22
+ const value: unknown = JSON.parse(text);
23
+ return credentialStrings(value);
24
+ } catch {
25
+ return text.trim() ? [text.trim()] : [];
26
+ }
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+
32
+ export function redactDiagnosticText(text: string, secrets: string[]): string {
33
+ for (const secret of secrets) {
34
+ text = text.replaceAll(secret, "[redacted]");
35
+ text = redactCredentialSuffix(text, secret);
36
+ }
37
+ return text
38
+ .replace(/(authorization\s*:\s*)[^\r\n]+/gi, "$1[redacted]")
39
+ .replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
40
+ .replace(/\bsk-[A-Za-z0-9_-]+/g, "[redacted]")
41
+ .replace(
42
+ /((?:api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[=:]\s*)\S+/gi,
43
+ "$1[redacted]",
44
+ );
45
+ }
46
+
47
+ function redactCredentialSuffix(text: string, secret: string): string {
48
+ const start = text.lastIndexOf(secret.slice(0, Math.min(8, secret.length)));
49
+ if (start >= 0 && secret.startsWith(text.slice(start)))
50
+ return text.slice(0, start) + "[redacted]";
51
+ return text;
52
+ }