@bjoernrochel/sevro 0.1.0-rc.8 → 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 (228) 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 +9 -2
  7. package/docs/codex-host.md +155 -57
  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 +122 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +255 -22
  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 +43 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +42 -11
  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 +9 -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} +77 -87
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/{evaluation-types.ts → evaluation/types.ts} +79 -29
  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} +12 -72
  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/{workspace-state.ts → fixtures/workspace-state.ts} +5 -5
  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 +50 -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 +46 -9
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +28 -15
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +121 -81
  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 -183
  158. package/src/hosts/codex-profile.ts +36 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +134 -18
  161. package/src/hosts/codex.ts +210 -183
  162. package/src/hosts/isolation-common.ts +8 -11
  163. package/src/hosts/isolation-roots.ts +2 -2
  164. package/src/hosts/linux-sandbox.ts +15 -2
  165. package/src/hosts/mac-sandbox.ts +87 -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 +216 -48
  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 +26 -11
  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/{runtime-config.ts → runtime/config.ts} +160 -46
  186. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  187. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  188. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  189. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  190. package/src/{runtime-hooks.ts → runtime/hooks.ts} +32 -22
  191. package/src/{runtime-paths.ts → runtime/paths.ts} +5 -19
  192. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  193. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  194. package/src/runtime/shared-cache.ts +130 -0
  195. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  196. package/src/schema-types.ts +73 -161
  197. package/src/shell.ts +4 -0
  198. package/src/transcript-retention.ts +146 -0
  199. package/src/value-guards.ts +22 -0
  200. package/docs/assets/sevro-logo.png +0 -0
  201. package/docs/assets/sevro-symbol.svg +0 -1
  202. package/docs/assets/sevro-wordmark.svg +0 -1
  203. package/docs/cache-ownership-validation.md +0 -49
  204. package/docs/claude-continuation-validation.md +0 -47
  205. package/docs/concurrency-validation.md +0 -86
  206. package/docs/configuration-root-validation.md +0 -70
  207. package/docs/engine-slice.md +0 -114
  208. package/docs/evidence/runtime-startup-30/README.md +0 -185
  209. package/docs/evidence/runtime-startup-30/after.json +0 -319
  210. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  211. package/docs/evidence/runtime-startup-30/before.json +0 -403
  212. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  213. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  214. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  216. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  217. package/docs/evidence.md +0 -17
  218. package/docs/fixture-cleanup-validation.md +0 -73
  219. package/docs/guide-verification.json +0 -239
  220. package/src/evaluation-context.ts +0 -259
  221. package/src/evaluation-fixture.ts +0 -302
  222. package/src/evaluation-run.ts +0 -200
  223. package/src/evaluation-trial.ts +0 -1016
  224. package/src/hosts/claude-nested-skills.ts +0 -272
  225. package/src/hosts/codex-command-output.ts +0 -510
  226. package/src/hosts/codex-executor-command.ts +0 -291
  227. package/src/hosts/codex-skill-reads.ts +0 -783
  228. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -1,25 +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";
10
+ } from "./paths";
11
+ import validate from "../generated/runtime.cjs";
11
12
  import {
12
13
  requireArtifactDirectories,
13
14
  type RuntimeArtifactDirectory,
14
- } from "./candidate-artifacts";
15
- import { runtimeSeedDigest } from "./runtime-seeds";
15
+ } from "../candidate-artifacts";
16
+ import { runtimeSeedDigest } from "./seeds";
17
+ import {
18
+ resolveSharedCaches,
19
+ requireSharedCachePolicy,
20
+ type SharedCacheDirectory,
21
+ } from "./shared-cache";
16
22
  import {
17
23
  discoverRuntimeSupport,
18
24
  type RuntimeDiscoverySource,
19
- } from "./runtime-discovery";
25
+ } from "./discovery";
20
26
 
21
- export type RuntimeRole =
22
- "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];
23
36
  export interface RuntimeSeed {
24
37
  source: string;
25
38
  target: string;
@@ -27,11 +40,14 @@ export interface RuntimeSeed {
27
40
  }
28
41
  export interface RuntimeConfiguration {
29
42
  format: "sevro.runtime.v1";
43
+ codexState?: boolean;
44
+ network?: { localHttp?: boolean };
30
45
  nativeTranscripts?: boolean;
31
46
  environment?: { inherit?: string[]; set?: Record<string, string> };
32
47
  filesystem?: { readOnlyRoots?: string[]; optionalReadOnlyRoots?: string[] };
33
48
  runtime?: {
34
49
  seedDirectories?: RuntimeSeed[];
50
+ sharedCacheDirectories?: SharedCacheDirectory[];
35
51
  artifactDirectories?: RuntimeArtifactDirectory[];
36
52
  };
37
53
  hooks?: { nativeGoal?: boolean; plugins?: string[] };
@@ -39,6 +55,8 @@ export interface RuntimeConfiguration {
39
55
 
40
56
  export interface RuntimePolicy {
41
57
  format: "sevro.runtime.v1";
58
+ codexState?: boolean;
59
+ network?: { localHttp?: boolean };
42
60
  artifactDirectories?: RuntimeArtifactDirectory[];
43
61
  nativeTranscripts?: boolean;
44
62
  environment: Record<string, string>;
@@ -48,9 +66,18 @@ export interface RuntimePolicy {
48
66
  /** Original source denials retained when a derived role policy omits seeds. */
49
67
  seedReadDenyRoots?: string[];
50
68
  seeds?: (RuntimeSeed & { sha256: string })[];
69
+ sharedCacheDirectories?: SharedCacheDirectory[];
51
70
  hooks?: { nativeGoal?: boolean; plugins?: string[] };
52
71
  }
53
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
+
54
81
  export function runtimeNativeGoalEnabled(
55
82
  policy: RuntimePolicy | undefined,
56
83
  ): boolean {
@@ -58,28 +85,64 @@ export function runtimeNativeGoalEnabled(
58
85
  return hooks?.nativeGoal === true;
59
86
  }
60
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;
61
94
  export function runtimePolicySnapshot(
62
95
  policy: RuntimePolicy | undefined,
63
96
  ): RuntimePolicy | undefined {
64
97
  if (!policy) return undefined;
65
- requireRuntimeEnvironment(policy.environment);
66
98
  const snapshot = structuredClone(policy);
99
+ if (!validatedPolicies.has(policy)) requireRuntimePolicy(snapshot);
100
+ Object.freeze(snapshot.network);
67
101
  freezeArtifactDirectories(snapshot);
68
102
  Object.freeze(snapshot.environment);
69
103
  Object.freeze(snapshot.readOnlyRoots);
70
104
  freezeDiscovery(snapshot);
71
- for (const seed of snapshot.seeds ?? []) {
72
- Object.freeze(seed.roles);
73
- Object.freeze(seed);
74
- }
75
- Object.freeze(snapshot.seeds);
105
+ freezeRuntimeCaches(snapshot);
76
106
  Object.freeze(snapshot.seedReadDenyRoots);
77
107
  const hooks = snapshot.hooks;
78
108
  if (hooks) {
79
109
  Object.freeze(hooks.plugins);
80
110
  Object.freeze(hooks);
81
111
  }
82
- 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);
83
146
  }
84
147
 
85
148
  function freezeDiscovery(snapshot: RuntimePolicy): void {
@@ -93,7 +156,6 @@ function freezeDiscovery(snapshot: RuntimePolicy): void {
93
156
 
94
157
  function freezeArtifactDirectories(snapshot: RuntimePolicy): void {
95
158
  const directories = snapshot.artifactDirectories ?? [];
96
- requireArtifactDirectories(directories);
97
159
  for (const directory of directories) Object.freeze(directory);
98
160
  Object.freeze(snapshot.artifactDirectories);
99
161
  }
@@ -101,11 +163,27 @@ function freezeArtifactDirectories(snapshot: RuntimePolicy): void {
101
163
  function assertRuntimeConfiguration(
102
164
  value: unknown,
103
165
  ): asserts value is RuntimeConfiguration {
104
- 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 || "/");
105
183
  }
106
184
 
107
185
  const RESERVED_ENVIRONMENT =
108
- /^(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;
109
187
 
110
188
  function requireEnvironmentName(name: string): void {
111
189
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(name))
@@ -139,16 +217,7 @@ function snapshotEnvironment(
139
217
  return environment;
140
218
  }
141
219
 
142
- export function requireRuntimeEnvironment(
143
- environment: Record<string, string>,
144
- ): void {
145
- snapshotEnvironment([], environment);
146
- }
147
-
148
- export function requireRuntimeSeedTargets(
149
- seeds: NonNullable<RuntimePolicy["seeds"]>,
150
- ): void {
151
- for (const seed of seeds) requireRuntimeSeedRoles(seed.roles);
220
+ function requireRuntimeSeedTargets(seeds: RuntimeSeed[]): void {
152
221
  const targets = seeds.map((seed) => seed.target);
153
222
  if (
154
223
  targets.some(
@@ -175,13 +244,29 @@ export async function loadRuntimeConfiguration(
175
244
  const source = await readFile(canonical, "utf8");
176
245
  if (Buffer.byteLength(source) > 65536)
177
246
  throw new Error("runtime configuration exceeds 64 KiB");
178
- return await resolveRuntimeConfiguration(
179
- JSON.parse(source),
180
- dirname(canonical),
181
- protectedRoots,
247
+ return runtimePolicySnapshot(
248
+ await resolveRuntimeConfiguration(
249
+ parseRuntimeConfiguration(source),
250
+ dirname(canonical),
251
+ protectedRoots,
252
+ ),
182
253
  );
183
254
  } catch (cause) {
184
- 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
+ );
185
270
  }
186
271
  }
187
272
 
@@ -256,7 +341,7 @@ async function resolvedSeeds(
256
341
  environment: Record<string, string>,
257
342
  base: string,
258
343
  ) {
259
- const resolved = await Promise.all(
344
+ return Promise.all(
260
345
  seeds.map(async (seed) => {
261
346
  const roots = await declaredRuntimeRoots(
262
347
  [seed.source],
@@ -274,8 +359,6 @@ async function resolvedSeeds(
274
359
  };
275
360
  }),
276
361
  );
277
- requireRuntimeSeedTargets(resolved);
278
- return resolved;
279
362
  }
280
363
 
281
364
  async function resolveRuntimeConfiguration(
@@ -308,19 +391,53 @@ async function resolveRuntimeConfiguration(
308
391
  requireRuntimeReadRoots(readOnlyRoots, protectedRoots);
309
392
  return {
310
393
  format: value.format,
394
+ ...selectedRuntimeCapabilities(value),
311
395
  ...artifactConfiguration(value.runtime),
312
- ...(value.nativeTranscripts === undefined
313
- ? {}
314
- : { nativeTranscripts: value.nativeTranscripts }),
315
396
  environment,
316
397
  readOnlyRoots: [...new Set(readOnlyRoots)].sort(),
317
398
  discovery: discovery.sources,
318
399
  skippedOptionalReadOnlyRoots: optional.skipped,
319
- seeds: await resolvedSeeds(configurationSeeds(value), environment, base),
400
+ ...(await resolvedRuntimeCaches(value, environment, base, [
401
+ ...protectedRoots,
402
+ ...readOnlyRoots,
403
+ ])),
320
404
  hooks: resolvedHooks(value.hooks),
321
405
  };
322
406
  }
323
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
+
324
441
  async function resolvedRuntimeEnvironment(value: RuntimeConfiguration) {
325
442
  const { inherit, set } = configurationEnvironment(value);
326
443
  const environment = snapshotEnvironment(inherit, set);
@@ -339,7 +456,6 @@ function configurationEnvironment(value: RuntimeConfiguration) {
339
456
  function artifactConfiguration(runtime: RuntimeConfiguration["runtime"]) {
340
457
  const directories = runtime?.artifactDirectories;
341
458
  if (directories === undefined) return {};
342
- requireArtifactDirectories(directories);
343
459
  return { artifactDirectories: directories };
344
460
  }
345
461
  function configurationReadRoots(value: RuntimeConfiguration): string[] {
@@ -356,20 +472,18 @@ function resolvedHooks(hooks: RuntimeConfiguration["hooks"]) {
356
472
  };
357
473
  }
358
474
 
359
- function requireRuntimeSeedRoles(roles: RuntimeRole[] | undefined): void {
360
- if (roles === undefined) return;
361
- const allowed = ["candidate", "checks", "hooks", "semantic", "advisory"];
475
+ function requireRuntimeRoles(roles: RuntimeRole[], kind: string): void {
362
476
  if (
363
477
  !validRoleArray(roles) ||
364
478
  new Set(roles).size !== roles.length ||
365
- roles.some((role) => !allowed.includes(role))
479
+ roles.some((role) => !RUNTIME_ROLES.includes(role))
366
480
  )
367
- throw new Error("runtime seed roles are invalid");
481
+ throw new Error(`runtime ${kind} roles are invalid`);
368
482
  }
369
483
 
370
484
  export function runtimeRoleSeeds(policy: RuntimePolicy, role: RuntimeRole) {
371
485
  return (policy.seeds ?? []).filter(
372
- (seed) => seed.roles === undefined || seed.roles.includes(role),
486
+ (seed) => seed.roles?.includes(role) ?? role !== "checks",
373
487
  );
374
488
  }
375
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,31 +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,
202
210
  options.workspace,
211
+ undefined,
212
+ cacheRoots,
203
213
  ) + "\n(deny process-info* (require-not (target self)))\n",
204
214
  { mode: 0o600 },
205
215
  );
@@ -217,7 +227,7 @@ async function hookReadRoots(
217
227
  ];
218
228
  const curated = trusted ? [root] : [];
219
229
  requireRuntimeReadRoots(ordinary, options.protectedRoots);
220
- await requireNativeHookRoots(curated);
230
+ await requireNativeHookRoots(curated, options.privateRoot);
221
231
  return { ordinary, curated };
222
232
  }
223
233
 
@@ -422,7 +432,7 @@ async function rewriteMarkdown(
422
432
  else delete metadata.hooks;
423
433
  await writeFile(
424
434
  path,
425
- `---\n${Bun.YAML.stringify(metadata)}---\n${body.slice(header[0].length)}`,
435
+ `---\n${Bun.YAML.stringify(metadata)}\n---\n${body.slice(header[0].length)}`,
426
436
  );
427
437
  }
428
438
 
@@ -8,27 +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";
12
- import { workspaceStatePath } from "./workspace-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";
13
16
  import {
14
17
  basename,
15
18
  delimiter,
16
19
  dirname,
17
20
  isAbsolute,
18
21
  join,
19
- relative,
20
22
  resolve,
21
- sep,
22
23
  } from "node:path";
23
24
 
24
- export function insideRuntimeRoot(root: string, path: string): boolean {
25
- const child = relative(root, path);
26
- return (
27
- child === "" ||
28
- (!child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child))
29
- );
30
- }
31
-
32
25
  export function isRuntimeHomeRoot(root: string): boolean {
33
26
  return [homedir(), userInfo().homedir]
34
27
  .map((home) => resolve(home))
@@ -195,13 +188,6 @@ export async function runtimePathDirectories(
195
188
  return [...new Set(roots)].sort();
196
189
  }
197
190
 
198
- function containsControl(value: string): boolean {
199
- return Array.from(value).some(
200
- (character) =>
201
- character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
202
- );
203
- }
204
-
205
191
  function requirePathEntry(entry: string): void {
206
192
  if (!isAbsolute(entry) || containsControl(entry))
207
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;