@bjoernrochel/sevro 0.1.0-rc.7 → 1.0.0

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 (230) hide show
  1. package/CONTRIBUTING.md +22 -6
  2. package/README.md +5 -1
  3. package/docs/README.md +19 -15
  4. package/docs/architecture.md +138 -17
  5. package/docs/assets/README.md +5 -4
  6. package/docs/builtin-graders-v1.md +17 -3
  7. package/docs/codex-host.md +155 -49
  8. package/docs/contracts.md +2 -1
  9. package/docs/creating-cases.md +9 -2
  10. package/docs/development-cli.md +187 -19
  11. package/docs/documentation-quality.md +12 -2
  12. package/docs/evaluation-workflow.md +110 -0
  13. package/docs/extension-protocol-v1.md +26 -3
  14. package/docs/getting-started.md +23 -2
  15. package/docs/guide-evaluation.md +33 -1
  16. package/docs/identity-v1.md +25 -2
  17. package/docs/installing.md +11 -11
  18. package/docs/native-controls.md +76 -5
  19. package/docs/native-hosts.md +196 -7
  20. package/docs/reading-results.md +102 -0
  21. package/docs/recipes.md +489 -0
  22. package/docs/releases.md +31 -12
  23. package/docs/report-v1.md +29 -1
  24. package/docs/results-v1.md +141 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +307 -18
  27. package/docs/troubleshooting.md +12 -12
  28. package/docs/typescript-quality.md +89 -26
  29. package/examples/recipes/candidate.ts +26 -0
  30. package/examples/recipes/cases/coding-change.json +21 -0
  31. package/examples/recipes/cases/host-tools.json +21 -0
  32. package/examples/recipes/cases/mise.json +21 -0
  33. package/examples/recipes/cases/node.json +21 -0
  34. package/examples/recipes/cases/python-uv.json +21 -0
  35. package/examples/recipes/cases/semantic.json +17 -0
  36. package/examples/recipes/coding-change/check.ts +23 -0
  37. package/examples/recipes/extension-artifacts.ts +65 -0
  38. package/examples/recipes/extension-methods.ts +174 -0
  39. package/examples/recipes/extension.ts +37 -0
  40. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  41. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  42. package/examples/recipes/repository-skill/README.md +3 -0
  43. package/examples/recipes/run-inputs.ts +188 -0
  44. package/examples/recipes/run-options.ts +131 -0
  45. package/examples/recipes/run.ts +63 -0
  46. package/examples/recipes/runtime/empty.json +1 -0
  47. package/examples/recipes/runtime/goals.json +1 -0
  48. package/examples/recipes/runtime/mise.json +6 -0
  49. package/examples/recipes/runtime/path.json +1 -0
  50. package/examples/recipes/runtime/plugins.json +4 -0
  51. package/examples/recipes/runtime/python-uv.json +10 -0
  52. package/examples/recipes/semantic-judge.ts +29 -0
  53. package/examples/templates/coding/adapter.ts +17 -0
  54. package/examples/templates/coding/case.json +15 -0
  55. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  56. package/examples/templates/repository-skill/extension-data.ts +64 -0
  57. package/examples/templates/repository-skill/extension.ts +31 -0
  58. package/examples/templates/response/adapter.ts +12 -0
  59. package/examples/templates/response/case.json +13 -0
  60. package/package.json +11 -5
  61. package/schemas/report-v1.schema.json +9 -0
  62. package/schemas/run-evidence-v1.schema.json +2 -1
  63. package/schemas/runtime-v1.schema.json +63 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +206 -0
  66. package/src/candidate-codex-state.ts +185 -0
  67. package/src/case-validation.ts +73 -50
  68. package/src/cli/cancellation.ts +20 -0
  69. package/src/cli/command.ts +87 -0
  70. package/src/cli/completion.ts +15 -0
  71. package/src/cli/demo.ts +60 -0
  72. package/src/cli/evaluation.ts +55 -0
  73. package/src/cli/extension.ts +79 -0
  74. package/src/cli/help.ts +198 -0
  75. package/src/cli/hosts.ts +55 -0
  76. package/src/cli/init-settings.ts +30 -0
  77. package/src/cli/init.ts +111 -0
  78. package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
  79. package/src/cli/loading.ts +151 -0
  80. package/src/cli/preparation.ts +154 -0
  81. package/src/cli/presentation.ts +93 -0
  82. package/src/cli/progress.ts +6 -0
  83. package/src/cli/run-manifest.ts +128 -0
  84. package/src/cli/run-options.ts +403 -0
  85. package/src/cli/run-overrides.ts +69 -0
  86. package/src/cli/settings.ts +154 -0
  87. package/src/cli/summary.ts +43 -0
  88. package/src/cli.ts +13 -591
  89. package/src/contract-types.ts +94 -0
  90. package/src/engine.ts +13 -5
  91. package/src/evaluation/advisory.ts +150 -0
  92. package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
  93. package/src/evaluation/collection.ts +88 -0
  94. package/src/evaluation/context.ts +10 -0
  95. package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
  96. package/src/evaluation/grading.ts +235 -0
  97. package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
  98. package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
  99. package/src/evaluation/progress.ts +19 -0
  100. package/src/evaluation/resources.ts +241 -0
  101. package/src/evaluation/run.ts +206 -0
  102. package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +133 -115
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/evaluation/types.ts +207 -0
  105. package/src/evidence/host-results.ts +85 -0
  106. package/src/evidence/retained-artifacts.ts +18 -0
  107. package/src/evidence/trial-writer.ts +203 -0
  108. package/src/evidence/usage.ts +17 -0
  109. package/src/extension-client.ts +34 -96
  110. package/src/extension-session.ts +24 -48
  111. package/src/{advisory-fixture.ts → fixtures/advisory.ts} +15 -76
  112. package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
  113. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  114. package/src/fixtures/create.ts +183 -0
  115. package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
  116. package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
  117. package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
  118. package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
  119. package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
  120. package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
  121. package/src/fixtures/workspace-state.ts +212 -0
  122. package/src/generated/cli-result.d.cts +2 -1
  123. package/src/generated/extension.d.cts +2 -1
  124. package/src/generated/report.cjs +1 -1
  125. package/src/generated/report.d.cts +2 -1
  126. package/src/generated/run-evidence.cjs +1 -1
  127. package/src/generated/run-evidence.d.cts +2 -1
  128. package/src/generated/runtime.cjs +1 -1
  129. package/src/generated/runtime.d.cts +2 -1
  130. package/src/git.ts +93 -0
  131. package/src/graders/git-head.ts +29 -84
  132. package/src/graders/shell.ts +57 -68
  133. package/src/host-diagnostic-redaction.ts +52 -0
  134. package/src/host-diagnostics.ts +331 -0
  135. package/src/hosts/claude-continuation.ts +57 -40
  136. package/src/hosts/claude-credential.ts +160 -63
  137. package/src/hosts/claude-events.ts +34 -18
  138. package/src/hosts/claude-keychain.ts +78 -21
  139. package/src/hosts/claude-login-recovery.ts +85 -0
  140. package/src/hosts/claude-login-refresh.ts +178 -0
  141. package/src/hosts/claude-login-store.ts +162 -0
  142. package/src/hosts/claude-login.ts +107 -0
  143. package/src/hosts/claude-native-goal.ts +15 -12
  144. package/src/hosts/claude-process.ts +10 -50
  145. package/src/hosts/claude-refresh-lock.ts +81 -0
  146. package/src/hosts/claude-repository-invocation.ts +9 -34
  147. package/src/hosts/claude-settings.ts +91 -14
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +46 -16
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +122 -73
  152. package/src/hosts/codex-app-server.ts +78 -52
  153. package/src/hosts/codex-configuration.ts +5 -10
  154. package/src/hosts/codex-exec-controls.ts +516 -0
  155. package/src/hosts/codex-marketplace.ts +12 -10
  156. package/src/hosts/codex-native-calls.ts +15 -122
  157. package/src/hosts/codex-process.ts +10 -54
  158. package/src/hosts/codex-profile.ts +50 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +141 -19
  161. package/src/hosts/codex.ts +221 -179
  162. package/src/hosts/isolation-common.ts +15 -11
  163. package/src/hosts/isolation-roots.ts +13 -20
  164. package/src/hosts/linux-sandbox.ts +27 -3
  165. package/src/hosts/mac-sandbox.ts +111 -18
  166. package/src/hosts/native-controls.ts +72 -22
  167. package/src/hosts/native-request.ts +55 -0
  168. package/src/hosts/process-lifecycle.ts +221 -45
  169. package/src/hosts/protected-worktrees.ts +1 -1
  170. package/src/hosts/skill-hook-catalog.ts +131 -0
  171. package/src/hosts/skill-hook-handler.ts +374 -0
  172. package/src/hosts/skill-hooks.ts +577 -0
  173. package/src/instrumentation.ts +7 -8
  174. package/src/native-transcript-state.ts +22 -3
  175. package/src/native-transcript-view.ts +52 -13
  176. package/src/native-transcripts.ts +34 -15
  177. package/src/paths.ts +22 -0
  178. package/src/process-lifecycle.ts +150 -0
  179. package/src/provenance.ts +27 -7
  180. package/src/prune.ts +300 -0
  181. package/src/report.ts +63 -36
  182. package/src/resolved-case.ts +1 -1
  183. package/src/results-lock.ts +42 -0
  184. package/src/results.ts +14 -18
  185. package/src/run-owner.ts +4 -4
  186. package/src/{runtime-config.ts → runtime/config.ts} +182 -44
  187. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  188. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  189. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  190. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  191. package/src/{runtime-hooks.ts → runtime/hooks.ts} +33 -22
  192. package/src/{runtime-paths.ts → runtime/paths.ts} +6 -18
  193. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  194. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  195. package/src/runtime/shared-cache.ts +130 -0
  196. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  197. package/src/schema-types.ts +73 -161
  198. package/src/shell.ts +4 -0
  199. package/src/transcript-retention.ts +146 -0
  200. package/src/value-guards.ts +22 -0
  201. package/docs/assets/sevro-logo.png +0 -0
  202. package/docs/assets/sevro-symbol.svg +0 -1
  203. package/docs/assets/sevro-wordmark.svg +0 -1
  204. package/docs/cache-ownership-validation.md +0 -49
  205. package/docs/claude-continuation-validation.md +0 -47
  206. package/docs/concurrency-validation.md +0 -86
  207. package/docs/configuration-root-validation.md +0 -70
  208. package/docs/engine-slice.md +0 -114
  209. package/docs/evidence/runtime-startup-30/README.md +0 -185
  210. package/docs/evidence/runtime-startup-30/after.json +0 -319
  211. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  212. package/docs/evidence/runtime-startup-30/before.json +0 -403
  213. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  214. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  216. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  217. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  218. package/docs/evidence.md +0 -17
  219. package/docs/fixture-cleanup-validation.md +0 -70
  220. package/docs/guide-verification.json +0 -239
  221. package/src/evaluation-context.ts +0 -257
  222. package/src/evaluation-fixture.ts +0 -313
  223. package/src/evaluation-run.ts +0 -198
  224. package/src/evaluation-trial.ts +0 -946
  225. package/src/evaluation-types.ts +0 -149
  226. package/src/hosts/claude-nested-skills.ts +0 -272
  227. package/src/hosts/codex-command-output.ts +0 -510
  228. package/src/hosts/codex-executor-command.ts +0 -291
  229. package/src/hosts/codex-skill-reads.ts +0 -609
  230. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -1,21 +1,38 @@
1
1
  import { lstat, readFile, realpath } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
+ import type { ErrorObject } from "ajv";
3
4
  import {
4
5
  declaredRuntimeRoots,
5
6
  optionalRuntimeRoots,
6
7
  runtimePathDirectories,
7
8
  runtimeInheritedPath,
8
9
  requireRuntimeReadRoots,
9
- } from "./runtime-paths";
10
- import validate from "./generated/runtime.cjs";
11
- import { runtimeSeedDigest } from "./runtime-seeds";
10
+ } from "./paths";
11
+ import validate from "../generated/runtime.cjs";
12
+ import {
13
+ requireArtifactDirectories,
14
+ type RuntimeArtifactDirectory,
15
+ } from "../candidate-artifacts";
16
+ import { runtimeSeedDigest } from "./seeds";
17
+ import {
18
+ resolveSharedCaches,
19
+ requireSharedCachePolicy,
20
+ type SharedCacheDirectory,
21
+ } from "./shared-cache";
12
22
  import {
13
23
  discoverRuntimeSupport,
14
24
  type RuntimeDiscoverySource,
15
- } from "./runtime-discovery";
25
+ } from "./discovery";
16
26
 
17
- export type RuntimeRole =
18
- "candidate" | "checks" | "hooks" | "semantic" | "advisory";
27
+ /** Canonical roles; schemas:generate projects this list into the runtime schema. */
28
+ export const RUNTIME_ROLES = [
29
+ "candidate",
30
+ "checks",
31
+ "hooks",
32
+ "semantic",
33
+ "advisory",
34
+ ] as const;
35
+ export type RuntimeRole = (typeof RUNTIME_ROLES)[number];
19
36
  export interface RuntimeSeed {
20
37
  source: string;
21
38
  target: string;
@@ -23,15 +40,24 @@ export interface RuntimeSeed {
23
40
  }
24
41
  export interface RuntimeConfiguration {
25
42
  format: "sevro.runtime.v1";
43
+ codexState?: boolean;
44
+ network?: { localHttp?: boolean };
26
45
  nativeTranscripts?: boolean;
27
46
  environment?: { inherit?: string[]; set?: Record<string, string> };
28
47
  filesystem?: { readOnlyRoots?: string[]; optionalReadOnlyRoots?: string[] };
29
- runtime?: { seedDirectories?: RuntimeSeed[] };
48
+ runtime?: {
49
+ seedDirectories?: RuntimeSeed[];
50
+ sharedCacheDirectories?: SharedCacheDirectory[];
51
+ artifactDirectories?: RuntimeArtifactDirectory[];
52
+ };
30
53
  hooks?: { nativeGoal?: boolean; plugins?: string[] };
31
54
  }
32
55
 
33
56
  export interface RuntimePolicy {
34
57
  format: "sevro.runtime.v1";
58
+ codexState?: boolean;
59
+ network?: { localHttp?: boolean };
60
+ artifactDirectories?: RuntimeArtifactDirectory[];
35
61
  nativeTranscripts?: boolean;
36
62
  environment: Record<string, string>;
37
63
  readOnlyRoots: string[];
@@ -40,9 +66,18 @@ export interface RuntimePolicy {
40
66
  /** Original source denials retained when a derived role policy omits seeds. */
41
67
  seedReadDenyRoots?: string[];
42
68
  seeds?: (RuntimeSeed & { sha256: string })[];
69
+ sharedCacheDirectories?: SharedCacheDirectory[];
43
70
  hooks?: { nativeGoal?: boolean; plugins?: string[] };
44
71
  }
45
72
 
73
+ export function runtimeLocalHttpEnabled(
74
+ policy: RuntimePolicy | undefined,
75
+ role: RuntimeRole,
76
+ ): boolean {
77
+ const network = policy?.network;
78
+ return network?.localHttp === true && ["candidate", "checks"].includes(role);
79
+ }
80
+
46
81
  export function runtimeNativeGoalEnabled(
47
82
  policy: RuntimePolicy | undefined,
48
83
  ): boolean {
@@ -50,27 +85,64 @@ export function runtimeNativeGoalEnabled(
50
85
  return hooks?.nativeGoal === true;
51
86
  }
52
87
 
88
+ const validatedPolicies = new WeakSet<RuntimePolicy>();
89
+
90
+ export function runtimePolicySnapshot(policy: RuntimePolicy): RuntimePolicy;
91
+ export function runtimePolicySnapshot(
92
+ policy: RuntimePolicy | undefined,
93
+ ): RuntimePolicy | undefined;
53
94
  export function runtimePolicySnapshot(
54
95
  policy: RuntimePolicy | undefined,
55
96
  ): RuntimePolicy | undefined {
56
97
  if (!policy) return undefined;
57
- requireRuntimeEnvironment(policy.environment);
58
98
  const snapshot = structuredClone(policy);
99
+ if (!validatedPolicies.has(policy)) requireRuntimePolicy(snapshot);
100
+ Object.freeze(snapshot.network);
101
+ freezeArtifactDirectories(snapshot);
59
102
  Object.freeze(snapshot.environment);
60
103
  Object.freeze(snapshot.readOnlyRoots);
61
104
  freezeDiscovery(snapshot);
62
- for (const seed of snapshot.seeds ?? []) {
63
- Object.freeze(seed.roles);
64
- Object.freeze(seed);
65
- }
66
- Object.freeze(snapshot.seeds);
105
+ freezeRuntimeCaches(snapshot);
67
106
  Object.freeze(snapshot.seedReadDenyRoots);
68
107
  const hooks = snapshot.hooks;
69
108
  if (hooks) {
70
109
  Object.freeze(hooks.plugins);
71
110
  Object.freeze(hooks);
72
111
  }
73
- return Object.freeze(snapshot);
112
+ Object.freeze(snapshot);
113
+ validatedPolicies.add(snapshot);
114
+ return snapshot;
115
+ }
116
+
117
+ /** Loaded snapshots are already validated; mutable direct API inputs still need validation. */
118
+ export function requireRuntimePolicy(policy: RuntimePolicy): void {
119
+ if (validatedPolicies.has(policy)) return;
120
+ snapshotEnvironment([], policy.environment);
121
+ requireRuntimeCaches(policy.seeds ?? [], policy.sharedCacheDirectories ?? []);
122
+ requireSharedCachePolicy(policy);
123
+ requireArtifactDirectories(policy.artifactDirectories ?? []);
124
+ }
125
+
126
+ function requireRuntimeCaches(
127
+ seeds: RuntimeSeed[],
128
+ caches: SharedCacheDirectory[],
129
+ ): void {
130
+ requireRuntimeSeedTargets([...seeds, ...caches]);
131
+ for (const seed of seeds)
132
+ if (seed.roles !== undefined) requireRuntimeRoles(seed.roles, "seed");
133
+ for (const cache of caches) requireRuntimeRoles(cache.roles, "shared cache");
134
+ }
135
+
136
+ function freezeRuntimeCaches(snapshot: RuntimePolicy): void {
137
+ for (const seed of [
138
+ ...(snapshot.seeds ?? []),
139
+ ...(snapshot.sharedCacheDirectories ?? []),
140
+ ]) {
141
+ Object.freeze(seed.roles);
142
+ Object.freeze(seed);
143
+ }
144
+ Object.freeze(snapshot.seeds);
145
+ Object.freeze(snapshot.sharedCacheDirectories);
74
146
  }
75
147
 
76
148
  function freezeDiscovery(snapshot: RuntimePolicy): void {
@@ -82,14 +154,36 @@ function freezeDiscovery(snapshot: RuntimePolicy): void {
82
154
  Object.freeze(snapshot.skippedOptionalReadOnlyRoots);
83
155
  }
84
156
 
157
+ function freezeArtifactDirectories(snapshot: RuntimePolicy): void {
158
+ const directories = snapshot.artifactDirectories ?? [];
159
+ for (const directory of directories) Object.freeze(directory);
160
+ Object.freeze(snapshot.artifactDirectories);
161
+ }
162
+
85
163
  function assertRuntimeConfiguration(
86
164
  value: unknown,
87
165
  ): asserts value is RuntimeConfiguration {
88
- if (!validate(value)) throw new Error("invalid runtime configuration schema");
166
+ if (!validate(value)) {
167
+ // Ajv always populates errors when validation fails.
168
+ const issue = (validate.errors as [ErrorObject, ...ErrorObject[]])[0];
169
+ throw new Error(
170
+ `runtime configuration schema ${runtimeSchemaPath(issue)}: ${issue.message}`,
171
+ );
172
+ }
173
+ }
174
+
175
+ function runtimeSchemaPath(issue: ErrorObject): string {
176
+ const params = issue.params as Record<string, unknown>;
177
+ const property = params.additionalProperty ?? params.missingProperty;
178
+ const suffix =
179
+ typeof property === "string"
180
+ ? "/" + property.replaceAll("~", "~0").replaceAll("/", "~1")
181
+ : "";
182
+ return safeFilesystemPath(issue.instancePath + suffix || "/");
89
183
  }
90
184
 
91
185
  const RESERVED_ENVIRONMENT =
92
- /^(HOME|TMPDIR|TMP|TEMP|CODEX_HOME|CODEX_THREAD_ID|CLAUDE_CONFIG_DIR|CLAUDE_EFFORT|BASH_ENV|ENV|ZDOTDIR|NODE_OPTIONS|BUN_OPTIONS|UV_OFFLINE|GIT_CONFIG.*|LD_.*|DYLD_.*|CLAUDE_CODE_.*|SEVRO_NATIVE_.*|SEVRO_CANDIDATE_.*)$/i;
186
+ /^(HOME|TMPDIR|TMPPREFIX|TMP|TEMP|CODEX_HOME|CODEX_THREAD_ID|CLAUDE_CONFIG_DIR|CLAUDE_EFFORT|BASH_ENV|ENV|ZDOTDIR|NODE_OPTIONS|BUN_OPTIONS|UV_OFFLINE|GIT_CONFIG.*|LD_.*|DYLD_.*|CLAUDE_CODE_.*|SEVRO_CODEX_.*|SEVRO_NATIVE_.*|SEVRO_CANDIDATE_.*)$/i;
93
187
 
94
188
  function requireEnvironmentName(name: string): void {
95
189
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(name))
@@ -123,16 +217,7 @@ function snapshotEnvironment(
123
217
  return environment;
124
218
  }
125
219
 
126
- export function requireRuntimeEnvironment(
127
- environment: Record<string, string>,
128
- ): void {
129
- snapshotEnvironment([], environment);
130
- }
131
-
132
- export function requireRuntimeSeedTargets(
133
- seeds: NonNullable<RuntimePolicy["seeds"]>,
134
- ): void {
135
- for (const seed of seeds) requireRuntimeSeedRoles(seed.roles);
220
+ function requireRuntimeSeedTargets(seeds: RuntimeSeed[]): void {
136
221
  const targets = seeds.map((seed) => seed.target);
137
222
  if (
138
223
  targets.some(
@@ -159,13 +244,29 @@ export async function loadRuntimeConfiguration(
159
244
  const source = await readFile(canonical, "utf8");
160
245
  if (Buffer.byteLength(source) > 65536)
161
246
  throw new Error("runtime configuration exceeds 64 KiB");
162
- return await resolveRuntimeConfiguration(
163
- JSON.parse(source),
164
- dirname(canonical),
165
- protectedRoots,
247
+ return runtimePolicySnapshot(
248
+ await resolveRuntimeConfiguration(
249
+ parseRuntimeConfiguration(source),
250
+ dirname(canonical),
251
+ protectedRoots,
252
+ ),
166
253
  );
167
254
  } catch (cause) {
168
- throw new Error(runtimeConfigurationDiagnostic(cause), { cause });
255
+ throw new Error(
256
+ `${runtimeConfigurationDiagnostic(cause)} at ${safeFilesystemPath(path)}`,
257
+ { cause },
258
+ );
259
+ }
260
+ }
261
+
262
+ function parseRuntimeConfiguration(source: string): unknown {
263
+ try {
264
+ return JSON.parse(source);
265
+ } catch (cause) {
266
+ throw new Error(
267
+ "runtime configuration $: invalid JSON; check the JSON syntax",
268
+ { cause },
269
+ );
169
270
  }
170
271
  }
171
272
 
@@ -240,7 +341,7 @@ async function resolvedSeeds(
240
341
  environment: Record<string, string>,
241
342
  base: string,
242
343
  ) {
243
- const resolved = await Promise.all(
344
+ return Promise.all(
244
345
  seeds.map(async (seed) => {
245
346
  const roots = await declaredRuntimeRoots(
246
347
  [seed.source],
@@ -258,8 +359,6 @@ async function resolvedSeeds(
258
359
  };
259
360
  }),
260
361
  );
261
- requireRuntimeSeedTargets(resolved);
262
- return resolved;
263
362
  }
264
363
 
265
364
  async function resolveRuntimeConfiguration(
@@ -292,18 +391,53 @@ async function resolveRuntimeConfiguration(
292
391
  requireRuntimeReadRoots(readOnlyRoots, protectedRoots);
293
392
  return {
294
393
  format: value.format,
295
- ...(value.nativeTranscripts === undefined
296
- ? {}
297
- : { nativeTranscripts: value.nativeTranscripts }),
394
+ ...selectedRuntimeCapabilities(value),
395
+ ...artifactConfiguration(value.runtime),
298
396
  environment,
299
397
  readOnlyRoots: [...new Set(readOnlyRoots)].sort(),
300
398
  discovery: discovery.sources,
301
399
  skippedOptionalReadOnlyRoots: optional.skipped,
302
- seeds: await resolvedSeeds(configurationSeeds(value), environment, base),
400
+ ...(await resolvedRuntimeCaches(value, environment, base, [
401
+ ...protectedRoots,
402
+ ...readOnlyRoots,
403
+ ])),
303
404
  hooks: resolvedHooks(value.hooks),
304
405
  };
305
406
  }
306
407
 
408
+ async function resolvedRuntimeCaches(
409
+ value: RuntimeConfiguration,
410
+ environment: Record<string, string>,
411
+ base: string,
412
+ protectedRoots: string[],
413
+ ) {
414
+ const seeds = await resolvedSeeds(
415
+ configurationSeeds(value),
416
+ environment,
417
+ base,
418
+ );
419
+ const caches = await resolveSharedCaches(
420
+ value.runtime?.sharedCacheDirectories ?? [],
421
+ environment,
422
+ base,
423
+ [...protectedRoots, ...seeds.map((seed) => seed.source)],
424
+ );
425
+ return {
426
+ seeds,
427
+ ...(caches.length ? { sharedCacheDirectories: caches } : {}),
428
+ };
429
+ }
430
+
431
+ function selectedRuntimeCapabilities(value: RuntimeConfiguration) {
432
+ return {
433
+ ...(value.network === undefined ? {} : { network: value.network }),
434
+ ...(value.codexState === undefined ? {} : { codexState: value.codexState }),
435
+ ...(value.nativeTranscripts === undefined
436
+ ? {}
437
+ : { nativeTranscripts: value.nativeTranscripts }),
438
+ };
439
+ }
440
+
307
441
  async function resolvedRuntimeEnvironment(value: RuntimeConfiguration) {
308
442
  const { inherit, set } = configurationEnvironment(value);
309
443
  const environment = snapshotEnvironment(inherit, set);
@@ -318,6 +452,12 @@ function configurationEnvironment(value: RuntimeConfiguration) {
318
452
  const environment = value.environment;
319
453
  return { inherit: environment?.inherit ?? [], set: environment?.set ?? {} };
320
454
  }
455
+
456
+ function artifactConfiguration(runtime: RuntimeConfiguration["runtime"]) {
457
+ const directories = runtime?.artifactDirectories;
458
+ if (directories === undefined) return {};
459
+ return { artifactDirectories: directories };
460
+ }
321
461
  function configurationReadRoots(value: RuntimeConfiguration): string[] {
322
462
  return value.filesystem?.readOnlyRoots ?? [];
323
463
  }
@@ -332,20 +472,18 @@ function resolvedHooks(hooks: RuntimeConfiguration["hooks"]) {
332
472
  };
333
473
  }
334
474
 
335
- function requireRuntimeSeedRoles(roles: RuntimeRole[] | undefined): void {
336
- if (roles === undefined) return;
337
- const allowed = ["candidate", "checks", "hooks", "semantic", "advisory"];
475
+ function requireRuntimeRoles(roles: RuntimeRole[], kind: string): void {
338
476
  if (
339
477
  !validRoleArray(roles) ||
340
478
  new Set(roles).size !== roles.length ||
341
- roles.some((role) => !allowed.includes(role))
479
+ roles.some((role) => !RUNTIME_ROLES.includes(role))
342
480
  )
343
- throw new Error("runtime seed roles are invalid");
481
+ throw new Error(`runtime ${kind} roles are invalid`);
344
482
  }
345
483
 
346
484
  export function runtimeRoleSeeds(policy: RuntimePolicy, role: RuntimeRole) {
347
485
  return (policy.seeds ?? []).filter(
348
- (seed) => seed.roles === undefined || seed.roles.includes(role),
486
+ (seed) => seed.roles?.includes(role) ?? role !== "checks",
349
487
  );
350
488
  }
351
489
 
@@ -1,13 +1,13 @@
1
1
  import { realpath } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
- import { RuntimeMetadataQueryError } from "./runtime-query";
4
- import { optionalRuntimeRoots, requireRuntimeReadRoots } from "./runtime-paths";
3
+ import { RuntimeMetadataQueryError } from "./query";
4
+ import { optionalRuntimeRoots, requireRuntimeReadRoots } from "./paths";
5
5
  import {
6
6
  providerExecutable,
7
7
  query,
8
8
  requireProvider,
9
9
  type RuntimeDiscoverySource,
10
- } from "./runtime-discovery-providers";
10
+ } from "./discovery-providers";
11
11
 
12
12
  export async function discoverApple(
13
13
  path: string,
@@ -1,8 +1,9 @@
1
1
  import { access, realpath } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
3
  import { delimiter, isAbsolute, join } from "node:path";
4
- import { runtimeMetadataQuery } from "./runtime-query";
5
- import { requireRuntimeReadRoots } from "./runtime-paths";
4
+ import { runtimeMetadataQuery } from "./query";
5
+ import { requireRuntimeReadRoots } from "./paths";
6
+ import { containsControl } from "../value-guards";
6
7
 
7
8
  export interface RuntimeDiscoverySource {
8
9
  provider: "homebrew" | "apple-developer";
@@ -45,13 +46,6 @@ export async function query(
45
46
  }
46
47
  }
47
48
 
48
- function containsControl(value: string): boolean {
49
- return Array.from(value).some(
50
- (character) =>
51
- character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
52
- );
53
- }
54
-
55
49
  export async function requireProvider(
56
50
  executable: string,
57
51
  protectedRoots: string[],
@@ -4,14 +4,14 @@ import {
4
4
  insideRuntimeRoot,
5
5
  optionalRuntimeRoots,
6
6
  requireRuntimeReadRoots,
7
- } from "./runtime-paths";
7
+ } from "./paths";
8
8
  import {
9
9
  providerExecutable,
10
10
  query,
11
11
  requireProvider,
12
12
  type RuntimeDiscoverySource,
13
- } from "./runtime-discovery-providers";
14
- export type { RuntimeDiscoverySource } from "./runtime-discovery-providers";
13
+ } from "./discovery-providers";
14
+ export type { RuntimeDiscoverySource } from "./discovery-providers";
15
15
 
16
16
  async function homebrew(
17
17
  executable: string,
@@ -63,7 +63,7 @@ export async function discoverRuntimeSupport(
63
63
  });
64
64
  }
65
65
  if (process.platform === "darwin") {
66
- const { discoverApple } = await import("./runtime-discovery-apple");
66
+ const { discoverApple } = await import("./discovery-apple");
67
67
  await discoverApple(path, environment, sources, protectedRoots);
68
68
  }
69
69
  return { sources, excludedPrefixes };
@@ -1,6 +1,6 @@
1
1
  import { readFile, readdir, writeFile } from "node:fs/promises";
2
2
  import { basename, dirname, join } from "node:path";
3
- import { isRecord } from "./value-guards";
3
+ import { isRecord } from "../value-guards";
4
4
 
5
5
  export async function hookExecutions(
6
6
  receipts: string,
@@ -7,19 +7,21 @@ import {
7
7
  writeFile,
8
8
  } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
- import type { RuntimePolicy } from "./runtime-config";
11
- import { insideRuntimeRoot, requireRuntimeReadRoots } from "./runtime-paths";
12
- import { runtimeSeedDigest } from "./runtime-seeds";
13
- import { prepareRuntimeState } from "./runtime-state";
14
- import { macSandboxProfile } from "./hosts/mac-sandbox";
15
- import type { IsolatedCommand } from "./hosts/mac-sandbox";
16
- import { prepareLinuxSandboxCommand } from "./hosts/linux-sandbox";
17
- import { isRecord, isUnknownArray } from "./value-guards";
18
- import { hookExecutions, releaseHookProcesses } from "./runtime-hook-processes";
10
+ import { shellQuote as quote } from "../shell";
11
+ import type { RuntimePolicy } from "./config";
12
+ import { insideRuntimeRoot, requireRuntimeReadRoots } from "./paths";
13
+ import { runtimeSeedDigest } from "./seeds";
14
+ import { prepareRuntimeState } from "./state";
15
+ import { sharedCacheDenyRoots, sharedCacheRoots } from "./shared-cache";
16
+ import { macSandboxProfile } from "../hosts/mac-sandbox";
17
+ import type { IsolatedCommand } from "../hosts/mac-sandbox";
18
+ import { prepareLinuxSandboxCommand } from "../hosts/linux-sandbox";
19
+ import { isRecord, isUnknownArray } from "../value-guards";
20
+ import { hookExecutions, releaseHookProcesses } from "./hook-processes";
19
21
  import {
20
22
  nativeStateParent,
21
23
  requireNativeHookRoots,
22
- } from "./native-transcript-state";
24
+ } from "../native-transcript-state";
23
25
 
24
26
  export interface HookMounts {
25
27
  directories: string[];
@@ -61,10 +63,6 @@ function object(value: unknown): Record<string, unknown> {
61
63
  return value;
62
64
  }
63
65
 
64
- function quote(value: string): string {
65
- return `'${value.replaceAll("'", `'"'"'`)}'`;
66
- }
67
-
68
66
  /** Materialize immutable mounts; only declared plugin hooks reach the host. */
69
67
  export async function prepareHookMounts(options: {
70
68
  workspace: string;
@@ -156,7 +154,7 @@ function lazyHookExecution(
156
154
  options,
157
155
  index,
158
156
  readRoots,
159
- runtime.root,
157
+ runtime,
160
158
  );
161
159
  if (boundary.isolation) isolation.push(boundary.isolation);
162
160
  return {
@@ -175,30 +173,43 @@ async function prepareHookIsolation(
175
173
  options: Parameters<typeof prepareHookMounts>[0],
176
174
  index: number,
177
175
  roots: { ordinary: string[]; curated: string[] },
178
- runtimeRoot: string,
176
+ runtime: Awaited<ReturnType<typeof prepareRuntimeState>>,
179
177
  ): Promise<{ profile?: string; isolation?: IsolatedCommand }> {
178
+ const cacheRoots = await sharedCacheRoots(
179
+ options.policy,
180
+ "hooks",
181
+ options.protectedRoots,
182
+ );
183
+ const protectedRoots = [
184
+ ...options.protectedRoots,
185
+ ...sharedCacheDenyRoots(options.policy, "hooks"),
186
+ ];
180
187
  if (process.platform === "linux")
181
188
  return {
182
189
  isolation: await prepareLinuxSandboxCommand({
183
190
  argv: ["/bin/true"],
184
191
  workspace: options.workspace,
185
- protectedRoots: options.protectedRoots,
192
+ protectedRoots,
186
193
  privateStateRoot: options.privateRoot,
187
194
  denyNetwork: true,
188
195
  readOnlyRoots: roots.ordinary,
189
196
  hookReadRoots: roots.curated,
190
- writableRuntimeRoot: runtimeRoot,
197
+ writableRuntimeRoot: runtime.root,
198
+ writableCacheRoots: cacheRoots,
191
199
  }),
192
200
  };
193
201
  const profile = join(options.privateRoot, `hooks-${index}.sb`);
194
202
  await writeFile(
195
203
  profile,
196
204
  macSandboxProfile(
197
- options.protectedRoots,
205
+ protectedRoots,
198
206
  true,
199
207
  roots.ordinary,
200
- runtimeRoot,
208
+ runtime.root,
201
209
  roots.curated,
210
+ options.workspace,
211
+ undefined,
212
+ cacheRoots,
202
213
  ) + "\n(deny process-info* (require-not (target self)))\n",
203
214
  { mode: 0o600 },
204
215
  );
@@ -216,7 +227,7 @@ async function hookReadRoots(
216
227
  ];
217
228
  const curated = trusted ? [root] : [];
218
229
  requireRuntimeReadRoots(ordinary, options.protectedRoots);
219
- await requireNativeHookRoots(curated);
230
+ await requireNativeHookRoots(curated, options.privateRoot);
220
231
  return { ordinary, curated };
221
232
  }
222
233
 
@@ -421,7 +432,7 @@ async function rewriteMarkdown(
421
432
  else delete metadata.hooks;
422
433
  await writeFile(
423
434
  path,
424
- `---\n${Bun.YAML.stringify(metadata)}---\n${body.slice(header[0].length)}`,
435
+ `---\n${Bun.YAML.stringify(metadata)}\n---\n${body.slice(header[0].length)}`,
425
436
  );
426
437
  }
427
438
 
@@ -8,26 +8,20 @@ import {
8
8
  } from "node:fs/promises";
9
9
  import { constants, lstatSync, readlinkSync, realpathSync } from "node:fs";
10
10
  import { homedir, userInfo } from "node:os";
11
- import { nativeStateProtectedRoots } from "./native-transcript-state";
11
+ import { nativeStateProtectedRoots } from "../native-transcript-state";
12
+ import { workspaceStatePath } from "../fixtures/workspace-state";
13
+ import { insideRoot as insideRuntimeRoot } from "../paths";
14
+ import { containsControl } from "../value-guards";
15
+ export { insideRoot as insideRuntimeRoot } from "../paths";
12
16
  import {
13
17
  basename,
14
18
  delimiter,
15
19
  dirname,
16
20
  isAbsolute,
17
21
  join,
18
- relative,
19
22
  resolve,
20
- sep,
21
23
  } from "node:path";
22
24
 
23
- export function insideRuntimeRoot(root: string, path: string): boolean {
24
- const child = relative(root, path);
25
- return (
26
- child === "" ||
27
- (!child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child))
28
- );
29
- }
30
-
31
25
  export function isRuntimeHomeRoot(root: string): boolean {
32
26
  return [homedir(), userInfo().homedir]
33
27
  .map((home) => resolve(home))
@@ -42,6 +36,7 @@ export function requireRuntimeReadRoots(
42
36
  ...protectedRoots.filter((root) => !isRuntimeHomeRoot(root)),
43
37
  ...runtimeCredentialRoots(),
44
38
  ...nativeStateProtectedRoots(),
39
+ workspaceStatePath(),
45
40
  ];
46
41
  for (const root of readRoots) {
47
42
  if (root === "/" || isRuntimeHomeRoot(root))
@@ -193,13 +188,6 @@ export async function runtimePathDirectories(
193
188
  return [...new Set(roots)].sort();
194
189
  }
195
190
 
196
- function containsControl(value: string): boolean {
197
- return Array.from(value).some(
198
- (character) =>
199
- character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
200
- );
201
- }
202
-
203
191
  function requirePathEntry(entry: string): void {
204
192
  if (!isAbsolute(entry) || containsControl(entry))
205
193
  throw new Error("runtime PATH entries must be absolute and nonempty");
@@ -1,5 +1,6 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { homedir } from "node:os";
3
+ import { terminateOwnedProcessGroup } from "../process-lifecycle";
3
4
 
4
5
  export class RuntimeMetadataQueryError extends Error {
5
6
  constructor(
@@ -25,11 +26,10 @@ function exitError(
25
26
 
26
27
  function stopQuery(child: ChildProcess): void {
27
28
  if (child.pid === undefined) return;
28
- try {
29
- process.kill(-child.pid, "SIGKILL");
30
- } catch {
31
- child.kill("SIGKILL");
32
- }
29
+ terminateOwnedProcessGroup(
30
+ { pid: child.pid, kill: (signal) => child.kill(signal) },
31
+ { alwaysGroup: true, fallbackError: "propagate" },
32
+ );
33
33
  }
34
34
 
35
35
  /** Bound both output streams and stop every descendant of this metadata query. */
@@ -8,9 +8,9 @@ import {
8
8
  writeFile,
9
9
  } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
- import { hashJson, sha256 } from "./identity";
12
- import type { RuntimePolicy } from "./runtime-config";
13
- import { insideRuntimeRoot } from "./runtime-paths";
11
+ import { hashJson, sha256 } from "../identity";
12
+ import type { RuntimePolicy } from "./config";
13
+ import { insideRuntimeRoot } from "./paths";
14
14
 
15
15
  class SeedSnapshot {
16
16
  private entries = 0;