@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +170 -0
- package/LICENSE +63 -136
- package/README.md +83 -66
- package/docs/README.md +44 -0
- package/docs/acknowledgements.md +14 -0
- package/docs/architecture.md +146 -0
- package/docs/assets/README.md +27 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +1 -0
- package/docs/assets/sevro-wordmark.svg +1 -0
- package/docs/builtin-graders-v1.md +41 -13
- package/docs/codex-host.md +184 -52
- package/docs/contracts.md +19 -0
- package/docs/creating-cases.md +139 -0
- package/docs/development-cli.md +190 -30
- package/docs/documentation-quality.md +61 -0
- package/docs/evaluation-workflow.md +110 -0
- package/docs/evidence/code-mode-controls-71.md +64 -0
- package/docs/evidence/code-mode-programs-85.md +64 -0
- package/docs/evidence/codex-state-73/README.md +204 -0
- package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
- package/docs/evidence/codex-state-73/results.json +661 -0
- package/docs/evidence/local-http-74.md +104 -0
- package/docs/evidence/runtime-startup-30/README.md +185 -0
- package/docs/evidence/runtime-startup-30/after.json +319 -0
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
- package/docs/evidence/runtime-startup-30/before.json +403 -0
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
- package/docs/evidence/skill-reads-70/results.json +823 -0
- package/docs/evidence.md +21 -0
- package/docs/extension-protocol-v1.md +50 -16
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/getting-started.md +102 -0
- package/docs/guide-evaluation.md +89 -0
- package/docs/guide-verification.json +239 -0
- package/docs/identity-v1.md +35 -1
- package/docs/installing.md +88 -0
- package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
- package/docs/licensing.md +42 -0
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +228 -0
- package/docs/reading-results.md +141 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +81 -40
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +180 -0
- package/docs/run-settings-v1.md +147 -0
- package/docs/running-evaluations.md +31 -0
- package/docs/runtime-v1.md +614 -0
- package/docs/specs/repository-guide.md +58 -0
- package/docs/troubleshooting.md +30 -0
- package/docs/typescript-quality.md +234 -0
- package/docs/usability-study.md +114 -0
- package/examples/basic/host.ts +2 -2
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +39 -6
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +193 -0
- package/src/advisory.ts +10 -5
- package/src/candidate-artifacts.ts +208 -0
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +105 -0
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +81 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +59 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +222 -0
- package/src/cli/hosts.ts +59 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/cli/invocation.ts +627 -0
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +163 -0
- package/src/cli/presentation.ts +91 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +133 -0
- package/src/cli/run-options.ts +101 -0
- package/src/cli/run-overrides.ts +83 -0
- package/src/cli/settings.ts +160 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -843
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +19 -2373
- package/src/evaluation/advisory.ts +150 -0
- package/src/evaluation/check-plan.ts +510 -0
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/evaluation/grading-host.ts +39 -0
- package/src/evaluation/grading.ts +258 -0
- package/src/evaluation/invocation-plan.ts +548 -0
- package/src/evaluation/plan.ts +736 -0
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
- package/src/evaluation/semantic.ts +312 -0
- package/src/evaluation/trial.ts +866 -0
- package/src/evaluation/types.ts +207 -0
- package/src/evidence/host-results.ts +92 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +228 -148
- package/src/extension-session.ts +326 -195
- package/src/fixtures/advisory.ts +338 -0
- package/src/fixtures/bin.ts +57 -0
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +187 -0
- package/src/fixtures/generated.ts +300 -0
- package/src/fixtures/git-hooks.ts +24 -0
- package/src/fixtures/preparation.ts +270 -0
- package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
- package/src/fixtures/setup.ts +224 -0
- package/src/fixtures/tools.ts +88 -0
- package/src/fixtures/workspace-state.ts +212 -0
- package/src/generated/cli-result.cjs +2 -0
- package/src/generated/cli-result.d.cts +3 -0
- package/src/generated/extension.cjs +2 -0
- package/src/generated/extension.d.cts +3 -0
- package/src/generated/report.cjs +2 -0
- package/src/generated/report.d.cts +3 -0
- package/src/generated/run-evidence.cjs +2 -0
- package/src/generated/run-evidence.d.cts +3 -0
- package/src/generated/runtime.cjs +2 -0
- package/src/generated/runtime.d.cts +3 -0
- package/src/graders/artifact.ts +56 -23
- package/src/graders/git-head.ts +79 -75
- package/src/graders/output.ts +222 -140
- package/src/graders/semantic.ts +102 -43
- package/src/graders/shell.ts +379 -207
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +319 -0
- package/src/hosts/claude-continuation.ts +63 -18
- package/src/hosts/claude-credential.ts +43 -62
- package/src/hosts/claude-events.ts +86 -51
- package/src/hosts/claude-keychain.ts +59 -0
- package/src/hosts/claude-native-goal.ts +57 -33
- package/src/hosts/claude-process.ts +111 -0
- package/src/hosts/claude-repository-invocation.ts +208 -117
- package/src/hosts/claude-settings.ts +240 -21
- package/src/hosts/claude-skill-activations.ts +166 -0
- package/src/hosts/claude-state.ts +403 -0
- package/src/hosts/claude-tool-calls.ts +171 -106
- package/src/hosts/claude.ts +561 -451
- package/src/hosts/codex-app-server.ts +326 -178
- package/src/hosts/codex-configuration.ts +44 -18
- package/src/hosts/codex-events.ts +97 -60
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-invocation.ts +70 -0
- package/src/hosts/codex-marketplace.ts +283 -0
- package/src/hosts/codex-native-calls.ts +546 -500
- package/src/hosts/codex-process.ts +202 -0
- package/src/hosts/codex-profile.ts +257 -28
- package/src/hosts/codex-session-files.ts +106 -0
- package/src/hosts/codex-state.ts +492 -0
- package/src/hosts/codex.ts +783 -832
- package/src/hosts/isolation-common.ts +94 -0
- package/src/hosts/isolation-roots.ts +54 -27
- package/src/hosts/isolation.ts +10 -0
- package/src/hosts/linux-sandbox.ts +159 -0
- package/src/hosts/mac-sandbox.ts +187 -61
- package/src/hosts/native-controls.ts +152 -62
- package/src/hosts/process-lifecycle.ts +37 -0
- package/src/hosts/protected-worktrees.ts +51 -19
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +580 -0
- package/src/hosts/workspace-fingerprint.ts +55 -35
- package/src/identity.ts +102 -48
- package/src/instrumentation.ts +109 -48
- package/src/native-transcript-state.ts +131 -0
- package/src/native-transcript-view.ts +90 -0
- package/src/native-transcripts.ts +286 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +161 -81
- package/src/prune.ts +215 -0
- package/src/report.ts +172 -140
- package/src/resolved-case.ts +33 -0
- package/src/results-lock.ts +29 -0
- package/src/results.ts +133 -86
- package/src/run-owner.ts +154 -63
- package/src/runtime/config.ts +456 -0
- package/src/runtime/discovery-apple.ts +90 -0
- package/src/runtime/discovery-providers.ts +63 -0
- package/src/runtime/discovery.ts +70 -0
- package/src/runtime/hook-processes.ts +97 -0
- package/src/runtime/hooks.ts +476 -0
- package/src/runtime/paths.ts +313 -0
- package/src/runtime/query.ts +85 -0
- package/src/runtime/seeds.ts +134 -0
- package/src/runtime/shared-cache.ts +143 -0
- package/src/runtime/state.ts +143 -0
- package/src/schema-types.ts +198 -0
- package/src/schema.ts +15 -19
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +20 -0
- package/src/advisory-fixture.ts +0 -282
- package/src/fixture-bin.ts +0 -87
- package/src/fixture-setup.ts +0 -163
- package/src/generated-fixture.ts +0 -220
- package/src/git-hooks.ts +0 -63
- package/src/hosts/claude-nested-skills.ts +0 -218
- package/src/hosts/codex-command-output.ts +0 -399
- package/src/hosts/codex-executor-command.ts +0 -195
- package/src/hosts/codex-skill-reads.ts +0 -391
- package/src/preparation.ts +0 -211
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { runtimeSeedSources } from "../runtime/config";
|
|
2
|
+
import {
|
|
3
|
+
candidateArtifactEnvironment,
|
|
4
|
+
candidateArtifactReadRoots,
|
|
5
|
+
} from "../candidate-artifacts";
|
|
6
|
+
import {
|
|
7
|
+
cp,
|
|
8
|
+
mkdir,
|
|
9
|
+
realpath,
|
|
10
|
+
stat,
|
|
11
|
+
symlink,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "node:fs/promises";
|
|
14
|
+
import { delimiter, dirname, join } from "node:path";
|
|
15
|
+
import type { HostAdapter } from "../engine";
|
|
16
|
+
import { nativeStateParent } from "../native-transcript-state";
|
|
17
|
+
import {
|
|
18
|
+
workspaceBoundaryException,
|
|
19
|
+
workspaceStatePath,
|
|
20
|
+
} from "../fixtures/workspace-state";
|
|
21
|
+
import {
|
|
22
|
+
candidateTranscriptEnvironment,
|
|
23
|
+
candidateTranscriptRoots,
|
|
24
|
+
} from "../native-transcripts";
|
|
25
|
+
import { prepareHookMounts, runtimeHooksEnabled } from "../runtime/hooks";
|
|
26
|
+
import {
|
|
27
|
+
insideRuntimeRoot as inside,
|
|
28
|
+
runtimeExecutableProtection,
|
|
29
|
+
} from "../runtime/paths";
|
|
30
|
+
import { prepareRuntimeState } from "../runtime/state";
|
|
31
|
+
import {
|
|
32
|
+
sharedCacheDenyRoots,
|
|
33
|
+
sharedCacheRoots,
|
|
34
|
+
} from "../runtime/shared-cache";
|
|
35
|
+
import { stageClaudeAuthentication } from "./claude-credential";
|
|
36
|
+
import { claudeHostSettings } from "./claude-settings";
|
|
37
|
+
import { evaluationProtectedRoots } from "./isolation-roots";
|
|
38
|
+
type Request = Parameters<HostAdapter["run"]>[0];
|
|
39
|
+
export interface ClaudeHostOptions {
|
|
40
|
+
binary: string;
|
|
41
|
+
model: string;
|
|
42
|
+
effort: string;
|
|
43
|
+
projectRoot: string;
|
|
44
|
+
resultsRoot: string;
|
|
45
|
+
additionalProtectedRoots: string[];
|
|
46
|
+
credentialFile?: string;
|
|
47
|
+
uvCacheDir?: string;
|
|
48
|
+
toolchainBinDir?: string;
|
|
49
|
+
projectSettings?: boolean;
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
retainHostDiagnostics?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function minimalRoots(roots: string[]): string[] {
|
|
55
|
+
return roots.filter(
|
|
56
|
+
(root) => !roots.some((other) => other !== root && inside(other, root)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function claudeSeedSources(request: Request): string[] {
|
|
61
|
+
return runtimeSeedSources(request.runtimePolicy);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function createClaudePrivateState(
|
|
65
|
+
stateRoot: string,
|
|
66
|
+
options: ClaudeHostOptions,
|
|
67
|
+
) {
|
|
68
|
+
await mkdir(join(stateRoot, "private"), { mode: 0o700 });
|
|
69
|
+
const privateRoot = await realpath(join(stateRoot, "private"));
|
|
70
|
+
const home = join(stateRoot, "home"),
|
|
71
|
+
temp = join(stateRoot, "tmp");
|
|
72
|
+
await Promise.all([home, temp].map((path) => mkdir(path, { mode: 0o700 })));
|
|
73
|
+
const authentication = await stageClaudeAuthentication(
|
|
74
|
+
join(privateRoot, "config"),
|
|
75
|
+
options.credentialFile,
|
|
76
|
+
);
|
|
77
|
+
return {
|
|
78
|
+
privateRoot,
|
|
79
|
+
home,
|
|
80
|
+
temp,
|
|
81
|
+
authentication,
|
|
82
|
+
credential: authentication.credentialFile,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function protectedClaudeRoots(
|
|
87
|
+
request: Request,
|
|
88
|
+
options: ClaudeHostOptions,
|
|
89
|
+
credential: string,
|
|
90
|
+
) {
|
|
91
|
+
const candidates = await evaluationProtectedRoots({
|
|
92
|
+
ownedStateRoot: dirname(dirname(dirname(credential))),
|
|
93
|
+
workspace: request.workspace,
|
|
94
|
+
projectRoot: options.projectRoot,
|
|
95
|
+
resultsRoot: options.resultsRoot,
|
|
96
|
+
additionalRoots: [
|
|
97
|
+
...options.additionalProtectedRoots,
|
|
98
|
+
...claudeSeedSources(request),
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
const roots = request.runtimePolicy ? candidates : minimalRoots(candidates);
|
|
102
|
+
const namespace = await nativeStateParent();
|
|
103
|
+
if (roots.some((root) => root !== namespace && inside(root, credential)))
|
|
104
|
+
throw new Error("Claude private state overlaps a protected root");
|
|
105
|
+
const binary = await realpath(options.binary);
|
|
106
|
+
const executableRoots = runtimeExecutableProtection(
|
|
107
|
+
[dirname(binary)],
|
|
108
|
+
roots,
|
|
109
|
+
Boolean(request.runtimePolicy),
|
|
110
|
+
);
|
|
111
|
+
if (executableRoots.some((root) => inside(root, binary)))
|
|
112
|
+
throw new Error("Claude executable resides inside a protected root");
|
|
113
|
+
return roots;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function claudeToolchainDirectory(
|
|
117
|
+
options: ClaudeHostOptions,
|
|
118
|
+
roots: string[],
|
|
119
|
+
): Promise<string | null> {
|
|
120
|
+
const directory = options.toolchainBinDir
|
|
121
|
+
? await realpath(options.toolchainBinDir)
|
|
122
|
+
: null;
|
|
123
|
+
if (
|
|
124
|
+
directory &&
|
|
125
|
+
(roots.some((root) => inside(root, directory)) ||
|
|
126
|
+
!(await stat(directory)).isDirectory())
|
|
127
|
+
)
|
|
128
|
+
throw new Error("Claude toolchain directory is unavailable or protected");
|
|
129
|
+
return directory;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function prepareClaudeRuntime(
|
|
133
|
+
options: ClaudeHostOptions,
|
|
134
|
+
roots: string[],
|
|
135
|
+
workspace: string,
|
|
136
|
+
): Promise<string | null> {
|
|
137
|
+
if (!options.uvCacheDir) return null;
|
|
138
|
+
const cache = await realpath(options.uvCacheDir);
|
|
139
|
+
if (
|
|
140
|
+
roots.some((root) => inside(root, cache)) ||
|
|
141
|
+
!(await stat(cache)).isDirectory()
|
|
142
|
+
)
|
|
143
|
+
throw new Error("Claude UV cache is unavailable or protected");
|
|
144
|
+
const runtimeRoot = join(workspace, ".git", "sevro-runtime");
|
|
145
|
+
await mkdir(runtimeRoot, { recursive: true, mode: 0o700 });
|
|
146
|
+
await cp(cache, join(runtimeRoot, "uv-cache"), {
|
|
147
|
+
recursive: true,
|
|
148
|
+
force: false,
|
|
149
|
+
errorOnExist: true,
|
|
150
|
+
});
|
|
151
|
+
return runtimeRoot;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function claudeEnvironment(
|
|
155
|
+
state: Awaited<ReturnType<typeof createClaudePrivateState>>,
|
|
156
|
+
request: Request,
|
|
157
|
+
toolchain: string | null,
|
|
158
|
+
runtime: string | null,
|
|
159
|
+
): Record<string, string> {
|
|
160
|
+
return {
|
|
161
|
+
...state.authentication.environment,
|
|
162
|
+
...request.runtimePolicy?.environment,
|
|
163
|
+
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
|
|
164
|
+
PATH: claudePath(request, toolchain),
|
|
165
|
+
LANG: process.env.LANG ?? "C",
|
|
166
|
+
HOME: runtime ? join(runtime, "host-home") : state.home,
|
|
167
|
+
TMPDIR: state.temp,
|
|
168
|
+
CLAUDE_CONFIG_DIR: dirname(state.credential),
|
|
169
|
+
NO_COLOR: "1",
|
|
170
|
+
...claudeRuntimeEnvironment(runtime),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function claudePath(request: Request, toolchain: string | null): string {
|
|
175
|
+
const path =
|
|
176
|
+
request.runtimePolicy?.environment.PATH ??
|
|
177
|
+
process.env.PATH ??
|
|
178
|
+
"/usr/bin:/bin";
|
|
179
|
+
return [request.fixtureBinDir, toolchain, path]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join(delimiter);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function claudeRuntimeEnvironment(
|
|
185
|
+
runtime: string | null,
|
|
186
|
+
): Record<string, string> {
|
|
187
|
+
return runtime
|
|
188
|
+
? {
|
|
189
|
+
UV_CACHE_DIR: join(runtime, "uv-cache"),
|
|
190
|
+
UV_PROJECT_ENVIRONMENT: join(runtime, "project-environment"),
|
|
191
|
+
UV_OFFLINE: "1",
|
|
192
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
193
|
+
}
|
|
194
|
+
: {};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function prepareClaudeState(
|
|
198
|
+
stateRoot: string,
|
|
199
|
+
options: ClaudeHostOptions,
|
|
200
|
+
request: Request,
|
|
201
|
+
pluginDirs: string[],
|
|
202
|
+
) {
|
|
203
|
+
await requireClaudeRuntimeNamespace(request, stateRoot);
|
|
204
|
+
const state = await createClaudePrivateState(stateRoot, options);
|
|
205
|
+
const transcripts = await claudeTranscriptAccess(
|
|
206
|
+
stateRoot,
|
|
207
|
+
state.credential,
|
|
208
|
+
request,
|
|
209
|
+
);
|
|
210
|
+
const roots = await protectedClaudeRoots(request, options, state.credential);
|
|
211
|
+
const toolchain = await claudeToolchainDirectory(options, roots);
|
|
212
|
+
const runtime = await prepareClaudeRuntime(options, roots, request.workspace);
|
|
213
|
+
const settingsPath = join(state.privateRoot, "settings.json");
|
|
214
|
+
const hooks = await claudeHookMounts(
|
|
215
|
+
request,
|
|
216
|
+
stateRoot,
|
|
217
|
+
state.privateRoot,
|
|
218
|
+
pluginDirs,
|
|
219
|
+
roots,
|
|
220
|
+
);
|
|
221
|
+
const effectivePlugins = hooks?.directories ?? pluginDirs;
|
|
222
|
+
const env = claudeEnvironment(state, request, toolchain, runtime);
|
|
223
|
+
const runtimeWriteRoot = await applyClaudeRuntimeEnvironment(
|
|
224
|
+
request,
|
|
225
|
+
env,
|
|
226
|
+
runtime,
|
|
227
|
+
stateRoot,
|
|
228
|
+
);
|
|
229
|
+
const cacheRoots = await sharedCacheRoots(
|
|
230
|
+
request.runtimePolicy,
|
|
231
|
+
request.runtimeRole,
|
|
232
|
+
roots,
|
|
233
|
+
);
|
|
234
|
+
await writeFile(
|
|
235
|
+
settingsPath,
|
|
236
|
+
JSON.stringify(
|
|
237
|
+
claudeHostSettings(
|
|
238
|
+
state.privateRoot,
|
|
239
|
+
state.credential,
|
|
240
|
+
[...pluginDirs, ...effectivePlugins],
|
|
241
|
+
[
|
|
242
|
+
...roots,
|
|
243
|
+
...sharedCacheDenyRoots(request.runtimePolicy, request.runtimeRole),
|
|
244
|
+
],
|
|
245
|
+
transcripts.policy,
|
|
246
|
+
[
|
|
247
|
+
...transcripts.readRoots,
|
|
248
|
+
...candidateTranscriptRoots(request.candidateTranscriptRoot),
|
|
249
|
+
...candidateArtifactReadRoots(request.candidateArtifactRoots),
|
|
250
|
+
],
|
|
251
|
+
runtimeWriteRoot
|
|
252
|
+
? { root: runtimeWriteRoot, workspace: request.workspace, cacheRoots }
|
|
253
|
+
: undefined,
|
|
254
|
+
request.workspace,
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
{ flag: "wx", mode: 0o600 },
|
|
258
|
+
);
|
|
259
|
+
Object.assign(env, transcripts.environment);
|
|
260
|
+
Object.assign(
|
|
261
|
+
env,
|
|
262
|
+
candidateTranscriptEnvironment(request.candidateTranscriptRoot),
|
|
263
|
+
candidateArtifactEnvironment(request.candidateArtifactRoots),
|
|
264
|
+
);
|
|
265
|
+
if (runtime) await mkdir(join(runtime, "host-home"), { mode: 0o700 });
|
|
266
|
+
return {
|
|
267
|
+
credential: state.credential,
|
|
268
|
+
settingsPath,
|
|
269
|
+
env,
|
|
270
|
+
hooks,
|
|
271
|
+
pluginDirs: effectivePlugins,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Claude's implicit /tmp/claude writes cannot contain the read-denied peer namespace. */
|
|
276
|
+
async function requireClaudeRuntimeNamespace(
|
|
277
|
+
request: Request,
|
|
278
|
+
stateRoot: string,
|
|
279
|
+
) {
|
|
280
|
+
if (
|
|
281
|
+
!request.runtimePolicy &&
|
|
282
|
+
!workspaceBoundaryException(workspaceStatePath(), request.workspace).length
|
|
283
|
+
)
|
|
284
|
+
return;
|
|
285
|
+
const implicitRoots = await Promise.all(
|
|
286
|
+
["/tmp/claude", "/private/tmp/claude"].map(implicitClaudeWritePaths),
|
|
287
|
+
);
|
|
288
|
+
const namespace = dirname(stateRoot);
|
|
289
|
+
if (
|
|
290
|
+
implicitRoots
|
|
291
|
+
.flat()
|
|
292
|
+
.some((root) => inside(root, namespace) || inside(namespace, root))
|
|
293
|
+
)
|
|
294
|
+
throw new Error(
|
|
295
|
+
"Claude namespace overlaps an implicit native writable root",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function implicitClaudeWritePaths(root: string): Promise<string[]> {
|
|
300
|
+
try {
|
|
301
|
+
return [root, await realpath(root)];
|
|
302
|
+
} catch (cause) {
|
|
303
|
+
if ((cause as NodeJS.ErrnoException).code !== "ENOENT")
|
|
304
|
+
throw new Error("Claude implicit native writable root is unreadable", {
|
|
305
|
+
cause,
|
|
306
|
+
});
|
|
307
|
+
return [root];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function claudeTranscriptAccess(
|
|
312
|
+
stateRoot: string,
|
|
313
|
+
credential: string,
|
|
314
|
+
request: Request,
|
|
315
|
+
) {
|
|
316
|
+
const policy = request.runtimePolicy;
|
|
317
|
+
if (!policy?.nativeTranscripts)
|
|
318
|
+
return { policy, environment: {}, readRoots: [] };
|
|
319
|
+
const view = join(stateRoot, "native-home");
|
|
320
|
+
const projects = join(view, "projects");
|
|
321
|
+
await mkdir(projects, { recursive: true, mode: 0o700 });
|
|
322
|
+
await symlink(projects, join(dirname(credential), "projects"));
|
|
323
|
+
const startup = join(view, "shell-env");
|
|
324
|
+
const script = `export CLAUDE_CONFIG_DIR='${view.replaceAll("'", `'"'"'`)}'\n`;
|
|
325
|
+
await writeFile(startup, script, { flag: "wx", mode: 0o600 });
|
|
326
|
+
await writeFile(join(view, ".zshenv"), script, { flag: "wx", mode: 0o600 });
|
|
327
|
+
return {
|
|
328
|
+
policy,
|
|
329
|
+
readRoots: [projects, view],
|
|
330
|
+
environment: {
|
|
331
|
+
SEVRO_NATIVE_TRANSCRIPT_ROOT: projects,
|
|
332
|
+
BASH_ENV: startup,
|
|
333
|
+
ZDOTDIR: view,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function claudeHookMounts(
|
|
339
|
+
request: Request,
|
|
340
|
+
stateRoot: string,
|
|
341
|
+
privateRoot: string,
|
|
342
|
+
pluginDirs: string[],
|
|
343
|
+
roots: string[],
|
|
344
|
+
) {
|
|
345
|
+
const policy = request.runtimePolicy;
|
|
346
|
+
if (!policy || !runtimeHooksEnabled(policy)) return undefined;
|
|
347
|
+
return prepareHookMounts({
|
|
348
|
+
workspace: request.workspace,
|
|
349
|
+
stateRoot,
|
|
350
|
+
privateRoot,
|
|
351
|
+
pluginRoots: pluginDirs,
|
|
352
|
+
protectedRoots: [privateRoot, ...roots, ...runtimeSeedSources(policy)],
|
|
353
|
+
policy,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function applyClaudeRuntimeEnvironment(
|
|
358
|
+
request: Request,
|
|
359
|
+
env: Record<string, string>,
|
|
360
|
+
runtime: string | null,
|
|
361
|
+
stateRoot: string,
|
|
362
|
+
): Promise<string | undefined> {
|
|
363
|
+
const isolated = await prepareRuntimeState(
|
|
364
|
+
request.workspace,
|
|
365
|
+
claudeCommandPolicy(request),
|
|
366
|
+
request.runtimeRole ?? "candidate",
|
|
367
|
+
claudeRuntimeDirectory(request, stateRoot),
|
|
368
|
+
);
|
|
369
|
+
if (request.runtimePolicy) {
|
|
370
|
+
Object.assign(env, isolated.environment);
|
|
371
|
+
env.PATH = [request.fixtureBinDir, isolated.environment.PATH ?? env.PATH]
|
|
372
|
+
.filter(Boolean)
|
|
373
|
+
.join(delimiter);
|
|
374
|
+
return isolated.root;
|
|
375
|
+
}
|
|
376
|
+
applyLegacyClaudeCommandEnvironment(env, isolated, runtime);
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function claudeRuntimeDirectory(request: Request, stateRoot: string) {
|
|
381
|
+
return request.runtimePolicy
|
|
382
|
+
? (request.candidateRuntimeRoot ?? join(stateRoot, "runtime"))
|
|
383
|
+
: undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function applyLegacyClaudeCommandEnvironment(
|
|
387
|
+
env: Record<string, string>,
|
|
388
|
+
isolated: Awaited<ReturnType<typeof prepareRuntimeState>>,
|
|
389
|
+
runtime: string | null,
|
|
390
|
+
): void {
|
|
391
|
+
if (!runtime) env.HOME = isolated.home;
|
|
392
|
+
env.TMPDIR = isolated.temp;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function claudeCommandPolicy(request: Request) {
|
|
396
|
+
return (
|
|
397
|
+
request.runtimePolicy ?? {
|
|
398
|
+
format: "sevro.runtime.v1" as const,
|
|
399
|
+
environment: {},
|
|
400
|
+
readOnlyRoots: [],
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
}
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256 } from "../identity";
|
|
2
|
+
import { isRecord } from "../value-guards";
|
|
2
3
|
import { summarizeClaudeEvents } from "./claude-events";
|
|
3
4
|
|
|
4
5
|
const MAX_RETAINED_CALLS = 128;
|
|
5
6
|
type Entry = Record<string, unknown>;
|
|
6
7
|
|
|
7
|
-
function record(value: unknown): value is Entry {
|
|
8
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
8
|
function label(value: unknown, max = 256): string | null {
|
|
12
|
-
return typeof value === "string" &&
|
|
13
|
-
value.length >= 1 &&
|
|
14
|
-
value.length <= max &&
|
|
15
|
-
/^[A-Za-z0-9_.:-]+$/.test(value)
|
|
16
|
-
? value
|
|
17
|
-
: null;
|
|
9
|
+
return typeof value === "string" && boundedLabel(value, max) ? value : null;
|
|
18
10
|
}
|
|
19
11
|
|
|
20
12
|
interface SkillCall {
|
|
@@ -49,111 +41,184 @@ export interface ClaudeToolCallsObservation {
|
|
|
49
41
|
};
|
|
50
42
|
}
|
|
51
43
|
|
|
44
|
+
interface CallState {
|
|
45
|
+
calls: Array<SkillCall | AgentCall>;
|
|
46
|
+
malformed: boolean;
|
|
47
|
+
truncated: boolean;
|
|
48
|
+
ordinal: number;
|
|
49
|
+
}
|
|
50
|
+
type Actor = { actor: "parent" | "nested"; parentToolUseId: string | null };
|
|
51
|
+
|
|
52
52
|
/** Retain ordered host call metadata, never tool prompts or results. */
|
|
53
53
|
export function claudeToolCallsObservation(
|
|
54
54
|
stream: string,
|
|
55
55
|
exitCode: number,
|
|
56
56
|
): ClaudeToolCallsObservation {
|
|
57
57
|
const summary = summarizeClaudeEvents(stream, exitCode);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
const state: CallState = {
|
|
59
|
+
calls: [],
|
|
60
|
+
malformed: false,
|
|
61
|
+
truncated: false,
|
|
62
|
+
ordinal: 0,
|
|
63
|
+
};
|
|
62
64
|
for (const line of stream.split("\n")) {
|
|
63
65
|
if (!line.trim()) continue;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
event = JSON.parse(line);
|
|
67
|
-
} catch {
|
|
68
|
-
malformed = true;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (!record(event) || event.type !== "assistant") continue;
|
|
72
|
-
const blocks = record(event.message) ? event.message.content : null;
|
|
73
|
-
if (!Array.isArray(blocks)) {
|
|
74
|
-
malformed = true;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const parentToolUseId = label(event.parent_tool_use_id, 128);
|
|
78
|
-
const actor = parentToolUseId ? "nested" : "parent";
|
|
79
|
-
if (
|
|
80
|
-
event.parent_tool_use_id !== undefined &&
|
|
81
|
-
event.parent_tool_use_id !== null &&
|
|
82
|
-
actor === "parent"
|
|
83
|
-
)
|
|
84
|
-
malformed = true;
|
|
85
|
-
for (const value of blocks) {
|
|
86
|
-
if (!record(value) || value.type !== "tool_use") continue;
|
|
87
|
-
ordinal++;
|
|
88
|
-
if (!["Skill", "Agent", "Task"].includes(String(value.name))) continue;
|
|
89
|
-
if (calls.length >= MAX_RETAINED_CALLS) {
|
|
90
|
-
truncated = true;
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
const input = record(value.input) ? value.input : null;
|
|
94
|
-
if (value.name === "Skill") {
|
|
95
|
-
const invocation = label(input?.skill);
|
|
96
|
-
if (!invocation) {
|
|
97
|
-
malformed = true;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
calls.push({
|
|
101
|
-
ordinal,
|
|
102
|
-
actor,
|
|
103
|
-
parentToolUseId,
|
|
104
|
-
name: "Skill",
|
|
105
|
-
skill: invocation.split(":").at(-1)!,
|
|
106
|
-
invocation,
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const toolUseId = label(value.id, 128);
|
|
111
|
-
const subagentType = label(input?.subagent_type ?? input?.subagentType);
|
|
112
|
-
const runInBackground =
|
|
113
|
-
typeof input?.run_in_background === "boolean"
|
|
114
|
-
? input.run_in_background
|
|
115
|
-
: null;
|
|
116
|
-
const model = input?.model === undefined ? null : label(input.model, 128);
|
|
117
|
-
const boundedPrompt =
|
|
118
|
-
typeof input?.prompt === "string" &&
|
|
119
|
-
Buffer.byteLength(input.prompt, "utf8") <= 1024 * 1024
|
|
120
|
-
? input.prompt
|
|
121
|
-
: null;
|
|
122
|
-
const promptSha256 = boundedPrompt
|
|
123
|
-
? createHash("sha256").update(boundedPrompt).digest("hex")
|
|
124
|
-
: null;
|
|
125
|
-
const promptFirstLineSha256 = boundedPrompt
|
|
126
|
-
? createHash("sha256")
|
|
127
|
-
.update(boundedPrompt.split(/\r?\n/, 1)[0]!)
|
|
128
|
-
.digest("hex")
|
|
129
|
-
: null;
|
|
130
|
-
if (
|
|
131
|
-
!toolUseId ||
|
|
132
|
-
!subagentType ||
|
|
133
|
-
runInBackground === null ||
|
|
134
|
-
(input?.model !== undefined && !model) ||
|
|
135
|
-
!promptSha256
|
|
136
|
-
)
|
|
137
|
-
malformed = true;
|
|
138
|
-
calls.push({
|
|
139
|
-
ordinal,
|
|
140
|
-
actor,
|
|
141
|
-
parentToolUseId,
|
|
142
|
-
name: "Agent",
|
|
143
|
-
toolUseId,
|
|
144
|
-
subagentType,
|
|
145
|
-
runInBackground,
|
|
146
|
-
model,
|
|
147
|
-
promptSha256,
|
|
148
|
-
promptFirstLineSha256,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (truncated) break;
|
|
66
|
+
collectEvent(line, state);
|
|
67
|
+
if (state.truncated) break;
|
|
152
68
|
}
|
|
153
69
|
return {
|
|
154
70
|
id: "sevro.claude.tool-calls",
|
|
155
|
-
completeness:
|
|
156
|
-
|
|
157
|
-
|
|
71
|
+
completeness: callCompleteness(summary.complete, state),
|
|
72
|
+
data: {
|
|
73
|
+
method: "stream_tool_calls",
|
|
74
|
+
calls: state.calls,
|
|
75
|
+
truncated: state.truncated,
|
|
76
|
+
},
|
|
158
77
|
};
|
|
159
78
|
}
|
|
79
|
+
|
|
80
|
+
function callCompleteness(
|
|
81
|
+
complete: boolean,
|
|
82
|
+
state: CallState,
|
|
83
|
+
): "complete" | "partial" {
|
|
84
|
+
return complete && !state.malformed && !state.truncated
|
|
85
|
+
? "complete"
|
|
86
|
+
: "partial";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collectEvent(line: string, state: CallState): void {
|
|
90
|
+
let event: unknown;
|
|
91
|
+
try {
|
|
92
|
+
event = JSON.parse(line);
|
|
93
|
+
} catch {
|
|
94
|
+
state.malformed = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!isRecord(event) || event.type !== "assistant") return;
|
|
98
|
+
collectAssistant(event, state);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function collectAssistant(event: Entry, state: CallState): void {
|
|
102
|
+
const blocks: unknown = isRecord(event.message)
|
|
103
|
+
? event.message.content
|
|
104
|
+
: null;
|
|
105
|
+
if (!Array.isArray(blocks)) {
|
|
106
|
+
state.malformed = true;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const actor = actorContext(event, state);
|
|
110
|
+
for (const block of blocks) {
|
|
111
|
+
collectBlock(block, actor, state);
|
|
112
|
+
if (state.truncated) break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function actorContext(event: Entry, state: CallState): Actor {
|
|
117
|
+
const parentToolUseId = label(event.parent_tool_use_id, 128);
|
|
118
|
+
const actor = parentToolUseId ? "nested" : "parent";
|
|
119
|
+
if (
|
|
120
|
+
event.parent_tool_use_id !== undefined &&
|
|
121
|
+
event.parent_tool_use_id !== null &&
|
|
122
|
+
actor === "parent"
|
|
123
|
+
)
|
|
124
|
+
state.malformed = true;
|
|
125
|
+
return { actor, parentToolUseId };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectBlock(value: unknown, actor: Actor, state: CallState): void {
|
|
129
|
+
if (!isRecord(value) || value.type !== "tool_use") return;
|
|
130
|
+
state.ordinal++;
|
|
131
|
+
if (!["Skill", "Agent", "Task"].includes(String(value.name))) return;
|
|
132
|
+
if (state.calls.length >= MAX_RETAINED_CALLS) {
|
|
133
|
+
state.truncated = true;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
collectSupportedCall(value, actor, state);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function collectSupportedCall(
|
|
140
|
+
value: Entry,
|
|
141
|
+
actor: Actor,
|
|
142
|
+
state: CallState,
|
|
143
|
+
): void {
|
|
144
|
+
const input = isRecord(value.input) ? value.input : {};
|
|
145
|
+
if (value.name === "Skill") {
|
|
146
|
+
collectSkill(input, actor, state);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const call = agentCall(value, input, actor, state.ordinal);
|
|
150
|
+
if (!validAgentCall(call, input)) state.malformed = true;
|
|
151
|
+
state.calls.push(call);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function collectSkill(input: Entry, actor: Actor, state: CallState): void {
|
|
155
|
+
const invocation = label(input.skill);
|
|
156
|
+
if (!invocation) {
|
|
157
|
+
state.malformed = true;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
state.calls.push({
|
|
161
|
+
ordinal: state.ordinal,
|
|
162
|
+
...actor,
|
|
163
|
+
name: "Skill",
|
|
164
|
+
skill: invocation.slice(invocation.lastIndexOf(":") + 1),
|
|
165
|
+
invocation,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function boundedPrompt(input: Entry): string | null {
|
|
170
|
+
if (typeof input.prompt !== "string") return null;
|
|
171
|
+
return Buffer.byteLength(input.prompt, "utf8") <= 1024 * 1024
|
|
172
|
+
? input.prompt
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function promptDigest(prompt: string | null): string | null {
|
|
177
|
+
return prompt ? sha256(prompt) : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function firstLineDigest(prompt: string | null): string | null {
|
|
181
|
+
return prompt ? sha256(prompt.split(/\r?\n/, 1).join("")) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function agentCall(
|
|
185
|
+
value: Entry,
|
|
186
|
+
input: Entry,
|
|
187
|
+
actor: Actor,
|
|
188
|
+
ordinal: number,
|
|
189
|
+
): AgentCall {
|
|
190
|
+
const prompt = boundedPrompt(input);
|
|
191
|
+
return {
|
|
192
|
+
ordinal,
|
|
193
|
+
...actor,
|
|
194
|
+
name: "Agent",
|
|
195
|
+
toolUseId: label(value.id, 128),
|
|
196
|
+
subagentType: label(input.subagent_type ?? input.subagentType),
|
|
197
|
+
runInBackground:
|
|
198
|
+
typeof input.run_in_background === "boolean"
|
|
199
|
+
? input.run_in_background
|
|
200
|
+
: null,
|
|
201
|
+
model: input.model === undefined ? null : label(input.model, 128),
|
|
202
|
+
promptSha256: promptDigest(prompt),
|
|
203
|
+
promptFirstLineSha256: firstLineDigest(prompt),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validAgentCall(call: AgentCall, input: Entry): boolean {
|
|
208
|
+
const required = [call.toolUseId, call.subagentType, call.promptSha256];
|
|
209
|
+
return (
|
|
210
|
+
required.every(Boolean) &&
|
|
211
|
+
call.runInBackground !== null &&
|
|
212
|
+
validAgentModel(input, call.model)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function validAgentModel(input: Entry, model: string | null): boolean {
|
|
217
|
+
return input.model === undefined || model !== null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function boundedLabel(value: string, max: number): boolean {
|
|
221
|
+
return (
|
|
222
|
+
value.length >= 1 && value.length <= max && /^[A-Za-z0-9_.:-]+$/.test(value)
|
|
223
|
+
);
|
|
224
|
+
}
|