@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
@@ -0,0 +1,403 @@
1
+ import { runtimeSeedSources } from "../runtime/config";
2
+ import {
3
+ candidateArtifactEnvironment,
4
+ candidateArtifactReadRoots,
5
+ } from "../candidate-artifacts";
6
+ import {
7
+ cp,
8
+ mkdir,
9
+ realpath,
10
+ stat,
11
+ symlink,
12
+ writeFile,
13
+ } from "node:fs/promises";
14
+ import { delimiter, dirname, join } from "node:path";
15
+ import type { HostAdapter } from "../engine";
16
+ import { nativeStateParent } from "../native-transcript-state";
17
+ import {
18
+ workspaceBoundaryException,
19
+ workspaceStatePath,
20
+ } from "../fixtures/workspace-state";
21
+ import {
22
+ candidateTranscriptEnvironment,
23
+ candidateTranscriptRoots,
24
+ } from "../native-transcripts";
25
+ import { prepareHookMounts, runtimeHooksEnabled } from "../runtime/hooks";
26
+ import {
27
+ insideRuntimeRoot as inside,
28
+ runtimeExecutableProtection,
29
+ } from "../runtime/paths";
30
+ import { prepareRuntimeState } from "../runtime/state";
31
+ import {
32
+ sharedCacheDenyRoots,
33
+ sharedCacheRoots,
34
+ } from "../runtime/shared-cache";
35
+ import { stageClaudeAuthentication } from "./claude-credential";
36
+ import { claudeHostSettings } from "./claude-settings";
37
+ import { evaluationProtectedRoots } from "./isolation-roots";
38
+ type Request = Parameters<HostAdapter["run"]>[0];
39
+ export interface ClaudeHostOptions {
40
+ binary: string;
41
+ model: string;
42
+ effort: string;
43
+ projectRoot: string;
44
+ resultsRoot: string;
45
+ additionalProtectedRoots: string[];
46
+ credentialFile?: string;
47
+ uvCacheDir?: string;
48
+ toolchainBinDir?: string;
49
+ projectSettings?: boolean;
50
+ timeoutMs?: number;
51
+ retainHostDiagnostics?: boolean;
52
+ }
53
+
54
+ function minimalRoots(roots: string[]): string[] {
55
+ return roots.filter(
56
+ (root) => !roots.some((other) => other !== root && inside(other, root)),
57
+ );
58
+ }
59
+
60
+ function claudeSeedSources(request: Request): string[] {
61
+ return runtimeSeedSources(request.runtimePolicy);
62
+ }
63
+
64
+ async function createClaudePrivateState(
65
+ stateRoot: string,
66
+ options: ClaudeHostOptions,
67
+ ) {
68
+ await mkdir(join(stateRoot, "private"), { mode: 0o700 });
69
+ const privateRoot = await realpath(join(stateRoot, "private"));
70
+ const home = join(stateRoot, "home"),
71
+ temp = join(stateRoot, "tmp");
72
+ await Promise.all([home, temp].map((path) => mkdir(path, { mode: 0o700 })));
73
+ const authentication = await stageClaudeAuthentication(
74
+ join(privateRoot, "config"),
75
+ options.credentialFile,
76
+ );
77
+ return {
78
+ privateRoot,
79
+ home,
80
+ temp,
81
+ authentication,
82
+ credential: authentication.credentialFile,
83
+ };
84
+ }
85
+
86
+ async function protectedClaudeRoots(
87
+ request: Request,
88
+ options: ClaudeHostOptions,
89
+ credential: string,
90
+ ) {
91
+ const candidates = await evaluationProtectedRoots({
92
+ ownedStateRoot: dirname(dirname(dirname(credential))),
93
+ workspace: request.workspace,
94
+ projectRoot: options.projectRoot,
95
+ resultsRoot: options.resultsRoot,
96
+ additionalRoots: [
97
+ ...options.additionalProtectedRoots,
98
+ ...claudeSeedSources(request),
99
+ ],
100
+ });
101
+ const roots = request.runtimePolicy ? candidates : minimalRoots(candidates);
102
+ const namespace = await nativeStateParent();
103
+ if (roots.some((root) => root !== namespace && inside(root, credential)))
104
+ throw new Error("Claude private state overlaps a protected root");
105
+ const binary = await realpath(options.binary);
106
+ const executableRoots = runtimeExecutableProtection(
107
+ [dirname(binary)],
108
+ roots,
109
+ Boolean(request.runtimePolicy),
110
+ );
111
+ if (executableRoots.some((root) => inside(root, binary)))
112
+ throw new Error("Claude executable resides inside a protected root");
113
+ return roots;
114
+ }
115
+
116
+ async function claudeToolchainDirectory(
117
+ options: ClaudeHostOptions,
118
+ roots: string[],
119
+ ): Promise<string | null> {
120
+ const directory = options.toolchainBinDir
121
+ ? await realpath(options.toolchainBinDir)
122
+ : null;
123
+ if (
124
+ directory &&
125
+ (roots.some((root) => inside(root, directory)) ||
126
+ !(await stat(directory)).isDirectory())
127
+ )
128
+ throw new Error("Claude toolchain directory is unavailable or protected");
129
+ return directory;
130
+ }
131
+
132
+ async function prepareClaudeRuntime(
133
+ options: ClaudeHostOptions,
134
+ roots: string[],
135
+ workspace: string,
136
+ ): Promise<string | null> {
137
+ if (!options.uvCacheDir) return null;
138
+ const cache = await realpath(options.uvCacheDir);
139
+ if (
140
+ roots.some((root) => inside(root, cache)) ||
141
+ !(await stat(cache)).isDirectory()
142
+ )
143
+ throw new Error("Claude UV cache is unavailable or protected");
144
+ const runtimeRoot = join(workspace, ".git", "sevro-runtime");
145
+ await mkdir(runtimeRoot, { recursive: true, mode: 0o700 });
146
+ await cp(cache, join(runtimeRoot, "uv-cache"), {
147
+ recursive: true,
148
+ force: false,
149
+ errorOnExist: true,
150
+ });
151
+ return runtimeRoot;
152
+ }
153
+
154
+ function claudeEnvironment(
155
+ state: Awaited<ReturnType<typeof createClaudePrivateState>>,
156
+ request: Request,
157
+ toolchain: string | null,
158
+ runtime: string | null,
159
+ ): Record<string, string> {
160
+ return {
161
+ ...state.authentication.environment,
162
+ ...request.runtimePolicy?.environment,
163
+ CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
164
+ PATH: claudePath(request, toolchain),
165
+ LANG: process.env.LANG ?? "C",
166
+ HOME: runtime ? join(runtime, "host-home") : state.home,
167
+ TMPDIR: state.temp,
168
+ CLAUDE_CONFIG_DIR: dirname(state.credential),
169
+ NO_COLOR: "1",
170
+ ...claudeRuntimeEnvironment(runtime),
171
+ };
172
+ }
173
+
174
+ function claudePath(request: Request, toolchain: string | null): string {
175
+ const path =
176
+ request.runtimePolicy?.environment.PATH ??
177
+ process.env.PATH ??
178
+ "/usr/bin:/bin";
179
+ return [request.fixtureBinDir, toolchain, path]
180
+ .filter(Boolean)
181
+ .join(delimiter);
182
+ }
183
+
184
+ function claudeRuntimeEnvironment(
185
+ runtime: string | null,
186
+ ): Record<string, string> {
187
+ return runtime
188
+ ? {
189
+ UV_CACHE_DIR: join(runtime, "uv-cache"),
190
+ UV_PROJECT_ENVIRONMENT: join(runtime, "project-environment"),
191
+ UV_OFFLINE: "1",
192
+ PYTHONDONTWRITEBYTECODE: "1",
193
+ }
194
+ : {};
195
+ }
196
+
197
+ export async function prepareClaudeState(
198
+ stateRoot: string,
199
+ options: ClaudeHostOptions,
200
+ request: Request,
201
+ pluginDirs: string[],
202
+ ) {
203
+ await requireClaudeRuntimeNamespace(request, stateRoot);
204
+ const state = await createClaudePrivateState(stateRoot, options);
205
+ const transcripts = await claudeTranscriptAccess(
206
+ stateRoot,
207
+ state.credential,
208
+ request,
209
+ );
210
+ const roots = await protectedClaudeRoots(request, options, state.credential);
211
+ const toolchain = await claudeToolchainDirectory(options, roots);
212
+ const runtime = await prepareClaudeRuntime(options, roots, request.workspace);
213
+ const settingsPath = join(state.privateRoot, "settings.json");
214
+ const hooks = await claudeHookMounts(
215
+ request,
216
+ stateRoot,
217
+ state.privateRoot,
218
+ pluginDirs,
219
+ roots,
220
+ );
221
+ const effectivePlugins = hooks?.directories ?? pluginDirs;
222
+ const env = claudeEnvironment(state, request, toolchain, runtime);
223
+ const runtimeWriteRoot = await applyClaudeRuntimeEnvironment(
224
+ request,
225
+ env,
226
+ runtime,
227
+ stateRoot,
228
+ );
229
+ const cacheRoots = await sharedCacheRoots(
230
+ request.runtimePolicy,
231
+ request.runtimeRole,
232
+ roots,
233
+ );
234
+ await writeFile(
235
+ settingsPath,
236
+ JSON.stringify(
237
+ claudeHostSettings(
238
+ state.privateRoot,
239
+ state.credential,
240
+ [...pluginDirs, ...effectivePlugins],
241
+ [
242
+ ...roots,
243
+ ...sharedCacheDenyRoots(request.runtimePolicy, request.runtimeRole),
244
+ ],
245
+ transcripts.policy,
246
+ [
247
+ ...transcripts.readRoots,
248
+ ...candidateTranscriptRoots(request.candidateTranscriptRoot),
249
+ ...candidateArtifactReadRoots(request.candidateArtifactRoots),
250
+ ],
251
+ runtimeWriteRoot
252
+ ? { root: runtimeWriteRoot, workspace: request.workspace, cacheRoots }
253
+ : undefined,
254
+ request.workspace,
255
+ ),
256
+ ),
257
+ { flag: "wx", mode: 0o600 },
258
+ );
259
+ Object.assign(env, transcripts.environment);
260
+ Object.assign(
261
+ env,
262
+ candidateTranscriptEnvironment(request.candidateTranscriptRoot),
263
+ candidateArtifactEnvironment(request.candidateArtifactRoots),
264
+ );
265
+ if (runtime) await mkdir(join(runtime, "host-home"), { mode: 0o700 });
266
+ return {
267
+ credential: state.credential,
268
+ settingsPath,
269
+ env,
270
+ hooks,
271
+ pluginDirs: effectivePlugins,
272
+ };
273
+ }
274
+
275
+ /** Claude's implicit /tmp/claude writes cannot contain the read-denied peer namespace. */
276
+ async function requireClaudeRuntimeNamespace(
277
+ request: Request,
278
+ stateRoot: string,
279
+ ) {
280
+ if (
281
+ !request.runtimePolicy &&
282
+ !workspaceBoundaryException(workspaceStatePath(), request.workspace).length
283
+ )
284
+ return;
285
+ const implicitRoots = await Promise.all(
286
+ ["/tmp/claude", "/private/tmp/claude"].map(implicitClaudeWritePaths),
287
+ );
288
+ const namespace = dirname(stateRoot);
289
+ if (
290
+ implicitRoots
291
+ .flat()
292
+ .some((root) => inside(root, namespace) || inside(namespace, root))
293
+ )
294
+ throw new Error(
295
+ "Claude namespace overlaps an implicit native writable root",
296
+ );
297
+ }
298
+
299
+ async function implicitClaudeWritePaths(root: string): Promise<string[]> {
300
+ try {
301
+ return [root, await realpath(root)];
302
+ } catch (cause) {
303
+ if ((cause as NodeJS.ErrnoException).code !== "ENOENT")
304
+ throw new Error("Claude implicit native writable root is unreadable", {
305
+ cause,
306
+ });
307
+ return [root];
308
+ }
309
+ }
310
+
311
+ async function claudeTranscriptAccess(
312
+ stateRoot: string,
313
+ credential: string,
314
+ request: Request,
315
+ ) {
316
+ const policy = request.runtimePolicy;
317
+ if (!policy?.nativeTranscripts)
318
+ return { policy, environment: {}, readRoots: [] };
319
+ const view = join(stateRoot, "native-home");
320
+ const projects = join(view, "projects");
321
+ await mkdir(projects, { recursive: true, mode: 0o700 });
322
+ await symlink(projects, join(dirname(credential), "projects"));
323
+ const startup = join(view, "shell-env");
324
+ const script = `export CLAUDE_CONFIG_DIR='${view.replaceAll("'", `'"'"'`)}'\n`;
325
+ await writeFile(startup, script, { flag: "wx", mode: 0o600 });
326
+ await writeFile(join(view, ".zshenv"), script, { flag: "wx", mode: 0o600 });
327
+ return {
328
+ policy,
329
+ readRoots: [projects, view],
330
+ environment: {
331
+ SEVRO_NATIVE_TRANSCRIPT_ROOT: projects,
332
+ BASH_ENV: startup,
333
+ ZDOTDIR: view,
334
+ },
335
+ };
336
+ }
337
+
338
+ async function claudeHookMounts(
339
+ request: Request,
340
+ stateRoot: string,
341
+ privateRoot: string,
342
+ pluginDirs: string[],
343
+ roots: string[],
344
+ ) {
345
+ const policy = request.runtimePolicy;
346
+ if (!policy || !runtimeHooksEnabled(policy)) return undefined;
347
+ return prepareHookMounts({
348
+ workspace: request.workspace,
349
+ stateRoot,
350
+ privateRoot,
351
+ pluginRoots: pluginDirs,
352
+ protectedRoots: [privateRoot, ...roots, ...runtimeSeedSources(policy)],
353
+ policy,
354
+ });
355
+ }
356
+
357
+ async function applyClaudeRuntimeEnvironment(
358
+ request: Request,
359
+ env: Record<string, string>,
360
+ runtime: string | null,
361
+ stateRoot: string,
362
+ ): Promise<string | undefined> {
363
+ const isolated = await prepareRuntimeState(
364
+ request.workspace,
365
+ claudeCommandPolicy(request),
366
+ request.runtimeRole ?? "candidate",
367
+ claudeRuntimeDirectory(request, stateRoot),
368
+ );
369
+ if (request.runtimePolicy) {
370
+ Object.assign(env, isolated.environment);
371
+ env.PATH = [request.fixtureBinDir, isolated.environment.PATH ?? env.PATH]
372
+ .filter(Boolean)
373
+ .join(delimiter);
374
+ return isolated.root;
375
+ }
376
+ applyLegacyClaudeCommandEnvironment(env, isolated, runtime);
377
+ return undefined;
378
+ }
379
+
380
+ function claudeRuntimeDirectory(request: Request, stateRoot: string) {
381
+ return request.runtimePolicy
382
+ ? (request.candidateRuntimeRoot ?? join(stateRoot, "runtime"))
383
+ : undefined;
384
+ }
385
+
386
+ function applyLegacyClaudeCommandEnvironment(
387
+ env: Record<string, string>,
388
+ isolated: Awaited<ReturnType<typeof prepareRuntimeState>>,
389
+ runtime: string | null,
390
+ ): void {
391
+ if (!runtime) env.HOME = isolated.home;
392
+ env.TMPDIR = isolated.temp;
393
+ }
394
+
395
+ function claudeCommandPolicy(request: Request) {
396
+ return (
397
+ request.runtimePolicy ?? {
398
+ format: "sevro.runtime.v1" as const,
399
+ environment: {},
400
+ readOnlyRoots: [],
401
+ }
402
+ );
403
+ }
@@ -1,20 +1,12 @@
1
- import { createHash } from "node:crypto";
1
+ import { sha256 } from "../identity";
2
+ import { isRecord } from "../value-guards";
2
3
  import { summarizeClaudeEvents } from "./claude-events";
3
4
 
4
5
  const MAX_RETAINED_CALLS = 128;
5
6
  type Entry = Record<string, unknown>;
6
7
 
7
- function record(value: unknown): value is Entry {
8
- return value !== null && typeof value === "object" && !Array.isArray(value);
9
- }
10
-
11
8
  function label(value: unknown, max = 256): string | null {
12
- return typeof value === "string" &&
13
- value.length >= 1 &&
14
- value.length <= max &&
15
- /^[A-Za-z0-9_.:-]+$/.test(value)
16
- ? value
17
- : null;
9
+ return typeof value === "string" && boundedLabel(value, max) ? value : null;
18
10
  }
19
11
 
20
12
  interface SkillCall {
@@ -49,111 +41,184 @@ export interface ClaudeToolCallsObservation {
49
41
  };
50
42
  }
51
43
 
44
+ interface CallState {
45
+ calls: Array<SkillCall | AgentCall>;
46
+ malformed: boolean;
47
+ truncated: boolean;
48
+ ordinal: number;
49
+ }
50
+ type Actor = { actor: "parent" | "nested"; parentToolUseId: string | null };
51
+
52
52
  /** Retain ordered host call metadata, never tool prompts or results. */
53
53
  export function claudeToolCallsObservation(
54
54
  stream: string,
55
55
  exitCode: number,
56
56
  ): ClaudeToolCallsObservation {
57
57
  const summary = summarizeClaudeEvents(stream, exitCode);
58
- const calls: Array<SkillCall | AgentCall> = [];
59
- let malformed = false;
60
- let truncated = false;
61
- let ordinal = 0;
58
+ const state: CallState = {
59
+ calls: [],
60
+ malformed: false,
61
+ truncated: false,
62
+ ordinal: 0,
63
+ };
62
64
  for (const line of stream.split("\n")) {
63
65
  if (!line.trim()) continue;
64
- let event: unknown;
65
- try {
66
- event = JSON.parse(line);
67
- } catch {
68
- malformed = true;
69
- continue;
70
- }
71
- if (!record(event) || event.type !== "assistant") continue;
72
- const blocks = record(event.message) ? event.message.content : null;
73
- if (!Array.isArray(blocks)) {
74
- malformed = true;
75
- continue;
76
- }
77
- const parentToolUseId = label(event.parent_tool_use_id, 128);
78
- const actor = parentToolUseId ? "nested" : "parent";
79
- if (
80
- event.parent_tool_use_id !== undefined &&
81
- event.parent_tool_use_id !== null &&
82
- actor === "parent"
83
- )
84
- malformed = true;
85
- for (const value of blocks) {
86
- if (!record(value) || value.type !== "tool_use") continue;
87
- ordinal++;
88
- if (!["Skill", "Agent", "Task"].includes(String(value.name))) continue;
89
- if (calls.length >= MAX_RETAINED_CALLS) {
90
- truncated = true;
91
- break;
92
- }
93
- const input = record(value.input) ? value.input : null;
94
- if (value.name === "Skill") {
95
- const invocation = label(input?.skill);
96
- if (!invocation) {
97
- malformed = true;
98
- continue;
99
- }
100
- calls.push({
101
- ordinal,
102
- actor,
103
- parentToolUseId,
104
- name: "Skill",
105
- skill: invocation.split(":").at(-1)!,
106
- invocation,
107
- });
108
- continue;
109
- }
110
- const toolUseId = label(value.id, 128);
111
- const subagentType = label(input?.subagent_type ?? input?.subagentType);
112
- const runInBackground =
113
- typeof input?.run_in_background === "boolean"
114
- ? input.run_in_background
115
- : null;
116
- const model = input?.model === undefined ? null : label(input.model, 128);
117
- const boundedPrompt =
118
- typeof input?.prompt === "string" &&
119
- Buffer.byteLength(input.prompt, "utf8") <= 1024 * 1024
120
- ? input.prompt
121
- : null;
122
- const promptSha256 = boundedPrompt
123
- ? createHash("sha256").update(boundedPrompt).digest("hex")
124
- : null;
125
- const promptFirstLineSha256 = boundedPrompt
126
- ? createHash("sha256")
127
- .update(boundedPrompt.split(/\r?\n/, 1)[0]!)
128
- .digest("hex")
129
- : null;
130
- if (
131
- !toolUseId ||
132
- !subagentType ||
133
- runInBackground === null ||
134
- (input?.model !== undefined && !model) ||
135
- !promptSha256
136
- )
137
- malformed = true;
138
- calls.push({
139
- ordinal,
140
- actor,
141
- parentToolUseId,
142
- name: "Agent",
143
- toolUseId,
144
- subagentType,
145
- runInBackground,
146
- model,
147
- promptSha256,
148
- promptFirstLineSha256,
149
- });
150
- }
151
- if (truncated) break;
66
+ collectEvent(line, state);
67
+ if (state.truncated) break;
152
68
  }
153
69
  return {
154
70
  id: "sevro.claude.tool-calls",
155
- completeness:
156
- summary.complete && !malformed && !truncated ? "complete" : "partial",
157
- data: { method: "stream_tool_calls", calls, truncated },
71
+ completeness: callCompleteness(summary.complete, state),
72
+ data: {
73
+ method: "stream_tool_calls",
74
+ calls: state.calls,
75
+ truncated: state.truncated,
76
+ },
158
77
  };
159
78
  }
79
+
80
+ function callCompleteness(
81
+ complete: boolean,
82
+ state: CallState,
83
+ ): "complete" | "partial" {
84
+ return complete && !state.malformed && !state.truncated
85
+ ? "complete"
86
+ : "partial";
87
+ }
88
+
89
+ function collectEvent(line: string, state: CallState): void {
90
+ let event: unknown;
91
+ try {
92
+ event = JSON.parse(line);
93
+ } catch {
94
+ state.malformed = true;
95
+ return;
96
+ }
97
+ if (!isRecord(event) || event.type !== "assistant") return;
98
+ collectAssistant(event, state);
99
+ }
100
+
101
+ function collectAssistant(event: Entry, state: CallState): void {
102
+ const blocks: unknown = isRecord(event.message)
103
+ ? event.message.content
104
+ : null;
105
+ if (!Array.isArray(blocks)) {
106
+ state.malformed = true;
107
+ return;
108
+ }
109
+ const actor = actorContext(event, state);
110
+ for (const block of blocks) {
111
+ collectBlock(block, actor, state);
112
+ if (state.truncated) break;
113
+ }
114
+ }
115
+
116
+ function actorContext(event: Entry, state: CallState): Actor {
117
+ const parentToolUseId = label(event.parent_tool_use_id, 128);
118
+ const actor = parentToolUseId ? "nested" : "parent";
119
+ if (
120
+ event.parent_tool_use_id !== undefined &&
121
+ event.parent_tool_use_id !== null &&
122
+ actor === "parent"
123
+ )
124
+ state.malformed = true;
125
+ return { actor, parentToolUseId };
126
+ }
127
+
128
+ function collectBlock(value: unknown, actor: Actor, state: CallState): void {
129
+ if (!isRecord(value) || value.type !== "tool_use") return;
130
+ state.ordinal++;
131
+ if (!["Skill", "Agent", "Task"].includes(String(value.name))) return;
132
+ if (state.calls.length >= MAX_RETAINED_CALLS) {
133
+ state.truncated = true;
134
+ return;
135
+ }
136
+ collectSupportedCall(value, actor, state);
137
+ }
138
+
139
+ function collectSupportedCall(
140
+ value: Entry,
141
+ actor: Actor,
142
+ state: CallState,
143
+ ): void {
144
+ const input = isRecord(value.input) ? value.input : {};
145
+ if (value.name === "Skill") {
146
+ collectSkill(input, actor, state);
147
+ return;
148
+ }
149
+ const call = agentCall(value, input, actor, state.ordinal);
150
+ if (!validAgentCall(call, input)) state.malformed = true;
151
+ state.calls.push(call);
152
+ }
153
+
154
+ function collectSkill(input: Entry, actor: Actor, state: CallState): void {
155
+ const invocation = label(input.skill);
156
+ if (!invocation) {
157
+ state.malformed = true;
158
+ return;
159
+ }
160
+ state.calls.push({
161
+ ordinal: state.ordinal,
162
+ ...actor,
163
+ name: "Skill",
164
+ skill: invocation.slice(invocation.lastIndexOf(":") + 1),
165
+ invocation,
166
+ });
167
+ }
168
+
169
+ function boundedPrompt(input: Entry): string | null {
170
+ if (typeof input.prompt !== "string") return null;
171
+ return Buffer.byteLength(input.prompt, "utf8") <= 1024 * 1024
172
+ ? input.prompt
173
+ : null;
174
+ }
175
+
176
+ function promptDigest(prompt: string | null): string | null {
177
+ return prompt ? sha256(prompt) : null;
178
+ }
179
+
180
+ function firstLineDigest(prompt: string | null): string | null {
181
+ return prompt ? sha256(prompt.split(/\r?\n/, 1).join("")) : null;
182
+ }
183
+
184
+ function agentCall(
185
+ value: Entry,
186
+ input: Entry,
187
+ actor: Actor,
188
+ ordinal: number,
189
+ ): AgentCall {
190
+ const prompt = boundedPrompt(input);
191
+ return {
192
+ ordinal,
193
+ ...actor,
194
+ name: "Agent",
195
+ toolUseId: label(value.id, 128),
196
+ subagentType: label(input.subagent_type ?? input.subagentType),
197
+ runInBackground:
198
+ typeof input.run_in_background === "boolean"
199
+ ? input.run_in_background
200
+ : null,
201
+ model: input.model === undefined ? null : label(input.model, 128),
202
+ promptSha256: promptDigest(prompt),
203
+ promptFirstLineSha256: firstLineDigest(prompt),
204
+ };
205
+ }
206
+
207
+ function validAgentCall(call: AgentCall, input: Entry): boolean {
208
+ const required = [call.toolUseId, call.subagentType, call.promptSha256];
209
+ return (
210
+ required.every(Boolean) &&
211
+ call.runInBackground !== null &&
212
+ validAgentModel(input, call.model)
213
+ );
214
+ }
215
+
216
+ function validAgentModel(input: Entry, model: string | null): boolean {
217
+ return input.model === undefined || model !== null;
218
+ }
219
+
220
+ function boundedLabel(value: string, max: number): boolean {
221
+ return (
222
+ value.length >= 1 && value.length <= max && /^[A-Za-z0-9_.:-]+$/.test(value)
223
+ );
224
+ }