@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
@@ -0,0 +1,81 @@
1
+ import { mkdir, rmdir, stat, utimes } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ /**
5
+ * Claude Code serializes OAuth refresh through this directory lock in the
6
+ * configuration directory. These values match its lockfile settings, observed
7
+ * in Claude Code 2.1.284 and 2.1.309: stale after 60s, refreshed every 5s.
8
+ */
9
+ const LOCK_NAME = ".oauth_refresh.lock";
10
+ const STALE_MS = 60_000;
11
+ const UPDATE_MS = 5_000;
12
+ const RETRY_MS = 250;
13
+ const DEFAULT_WAIT_MS = 120_000;
14
+
15
+ /** Run an action while holding the refresh lock Claude Code also honors. */
16
+ export async function withClaudeRefreshLock<T>(
17
+ root: string,
18
+ action: () => Promise<T>,
19
+ { waitMs = DEFAULT_WAIT_MS, updateMs = UPDATE_MS } = {},
20
+ ): Promise<T> {
21
+ await mkdir(root, { recursive: true, mode: 0o700 });
22
+ const lock = join(root, LOCK_NAME);
23
+ await acquireLock(lock, Date.now() + waitMs);
24
+ const heartbeat = setInterval(() => {
25
+ void touchLock(lock);
26
+ }, updateMs);
27
+ try {
28
+ return await action();
29
+ } finally {
30
+ clearInterval(heartbeat);
31
+ await releaseLock(lock);
32
+ }
33
+ }
34
+
35
+ async function acquireLock(lock: string, deadline: number): Promise<void> {
36
+ while (!(await createLock(lock))) {
37
+ if (Date.now() >= deadline)
38
+ throw new Error(
39
+ "Timed out waiting for another process to refresh the Claude login",
40
+ );
41
+ await Bun.sleep(RETRY_MS);
42
+ }
43
+ }
44
+
45
+ async function createLock(lock: string): Promise<boolean> {
46
+ try {
47
+ await mkdir(lock);
48
+ return true;
49
+ } catch (cause) {
50
+ if ((cause as NodeJS.ErrnoException).code !== "EEXIST")
51
+ throw new Error("Claude refresh lock is unavailable", { cause });
52
+ }
53
+ await removeStaleLock(lock);
54
+ return false;
55
+ }
56
+
57
+ /** An abandoned holder stops touching the lock; reclaim it as Claude does. */
58
+ async function removeStaleLock(lock: string): Promise<void> {
59
+ try {
60
+ if (Date.now() - (await stat(lock)).mtimeMs > STALE_MS) await rmdir(lock);
61
+ } catch {
62
+ // Another contender released or reclaimed it first.
63
+ }
64
+ }
65
+
66
+ async function touchLock(lock: string): Promise<void> {
67
+ const now = new Date();
68
+ try {
69
+ await utimes(lock, now, now);
70
+ } catch {
71
+ // Release or reclamation races are resolved by the post-lock re-read.
72
+ }
73
+ }
74
+
75
+ async function releaseLock(lock: string): Promise<void> {
76
+ try {
77
+ await rmdir(lock);
78
+ } catch {
79
+ // Already removed; the heartbeat keeps a live holder from being reclaimed.
80
+ }
81
+ }
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import type { HostAdapter } from "../engine";
5
5
  import { isRecord } from "../value-guards";
6
6
  import type { ClaudeToolCallsObservation } from "./claude-tool-calls";
7
+ import { parseClaudeEvents, type ClaudeEvents } from "./claude-events";
7
8
 
8
9
  type Entry = Record<string, unknown>;
9
10
  export interface ClaudeRepositoryInvocation {
@@ -13,15 +14,12 @@ export interface ClaudeRepositoryInvocation {
13
14
  prompt: string;
14
15
  }
15
16
 
16
- function entries(text: string): Entry[] {
17
- return text
18
- .split(/\r?\n/)
19
- .filter((line) => line.trim())
20
- .map((line) => {
21
- const value: unknown = JSON.parse(line);
22
- if (!isRecord(value)) throw new Error("invalid Claude session entry");
23
- return value;
24
- });
17
+ function entries(text: string | ClaudeEvents): Entry[] {
18
+ return parseClaudeEvents(text).lines.map((line) => {
19
+ if (!line.parsed) throw line.error;
20
+ if (!isRecord(line.value)) throw new Error("invalid Claude session entry");
21
+ return line.value;
22
+ });
25
23
  }
26
24
 
27
25
  function content(entry: Entry): string {
@@ -250,7 +248,7 @@ function bodyCorrelated(
250
248
  /** Read only the completed bound native session; retain no arguments or body. */
251
249
  export async function claudeRepositoryInvocationObservation(input: {
252
250
  invocation: ClaudeRepositoryInvocation;
253
- stream: string;
251
+ stream: string | ClaudeEvents;
254
252
  configRoot: string;
255
253
  workspace: string;
256
254
  tools: ClaudeToolCallsObservation;
@@ -264,14 +262,6 @@ export async function claudeRepositoryInvocationObservation(input: {
264
262
  } catch {
265
263
  /* Unavailable or malformed native evidence stays partial. */
266
264
  }
267
- const observed = input.tools.data.calls
268
- .filter((call) => call.name === "Skill")
269
- .map((call) => call.skill);
270
- const observedSkills = repositoryObservedSkills(
271
- receipt,
272
- input.invocation.skillName,
273
- observed,
274
- );
275
265
  return {
276
266
  id: "sevro.claude.repository-invocation" as const,
277
267
  completeness:
@@ -282,26 +272,11 @@ export async function claudeRepositoryInvocationObservation(input: {
282
272
  method: "native_repository_command",
283
273
  ...receipt,
284
274
  skill: input.invocation.skillName,
285
- primarySkill: observedSkills[0] ?? null,
286
- observedSkills,
287
275
  },
288
276
  };
289
277
  }
290
278
 
291
- function repositoryObservedSkills(
292
- receipt: Receipt,
293
- skillName: string,
294
- observed: string[],
295
- ): string[] {
296
- return [
297
- ...new Set([
298
- ...(receipt.accepted === true ? [skillName] : []),
299
- ...observed,
300
- ]),
301
- ];
302
- }
303
-
304
- function repositorySessionId(stream: string): string | null {
279
+ function repositorySessionId(stream: string | ClaudeEvents): string | null {
305
280
  const sessions = [
306
281
  ...new Set(
307
282
  entries(stream)
@@ -1,13 +1,18 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { CLAUDE_AUTH_VARIABLES } from "./claude-credential";
3
- import type { RuntimePolicy } from "../runtime-config";
4
- import { runtimeHooksEnabled } from "../runtime-hooks";
5
- import { requireRuntimeReadRoots, isRuntimeHomeRoot } from "../runtime-paths";
3
+ import type { RuntimePolicy } from "../runtime/config";
4
+ import { runtimeHooksEnabled } from "../runtime/hooks";
5
+ import { requireRuntimeReadRoots, isRuntimeHomeRoot } from "../runtime/paths";
6
6
  import { nativeStatePath } from "../native-transcript-state";
7
+ import {
8
+ workspaceStatePath,
9
+ workspaceBoundaryException,
10
+ } from "../fixtures/workspace-state";
7
11
 
8
12
  interface WritableRuntime {
9
13
  root: string;
10
14
  workspace: string;
15
+ cacheRoots?: string[];
11
16
  }
12
17
 
13
18
  function absoluteRule(root: string, tool: "Read" | "Edit"): string {
@@ -38,6 +43,7 @@ export function claudeHostSettings(
38
43
  runtimePolicy?: RuntimePolicy,
39
44
  transcriptReadRoots: string[] = [],
40
45
  runtime?: WritableRuntime,
46
+ workspace?: string,
41
47
  ): Record<string, unknown> {
42
48
  if (
43
49
  !validPrivatePaths(privateRoot, credentialFile, [
@@ -48,6 +54,10 @@ export function claudeHostSettings(
48
54
  throw new Error("Claude private state paths are invalid");
49
55
  const readRoots = runtimeReadRoots(runtimePolicy);
50
56
  requireRuntimeReadRoots(readRoots, [privateRoot, ...protectedRoots]);
57
+ requireRuntimeReadRoots(runtimeCacheRoots(runtime), [
58
+ privateRoot,
59
+ ...protectedRoots,
60
+ ]);
51
61
  return {
52
62
  disableAllHooks: !runtimeHooksEnabled(runtimePolicy),
53
63
  sandbox: {
@@ -61,6 +71,7 @@ export function claudeHostSettings(
61
71
  transcriptReadRoots,
62
72
  pluginRoots,
63
73
  runtime,
74
+ workspace,
64
75
  ),
65
76
  credentials: {
66
77
  files: [{ path: credentialFile, mode: "deny" }],
@@ -74,6 +85,7 @@ export function claudeHostSettings(
74
85
  runtimePolicy,
75
86
  transcriptReadRoots,
76
87
  runtime,
88
+ workspace,
77
89
  ),
78
90
  };
79
91
  }
@@ -85,21 +97,29 @@ function claudeFilesystemPolicy(
85
97
  transcriptReadRoots: string[],
86
98
  pluginRoots: string[],
87
99
  runtime: WritableRuntime | undefined,
100
+ workspace: string | undefined,
88
101
  ) {
89
102
  const runtimeWriteRoot = runtime?.root;
103
+ const writableRoots = runtimeWriteRoots(runtime);
90
104
  return {
91
105
  denyRead: [privateRoot, ...protectedRoots],
92
106
  allowRead: [
107
+ ...workspaceBoundaryException(workspaceStatePath(), workspace),
93
108
  ...readRoots,
94
109
  ...transcriptReadRoots,
95
110
  ...pluginRoots,
96
- ...optionalRuntimeRoot(runtimeWriteRoot),
111
+ ...writableRoots,
97
112
  ],
98
- ...(runtimeWriteRoot ? { allowWrite: [runtimeWriteRoot] } : {}),
113
+ ...(writableRoots.length ? { allowWrite: writableRoots } : {}),
99
114
  denyWrite: [
100
115
  privateRoot,
101
116
  ...pluginRoots,
102
- ...writableProtectedRoots(protectedRoots, runtimeWriteRoot),
117
+ ...writableProtectedRoots(
118
+ protectedRoots,
119
+ runtimeWriteRoot,
120
+ workspace,
121
+ runtimeCacheRoots(runtime),
122
+ ),
103
123
  ...readRoots,
104
124
  ...transcriptReadRoots,
105
125
  ],
@@ -113,17 +133,27 @@ function claudePermissions(
113
133
  policy: RuntimePolicy | undefined,
114
134
  transcriptReadRoots: string[],
115
135
  runtime: WritableRuntime | undefined,
136
+ workspace: string | undefined,
116
137
  ) {
117
138
  const runtimeWriteRoot = runtime?.root;
118
- const roots = [privateRoot, ...protectedRoots];
139
+ const roots = [
140
+ privateRoot,
141
+ ...protectedRoots.filter(
142
+ (root) => !workspaceBoundaryException(root, workspace).length,
143
+ ),
144
+ ];
145
+ const managedWorkspace = workspaceBoundaryException(
146
+ workspaceStatePath(),
147
+ workspace,
148
+ )[0];
119
149
  const readRoots = policy
120
150
  ? roots.filter((root) => !isRuntimeHomeRoot(root))
121
151
  : roots;
122
152
  const toolRoots = [...runtimeReadRoots(policy), ...transcriptReadRoots];
123
- const writableRoots = optionalRuntimeRoot(runtimeWriteRoot);
153
+ const writableRoots = runtimeWriteRoots(runtime);
124
154
  return {
125
- allow: claudeToolAllowRules(policy, runtime),
126
- ...(policy
155
+ allow: claudeToolAllowRules(policy, runtime, managedWorkspace),
156
+ ...(policy || managedWorkspace
127
157
  ? {
128
158
  blockReadsOutsideWorkingDirectories: true,
129
159
  additionalDirectories: [
@@ -134,7 +164,12 @@ function claudePermissions(
134
164
  }
135
165
  : {}),
136
166
  deny: [
137
- ...writableProtectedRoots(roots, runtimeWriteRoot).flatMap((root) =>
167
+ ...writableProtectedRoots(
168
+ roots,
169
+ runtimeWriteRoot,
170
+ workspace,
171
+ runtimeCacheRoots(runtime),
172
+ ).flatMap((root) =>
138
173
  protectedToolRules(
139
174
  root,
140
175
  readRoots.includes(root) &&
@@ -153,8 +188,16 @@ function claudePermissions(
153
188
  function claudeToolAllowRules(
154
189
  policy: RuntimePolicy | undefined,
155
190
  runtime: WritableRuntime | undefined,
191
+ workspace?: string,
156
192
  ): string[] {
157
- if (!policy) return ["Bash", "Read", "Edit", "Skill", "Agent"];
193
+ if (!policy)
194
+ return [
195
+ "Bash",
196
+ "Read",
197
+ "Skill",
198
+ "Agent",
199
+ workspace ? absoluteRule(workspace, "Edit") : "Edit",
200
+ ];
158
201
  if (!runtime) return ["Bash", "Edit", "Skill", "Agent"];
159
202
  return [
160
203
  "Bash",
@@ -162,6 +205,7 @@ function claudeToolAllowRules(
162
205
  "Agent",
163
206
  absoluteRule(runtime.workspace, "Edit"),
164
207
  absoluteRule(runtime.root, "Edit"),
208
+ ...(runtime.cacheRoots ?? []).map((root) => absoluteRule(root, "Edit")),
165
209
  ];
166
210
  }
167
211
 
@@ -169,12 +213,45 @@ function claudeToolAllowRules(
169
213
  function writableProtectedRoots(
170
214
  roots: string[],
171
215
  runtimeWriteRoot: string | undefined,
216
+ workspace?: string,
217
+ cacheRoots: string[] = [],
172
218
  ): string[] {
173
- if (!runtimeWriteRoot) return roots;
174
219
  const namespace = nativeStatePath();
175
220
  return roots.filter(
176
221
  (root) =>
177
- root !== namespace || !runtimeWriteRoot.startsWith(`${namespace}/`),
222
+ !workspaceBoundaryException(root, workspace).length &&
223
+ !sharedHomeGrant(root, cacheRoots) &&
224
+ keepNativeWriteDeny(root, namespace, runtimeWriteRoot),
225
+ );
226
+ }
227
+
228
+ function sharedHomeGrant(root: string, cacheRoots: string[]): boolean {
229
+ return (
230
+ isRuntimeHomeRoot(root) &&
231
+ cacheRoots.some((cache) => cache.startsWith(`${root}/`))
232
+ );
233
+ }
234
+
235
+ function runtimeCacheRoots(runtime: WritableRuntime | undefined): string[] {
236
+ return runtime?.cacheRoots ?? [];
237
+ }
238
+
239
+ function runtimeWriteRoots(runtime: WritableRuntime | undefined): string[] {
240
+ return [
241
+ ...optionalRuntimeRoot(runtime?.root),
242
+ ...(runtime?.cacheRoots ?? []),
243
+ ];
244
+ }
245
+
246
+ function keepNativeWriteDeny(
247
+ root: string,
248
+ namespace: string,
249
+ runtimeWriteRoot?: string,
250
+ ): boolean {
251
+ return (
252
+ root !== namespace ||
253
+ runtimeWriteRoot === undefined ||
254
+ !runtimeWriteRoot.startsWith(`${namespace}/`)
178
255
  );
179
256
  }
180
257
 
@@ -0,0 +1,169 @@
1
+ import { isRecord } from "../value-guards";
2
+ import {
3
+ summarizeClaudeEvents,
4
+ parseClaudeEvents,
5
+ type ClaudeEvents,
6
+ type ClaudeEventLine,
7
+ } from "./claude-events";
8
+
9
+ const MAX_CALLS = 128;
10
+ type Entry = Record<string, unknown>;
11
+ interface SkillCall {
12
+ sessionId: string;
13
+ parentToolUseId: string | null;
14
+ toolUseId: string;
15
+ invocation: string;
16
+ status: "unknown" | "succeeded" | "failed";
17
+ }
18
+ interface State {
19
+ calls: Map<string, SkillCall>;
20
+ partial: boolean;
21
+ truncated: boolean;
22
+ }
23
+
24
+ function label(value: unknown): string | null {
25
+ return typeof value === "string" && /^[A-Za-z0-9_.:-]{1,256}$/.test(value)
26
+ ? value
27
+ : null;
28
+ }
29
+
30
+ function blocks(event: Entry): unknown[] {
31
+ const content: unknown = isRecord(event.message)
32
+ ? event.message.content
33
+ : null;
34
+ return Array.isArray(content) ? content : [];
35
+ }
36
+
37
+ function key(event: Entry, id: string): string {
38
+ return JSON.stringify([
39
+ event.session_id ?? event.sessionId,
40
+ event.parent_tool_use_id ?? null,
41
+ id,
42
+ ]);
43
+ }
44
+
45
+ function call(event: Entry, block: Entry, state: State) {
46
+ const id = label(block.id);
47
+ const sessionId = label(event.session_id ?? event.sessionId);
48
+ const invocation = skillInput(block.input);
49
+ if (!id || !sessionId || !invocation) {
50
+ state.partial = true;
51
+ return;
52
+ }
53
+ addCall(
54
+ key(event, id),
55
+ {
56
+ sessionId,
57
+ parentToolUseId: label(event.parent_tool_use_id),
58
+ toolUseId: id,
59
+ invocation,
60
+ status: "unknown",
61
+ },
62
+ state,
63
+ );
64
+ }
65
+
66
+ function skillInput(input: unknown): string | null {
67
+ return isRecord(input) ? label(input.skill) : null;
68
+ }
69
+
70
+ function addCall(id: string, value: SkillCall, state: State) {
71
+ const previous = state.calls.get(id);
72
+ if (previous) {
73
+ state.partial ||= previous.invocation !== value.invocation;
74
+ return;
75
+ }
76
+ if (state.calls.size >= MAX_CALLS) {
77
+ state.truncated = true;
78
+ return;
79
+ }
80
+ state.calls.set(id, value);
81
+ }
82
+
83
+ function result(event: Entry, block: Entry, state: State) {
84
+ const id = label(block.tool_use_id);
85
+ if (!id) return;
86
+ const pending = state.calls.get(key(event, id));
87
+ if (!pending) return;
88
+ const status = resultStatus(block.is_error);
89
+ if (!status) {
90
+ state.partial = true;
91
+ return;
92
+ }
93
+ state.partial ||= conflictingResult(pending.status, status);
94
+ pending.status = status;
95
+ }
96
+
97
+ function resultStatus(value: unknown): "failed" | "succeeded" | null {
98
+ if (value !== undefined && typeof value !== "boolean") return null;
99
+ return value === true ? "failed" : "succeeded";
100
+ }
101
+
102
+ function conflictingResult(
103
+ previous: SkillCall["status"],
104
+ next: SkillCall["status"],
105
+ ): boolean {
106
+ return previous !== "unknown" && previous !== next;
107
+ }
108
+
109
+ function visit(line: ClaudeEventLine, state: State) {
110
+ if (!line.parsed || !isRecord(line.value)) {
111
+ state.partial = true;
112
+ return;
113
+ }
114
+ for (const block of blocks(line.value)) visitBlock(line.value, block, state);
115
+ }
116
+
117
+ function visitBlock(event: Entry, block: unknown, state: State) {
118
+ if (!isRecord(block)) return;
119
+ switch (event.type) {
120
+ case "assistant":
121
+ if (isSkillCall(block)) call(event, block, state);
122
+ break;
123
+ case "user":
124
+ if (block.type === "tool_result") result(event, block, state);
125
+ }
126
+ }
127
+
128
+ function isSkillCall(block: Entry): boolean {
129
+ return block.type === "tool_use" && block.name === "Skill";
130
+ }
131
+
132
+ function completeCollection(completed: boolean, state: State): boolean {
133
+ return (
134
+ completed &&
135
+ !state.partial &&
136
+ !state.truncated &&
137
+ [...state.calls.values()].every((item) => item.status !== "unknown")
138
+ );
139
+ }
140
+
141
+ /** Observe the native Skill seam and distinguish successful results from attempts. */
142
+ export function claudeSkillActivationsObservation(
143
+ stream: string | ClaudeEvents,
144
+ code: number,
145
+ ) {
146
+ const events = parseClaudeEvents(stream);
147
+ const summary = summarizeClaudeEvents(events, code);
148
+ const state: State = { calls: new Map(), partial: false, truncated: false };
149
+ for (const line of events.lines) visit(line, state);
150
+ const calls = [...state.calls.values()];
151
+ const complete = completeCollection(summary.complete, state);
152
+ return {
153
+ id: "sevro.host.skill-activations",
154
+ completeness: complete ? ("complete" as const) : ("partial" as const),
155
+ data: {
156
+ method: "skill_tool_result",
157
+ visibility: "skill_tool",
158
+ observedSkills: [
159
+ ...new Set(
160
+ calls
161
+ .filter((item) => item.status === "succeeded")
162
+ .map((item) => item.invocation),
163
+ ),
164
+ ],
165
+ calls,
166
+ truncated: state.truncated,
167
+ },
168
+ };
169
+ }
@@ -1,4 +1,10 @@
1
- import { runtimeSeedSources } from "../runtime-config";
1
+ import { runtimeSeedSources } from "../runtime/config";
2
+ import { minimalRoots } from "../paths";
3
+ import { shellQuote } from "../shell";
4
+ import {
5
+ candidateArtifactEnvironment,
6
+ candidateArtifactReadRoots,
7
+ } from "../candidate-artifacts";
2
8
  import {
3
9
  cp,
4
10
  mkdir,
@@ -10,16 +16,24 @@ import {
10
16
  import { delimiter, dirname, join } from "node:path";
11
17
  import type { HostAdapter } from "../engine";
12
18
  import { nativeStateParent } from "../native-transcript-state";
19
+ import {
20
+ workspaceBoundaryException,
21
+ workspaceStatePath,
22
+ } from "../fixtures/workspace-state";
13
23
  import {
14
24
  candidateTranscriptEnvironment,
15
25
  candidateTranscriptRoots,
16
26
  } from "../native-transcripts";
17
- import { prepareHookMounts, runtimeHooksEnabled } from "../runtime-hooks";
27
+ import { prepareHookMounts, runtimeHooksEnabled } from "../runtime/hooks";
18
28
  import {
19
29
  insideRuntimeRoot as inside,
20
30
  runtimeExecutableProtection,
21
- } from "../runtime-paths";
22
- import { prepareRuntimeState } from "../runtime-state";
31
+ } from "../runtime/paths";
32
+ import { prepareRuntimeState } from "../runtime/state";
33
+ import {
34
+ sharedCacheDenyRoots,
35
+ sharedCacheRoots,
36
+ } from "../runtime/shared-cache";
23
37
  import { stageClaudeAuthentication } from "./claude-credential";
24
38
  import { claudeHostSettings } from "./claude-settings";
25
39
  import { evaluationProtectedRoots } from "./isolation-roots";
@@ -36,12 +50,7 @@ export interface ClaudeHostOptions {
36
50
  toolchainBinDir?: string;
37
51
  projectSettings?: boolean;
38
52
  timeoutMs?: number;
39
- }
40
-
41
- function minimalRoots(roots: string[]): string[] {
42
- return roots.filter(
43
- (root) => !roots.some((other) => other !== root && inside(other, root)),
44
- );
53
+ retainHostDiagnostics?: boolean;
45
54
  }
46
55
 
47
56
  function claudeSeedSources(request: Request): string[] {
@@ -51,6 +60,7 @@ function claudeSeedSources(request: Request): string[] {
51
60
  async function createClaudePrivateState(
52
61
  stateRoot: string,
53
62
  options: ClaudeHostOptions,
63
+ loginLifetimeMs: number,
54
64
  ) {
55
65
  await mkdir(join(stateRoot, "private"), { mode: 0o700 });
56
66
  const privateRoot = await realpath(join(stateRoot, "private"));
@@ -60,6 +70,7 @@ async function createClaudePrivateState(
60
70
  const authentication = await stageClaudeAuthentication(
61
71
  join(privateRoot, "config"),
62
72
  options.credentialFile,
73
+ { binary: options.binary, lifetimeMs: loginLifetimeMs },
63
74
  );
64
75
  return {
65
76
  privateRoot,
@@ -181,14 +192,16 @@ function claudeRuntimeEnvironment(
181
192
  : {};
182
193
  }
183
194
 
195
+ /** `lifetimeMs` is the access-token lifetime the complete execution needs. */
184
196
  export async function prepareClaudeState(
185
197
  stateRoot: string,
186
198
  options: ClaudeHostOptions,
187
199
  request: Request,
188
200
  pluginDirs: string[],
201
+ lifetimeMs: number,
189
202
  ) {
190
203
  await requireClaudeRuntimeNamespace(request, stateRoot);
191
- const state = await createClaudePrivateState(stateRoot, options);
204
+ const state = await createClaudePrivateState(stateRoot, options, lifetimeMs);
192
205
  const transcripts = await claudeTranscriptAccess(
193
206
  stateRoot,
194
207
  state.credential,
@@ -213,6 +226,11 @@ export async function prepareClaudeState(
213
226
  runtime,
214
227
  stateRoot,
215
228
  );
229
+ const cacheRoots = await sharedCacheRoots(
230
+ request.runtimePolicy,
231
+ request.runtimeRole,
232
+ roots,
233
+ );
216
234
  await writeFile(
217
235
  settingsPath,
218
236
  JSON.stringify(
@@ -220,15 +238,20 @@ export async function prepareClaudeState(
220
238
  state.privateRoot,
221
239
  state.credential,
222
240
  [...pluginDirs, ...effectivePlugins],
223
- roots,
241
+ [
242
+ ...roots,
243
+ ...sharedCacheDenyRoots(request.runtimePolicy, request.runtimeRole),
244
+ ],
224
245
  transcripts.policy,
225
246
  [
226
247
  ...transcripts.readRoots,
227
248
  ...candidateTranscriptRoots(request.candidateTranscriptRoot),
249
+ ...candidateArtifactReadRoots(request.candidateArtifactRoots),
228
250
  ],
229
251
  runtimeWriteRoot
230
- ? { root: runtimeWriteRoot, workspace: request.workspace }
252
+ ? { root: runtimeWriteRoot, workspace: request.workspace, cacheRoots }
231
253
  : undefined,
254
+ request.workspace,
232
255
  ),
233
256
  ),
234
257
  { flag: "wx", mode: 0o600 },
@@ -237,6 +260,7 @@ export async function prepareClaudeState(
237
260
  Object.assign(
238
261
  env,
239
262
  candidateTranscriptEnvironment(request.candidateTranscriptRoot),
263
+ candidateArtifactEnvironment(request.candidateArtifactRoots),
240
264
  );
241
265
  if (runtime) await mkdir(join(runtime, "host-home"), { mode: 0o700 });
242
266
  return {
@@ -253,7 +277,11 @@ async function requireClaudeRuntimeNamespace(
253
277
  request: Request,
254
278
  stateRoot: string,
255
279
  ) {
256
- if (!request.runtimePolicy) return;
280
+ if (
281
+ !request.runtimePolicy &&
282
+ !workspaceBoundaryException(workspaceStatePath(), request.workspace).length
283
+ )
284
+ return;
257
285
  const implicitRoots = await Promise.all(
258
286
  ["/tmp/claude", "/private/tmp/claude"].map(implicitClaudeWritePaths),
259
287
  );
@@ -293,7 +321,7 @@ async function claudeTranscriptAccess(
293
321
  await mkdir(projects, { recursive: true, mode: 0o700 });
294
322
  await symlink(projects, join(dirname(credential), "projects"));
295
323
  const startup = join(view, "shell-env");
296
- const script = `export CLAUDE_CONFIG_DIR='${view.replaceAll("'", `'"'"'`)}'\n`;
324
+ const script = `export CLAUDE_CONFIG_DIR=${shellQuote(view)}\n`;
297
325
  await writeFile(startup, script, { flag: "wx", mode: 0o600 });
298
326
  await writeFile(join(view, ".zshenv"), script, { flag: "wx", mode: 0o600 });
299
327
  return {
@@ -350,7 +378,9 @@ async function applyClaudeRuntimeEnvironment(
350
378
  }
351
379
 
352
380
  function claudeRuntimeDirectory(request: Request, stateRoot: string) {
353
- return request.runtimePolicy ? join(stateRoot, "runtime") : undefined;
381
+ return request.runtimePolicy
382
+ ? (request.candidateRuntimeRoot ?? join(stateRoot, "runtime"))
383
+ : undefined;
354
384
  }
355
385
 
356
386
  function applyLegacyClaudeCommandEnvironment(