@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
package/src/hosts/claude.ts
CHANGED
|
@@ -1,167 +1,110 @@
|
|
|
1
|
-
import { observeClaudeNativeGoal } from "./claude-native-goal";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
3
|
+
requireCandidateRuntime,
|
|
4
|
+
requireCandidateArtifactRoots,
|
|
5
|
+
} from "../candidate-artifacts";
|
|
6
|
+
import { readFile, realpath, rm, stat } from "node:fs/promises";
|
|
7
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
8
|
+
import type { HostAdapter, HostResult } from "../engine";
|
|
9
|
+
import {
|
|
10
|
+
allocateNativeState,
|
|
11
|
+
requireNativeTranscriptView,
|
|
12
|
+
} from "../native-transcript-state";
|
|
13
|
+
import {
|
|
14
|
+
failedNativeTranscripts,
|
|
15
|
+
retainEnabledNativeTranscripts,
|
|
16
|
+
} from "../native-transcripts";
|
|
17
|
+
import { fixtureParts } from "../fixtures/preparation";
|
|
18
|
+
import {
|
|
19
|
+
prepareHookMounts,
|
|
20
|
+
releaseRuntimeHooks,
|
|
21
|
+
runtimeHookObservation,
|
|
22
|
+
runtimeHooksEnabled,
|
|
23
|
+
type HookMounts,
|
|
24
|
+
} from "../runtime/hooks";
|
|
25
|
+
import { insideRuntimeRoot as inside } from "../runtime/paths";
|
|
26
|
+
import { runtimeObservations } from "../runtime/state";
|
|
27
|
+
import { isOptionalNonblankString, isRecord } from "../value-guards";
|
|
28
|
+
import { runClaudeTurns, type ClaudeSession } from "./claude-continuation";
|
|
19
29
|
import { summarizeClaudeEvents } from "./claude-events";
|
|
20
|
-
import {
|
|
30
|
+
import { observeClaudeNativeGoal } from "./claude-native-goal";
|
|
31
|
+
import { runClaudeProcess } from "./claude-process";
|
|
21
32
|
import {
|
|
22
33
|
claudeRepositoryInvocationObservation,
|
|
23
34
|
verifyClaudeRepositoryInvocation,
|
|
24
35
|
} from "./claude-repository-invocation";
|
|
25
|
-
import {
|
|
36
|
+
import { prepareClaudeState, type ClaudeHostOptions } from "./claude-state";
|
|
37
|
+
import {
|
|
38
|
+
cleanupNativeState,
|
|
39
|
+
createHostDiagnostics,
|
|
40
|
+
diagnoseHostRequest,
|
|
41
|
+
diagnosticConfiguration,
|
|
42
|
+
diagnosticCredentialEnvironment,
|
|
43
|
+
diagnosticStage,
|
|
44
|
+
readDiagnosticCredentials,
|
|
45
|
+
retainReturnedHostFailure,
|
|
46
|
+
type HostDiagnostics,
|
|
47
|
+
} from "../host-diagnostics";
|
|
48
|
+
import { CLAUDE_AUTH_VARIABLES } from "./claude-credential";
|
|
26
49
|
import { claudeToolCallsObservation } from "./claude-tool-calls";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
50
|
+
import { claudeSkillActivationsObservation } from "./claude-skill-activations";
|
|
51
|
+
import { claudeNativeControls } from "./native-controls";
|
|
52
|
+
export type { ClaudeHostOptions } from "./claude-state";
|
|
29
53
|
|
|
30
|
-
const MAX_EVENT_BYTES = 8 * 1024 * 1024;
|
|
31
|
-
const MAX_STDERR_BYTES = 64 * 1024;
|
|
32
54
|
const DEFAULT_TIMEOUT_MS = 10 * 60_000;
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
toolchainBinDir?: string;
|
|
44
|
-
projectSettings?: boolean;
|
|
45
|
-
timeoutMs?: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function inside(root: string, path: string): boolean {
|
|
49
|
-
const child = relative(root, path);
|
|
50
|
-
return (
|
|
51
|
-
child === "" ||
|
|
52
|
-
(child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function minimalRoots(roots: string[]): string[] {
|
|
57
|
-
return roots.filter(
|
|
58
|
-
(root) => !roots.some((other) => other !== root && inside(other, root)),
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function stop(proc: Bun.Subprocess): void {
|
|
63
|
-
try {
|
|
64
|
-
if (process.platform !== "win32") process.kill(-proc.pid, "SIGKILL");
|
|
65
|
-
else proc.kill("SIGKILL");
|
|
66
|
-
} catch {
|
|
67
|
-
proc.kill("SIGKILL");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function boundedStream(
|
|
72
|
-
stream: ReadableStream<Uint8Array>,
|
|
73
|
-
limit = MAX_EVENT_BYTES,
|
|
74
|
-
): Promise<string> {
|
|
75
|
-
const reader = stream.getReader();
|
|
76
|
-
const chunks: Uint8Array[] = [];
|
|
77
|
-
let size = 0;
|
|
78
|
-
while (true) {
|
|
79
|
-
const { value, done } = await reader.read();
|
|
80
|
-
if (done) break;
|
|
81
|
-
size += value.byteLength;
|
|
82
|
-
if (size > limit)
|
|
83
|
-
throw new Error("Claude process output exceeds its limit");
|
|
84
|
-
chunks.push(value);
|
|
85
|
-
}
|
|
86
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
87
|
-
Buffer.concat(chunks),
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function runProcess(options: {
|
|
92
|
-
argv: string[];
|
|
93
|
-
cwd: string;
|
|
94
|
-
env: Record<string, string>;
|
|
95
|
-
timeoutMs: number;
|
|
96
|
-
signal?: AbortSignal;
|
|
97
|
-
}): Promise<{ code: number; out: string; err: string }> {
|
|
98
|
-
if (options.signal?.aborted) throw new Error("Claude run cancelled");
|
|
99
|
-
const proc = Bun.spawn(options.argv, {
|
|
100
|
-
cwd: options.cwd,
|
|
101
|
-
env: options.env,
|
|
102
|
-
detached: process.platform !== "win32",
|
|
103
|
-
stdin: "ignore",
|
|
104
|
-
stdout: "pipe",
|
|
105
|
-
stderr: "pipe",
|
|
106
|
-
});
|
|
107
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
108
|
-
let cancel: (() => void) | undefined;
|
|
109
|
-
try {
|
|
110
|
-
const completed = Promise.all([
|
|
111
|
-
boundedStream(proc.stdout),
|
|
112
|
-
boundedStream(proc.stderr, MAX_STDERR_BYTES),
|
|
113
|
-
proc.exited,
|
|
114
|
-
]).then(([out, err, code]) => ({ out, err, code }));
|
|
115
|
-
const timeout = new Promise<never>((_resolve, reject) => {
|
|
116
|
-
timer = setTimeout(
|
|
117
|
-
() => reject(new Error("Claude run timed out")),
|
|
118
|
-
options.timeoutMs,
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
const aborted = new Promise<never>((_resolve, reject) => {
|
|
122
|
-
if (!options.signal) return;
|
|
123
|
-
cancel = () => reject(new Error("Claude run cancelled"));
|
|
124
|
-
if (options.signal.aborted) cancel();
|
|
125
|
-
else options.signal.addEventListener("abort", cancel, { once: true });
|
|
126
|
-
});
|
|
127
|
-
return await Promise.race([completed, timeout, aborted]);
|
|
128
|
-
} catch (error) {
|
|
129
|
-
stop(proc);
|
|
130
|
-
await proc.exited;
|
|
131
|
-
throw error;
|
|
132
|
-
} finally {
|
|
133
|
-
if (timer) clearTimeout(timer);
|
|
134
|
-
if (cancel) options.signal?.removeEventListener("abort", cancel);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
56
|
+
type Request = Parameters<HostAdapter["run"]>[0];
|
|
57
|
+
type PluginDeclaration = NonNullable<Request["claudePluginDirs"]>;
|
|
58
|
+
type PluginInvocation = NonNullable<Request["explicitSkillInvocation"]> & {
|
|
59
|
+
scope?: "plugin";
|
|
60
|
+
};
|
|
61
|
+
type RepositoryInvocation = Awaited<
|
|
62
|
+
ReturnType<typeof verifyClaudeRepositoryInvocation>
|
|
63
|
+
>;
|
|
64
|
+
type Execution = Awaited<ReturnType<typeof runClaudeTurns>>;
|
|
137
65
|
|
|
138
66
|
async function pluginDirectories(
|
|
139
67
|
workspace: string,
|
|
140
|
-
declaration:
|
|
141
|
-
Parameters<HostAdapter["run"]>[0]["claudePluginDirs"]
|
|
142
|
-
>,
|
|
68
|
+
declaration: PluginDeclaration,
|
|
143
69
|
): Promise<string[]> {
|
|
144
70
|
const root = await realpath(workspace);
|
|
71
|
+
requireUniquePluginDeclaration(declaration);
|
|
72
|
+
const paths = new Set(declaration.artifactPaths);
|
|
73
|
+
const directories: string[] = [];
|
|
74
|
+
for (const relativeRoot of declaration.artifactRoots)
|
|
75
|
+
directories.push(await declaredPluginDirectory(root, relativeRoot, paths));
|
|
76
|
+
requireDisjointPluginDirectories(directories);
|
|
77
|
+
for (const artifactPath of paths)
|
|
78
|
+
await verifyPluginArtifact(root, artifactPath, declaration, directories);
|
|
79
|
+
return directories;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requireUniquePluginDeclaration(declaration: PluginDeclaration): void {
|
|
145
83
|
if (
|
|
146
84
|
new Set(declaration.artifactRoots).size !==
|
|
147
85
|
declaration.artifactRoots.length ||
|
|
148
86
|
new Set(declaration.artifactPaths).size !== declaration.artifactPaths.length
|
|
149
87
|
)
|
|
150
88
|
throw new Error("Claude plugin declaration contains duplicates");
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function declaredPluginDirectory(
|
|
92
|
+
root: string,
|
|
93
|
+
relativeRoot: string,
|
|
94
|
+
paths: Set<string>,
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
fixtureParts(relativeRoot);
|
|
97
|
+
if (!paths.has(`${relativeRoot}/.claude-plugin/plugin.json`))
|
|
98
|
+
throw new Error("Claude plugin manifest is undeclared");
|
|
99
|
+
const actual = await realpath(join(root, relativeRoot));
|
|
100
|
+
if (!inside(root, actual))
|
|
101
|
+
throw new Error("Claude plugin directory escapes workspace");
|
|
102
|
+
if (!(await stat(join(actual, ".claude-plugin", "plugin.json"))).isFile())
|
|
103
|
+
throw new Error("Claude plugin manifest is missing");
|
|
104
|
+
return actual;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function requireDisjointPluginDirectories(directories: string[]): void {
|
|
165
108
|
if (new Set(directories).size !== directories.length)
|
|
166
109
|
throw new Error("Claude plugin directories overlap");
|
|
167
110
|
if (
|
|
@@ -170,94 +113,154 @@ async function pluginDirectories(
|
|
|
170
113
|
)
|
|
171
114
|
)
|
|
172
115
|
throw new Error("Claude plugin directories overlap");
|
|
173
|
-
for (const artifactPath of paths) {
|
|
174
|
-
fixtureParts(artifactPath);
|
|
175
|
-
const index = declaration.artifactRoots.findIndex((item) =>
|
|
176
|
-
artifactPath.startsWith(`${item}/`),
|
|
177
|
-
);
|
|
178
|
-
if (index < 0)
|
|
179
|
-
throw new Error("Claude plugin artifact is outside a declared directory");
|
|
180
|
-
if (!inside(directories[index]!, await realpath(join(root, artifactPath))))
|
|
181
|
-
throw new Error("Claude plugin artifact escapes workspace");
|
|
182
|
-
}
|
|
183
|
-
return directories;
|
|
184
116
|
}
|
|
185
117
|
|
|
186
|
-
async function
|
|
187
|
-
|
|
118
|
+
async function verifyPluginArtifact(
|
|
119
|
+
root: string,
|
|
120
|
+
artifactPath: string,
|
|
121
|
+
declaration: PluginDeclaration,
|
|
188
122
|
directories: string[],
|
|
189
123
|
): Promise<void> {
|
|
124
|
+
fixtureParts(artifactPath);
|
|
125
|
+
const index = declaration.artifactRoots.findIndex((item) =>
|
|
126
|
+
artifactPath.startsWith(`${item}/`),
|
|
127
|
+
);
|
|
128
|
+
const directory = directories.at(index);
|
|
129
|
+
if (index < 0 || directory === undefined)
|
|
130
|
+
throw new Error("Claude plugin artifact is outside a declared directory");
|
|
131
|
+
if (!inside(directory, await realpath(join(root, artifactPath))))
|
|
132
|
+
throw new Error("Claude plugin artifact escapes workspace");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pluginSelection(request: Request): PluginInvocation | null {
|
|
190
136
|
const selected = request.explicitSkillInvocation;
|
|
191
|
-
if (!selected) return;
|
|
192
|
-
|
|
193
|
-
|
|
137
|
+
if (!selected || selected.scope === "repository") return null;
|
|
138
|
+
return selected;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function requirePluginInvocation(
|
|
142
|
+
selected: PluginInvocation,
|
|
143
|
+
prompt: string,
|
|
144
|
+
): void {
|
|
145
|
+
const { token, pluginName, skillName } = selected;
|
|
194
146
|
if (
|
|
195
147
|
token !== `/${pluginName}:${skillName}` ||
|
|
196
|
-
|
|
148
|
+
prompt.split(token).length - 1 !== 1 ||
|
|
197
149
|
!/^[a-z][a-z0-9-]*$/.test(pluginName) ||
|
|
198
150
|
!/^[A-Za-z0-9._-]+$/.test(skillName)
|
|
199
151
|
)
|
|
200
152
|
throw new Error("invalid Claude explicit skill invocation");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function skillPathDeclared(
|
|
156
|
+
request: Request,
|
|
157
|
+
index: number,
|
|
158
|
+
skillName: string,
|
|
159
|
+
): boolean {
|
|
160
|
+
const relativeRoot = request.claudePluginDirs?.artifactRoots[index];
|
|
161
|
+
return !!request.claudePluginDirs?.artifactPaths.includes(
|
|
162
|
+
`${relativeRoot}/skills/${skillName}/SKILL.md`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function verifyInvocation(
|
|
167
|
+
request: Request,
|
|
168
|
+
directories: string[],
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const selected = pluginSelection(request);
|
|
171
|
+
if (!selected) return;
|
|
172
|
+
requirePluginInvocation(selected, request.prompt);
|
|
201
173
|
for (const [index, root] of directories.entries()) {
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
!request.claudePluginDirs?.artifactPaths.includes(
|
|
205
|
-
`${relativeRoot}/skills/${skillName}/SKILL.md`,
|
|
206
|
-
)
|
|
207
|
-
)
|
|
208
|
-
continue;
|
|
209
|
-
let manifest: unknown;
|
|
210
|
-
try {
|
|
211
|
-
manifest = JSON.parse(
|
|
212
|
-
await readFile(join(root, ".claude-plugin", "plugin.json"), "utf8"),
|
|
213
|
-
);
|
|
214
|
-
} catch {
|
|
215
|
-
throw new Error("invalid Claude plugin manifest");
|
|
216
|
-
}
|
|
217
|
-
if (
|
|
218
|
-
manifest &&
|
|
219
|
-
typeof manifest === "object" &&
|
|
220
|
-
"name" in manifest &&
|
|
221
|
-
manifest.name === pluginName &&
|
|
222
|
-
(
|
|
223
|
-
await stat(join(root, "skills", skillName, "SKILL.md")).catch(
|
|
224
|
-
() => null,
|
|
225
|
-
)
|
|
226
|
-
)?.isFile()
|
|
227
|
-
)
|
|
228
|
-
return;
|
|
174
|
+
if (!skillPathDeclared(request, index, selected.skillName)) continue;
|
|
175
|
+
if (await mountedPluginSkill(root, selected)) return;
|
|
229
176
|
}
|
|
230
177
|
throw new Error("invoked Claude skill is absent from the package");
|
|
231
178
|
}
|
|
232
179
|
|
|
180
|
+
async function mountedPluginSkill(
|
|
181
|
+
root: string,
|
|
182
|
+
selected: PluginInvocation,
|
|
183
|
+
): Promise<boolean> {
|
|
184
|
+
let manifest: unknown;
|
|
185
|
+
try {
|
|
186
|
+
manifest = JSON.parse(
|
|
187
|
+
await readFile(join(root, ".claude-plugin", "plugin.json"), "utf8"),
|
|
188
|
+
);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new Error("invalid Claude plugin manifest", { cause: error });
|
|
191
|
+
}
|
|
192
|
+
if (!isRecord(manifest) || manifest.name !== selected.pluginName)
|
|
193
|
+
return false;
|
|
194
|
+
const skill = await stat(
|
|
195
|
+
join(root, "skills", selected.skillName, "SKILL.md"),
|
|
196
|
+
).catch(() => null);
|
|
197
|
+
return !!skill?.isFile();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validClaudeOptions(
|
|
201
|
+
options: ClaudeHostOptions,
|
|
202
|
+
timeoutMs: number,
|
|
203
|
+
): boolean {
|
|
204
|
+
return (
|
|
205
|
+
availableClaudeSandbox() &&
|
|
206
|
+
requiredClaudePaths(options) &&
|
|
207
|
+
optionalClaudePaths(options) &&
|
|
208
|
+
!!options.model &&
|
|
209
|
+
validClaudeEffortTimeout(options.effort, timeoutMs)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
function availableClaudeSandbox(): boolean {
|
|
213
|
+
if (process.platform === "darwin") return existsSync("/usr/bin/sandbox-exec");
|
|
214
|
+
return (
|
|
215
|
+
process.platform === "linux" &&
|
|
216
|
+
existsSync("/usr/bin/bwrap") &&
|
|
217
|
+
existsSync("/usr/bin/socat")
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
function requiredClaudePaths(options: ClaudeHostOptions): boolean {
|
|
221
|
+
return [
|
|
222
|
+
options.binary,
|
|
223
|
+
options.projectRoot,
|
|
224
|
+
options.resultsRoot,
|
|
225
|
+
...options.additionalProtectedRoots,
|
|
226
|
+
].every(isAbsolute);
|
|
227
|
+
}
|
|
228
|
+
function optionalClaudePaths(options: ClaudeHostOptions): boolean {
|
|
229
|
+
return [
|
|
230
|
+
options.credentialFile,
|
|
231
|
+
options.uvCacheDir,
|
|
232
|
+
options.toolchainBinDir,
|
|
233
|
+
].every((path) => path === undefined || isAbsolute(path));
|
|
234
|
+
}
|
|
235
|
+
function validClaudeEffortTimeout(effort: string, timeoutMs: number): boolean {
|
|
236
|
+
return (
|
|
237
|
+
!!effort &&
|
|
238
|
+
Number.isSafeInteger(timeoutMs) &&
|
|
239
|
+
timeoutMs >= 1 &&
|
|
240
|
+
timeoutMs <= 30 * 60_000
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
233
244
|
/** Run Claude with explicit plugins and its native tool sandbox. */
|
|
234
245
|
export function createClaudeHost(options: ClaudeHostOptions): HostAdapter {
|
|
235
246
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
236
|
-
if (
|
|
237
|
-
process.platform !== "darwin" ||
|
|
238
|
-
!existsSync("/usr/bin/sandbox-exec") ||
|
|
239
|
-
!isAbsolute(options.binary) ||
|
|
240
|
-
!isAbsolute(options.projectRoot) ||
|
|
241
|
-
!isAbsolute(options.resultsRoot) ||
|
|
242
|
-
options.additionalProtectedRoots.some((root) => !isAbsolute(root)) ||
|
|
243
|
-
(options.credentialFile !== undefined &&
|
|
244
|
-
!isAbsolute(options.credentialFile)) ||
|
|
245
|
-
(options.uvCacheDir !== undefined && !isAbsolute(options.uvCacheDir)) ||
|
|
246
|
-
(options.toolchainBinDir !== undefined &&
|
|
247
|
-
!isAbsolute(options.toolchainBinDir)) ||
|
|
248
|
-
!options.model ||
|
|
249
|
-
!options.effort ||
|
|
250
|
-
!Number.isSafeInteger(timeoutMs) ||
|
|
251
|
-
timeoutMs < 1 ||
|
|
252
|
-
timeoutMs > 30 * 60_000
|
|
253
|
-
)
|
|
247
|
+
if (!validClaudeOptions(options, timeoutMs))
|
|
254
248
|
throw new Error("invalid Claude host configuration");
|
|
255
249
|
return {
|
|
256
250
|
id: "sevro.host.claude",
|
|
257
251
|
model: options.model,
|
|
258
252
|
effort: options.effort,
|
|
253
|
+
...(options.retainHostDiagnostics
|
|
254
|
+
? {
|
|
255
|
+
configuration: diagnosticConfiguration(options.retainHostDiagnostics),
|
|
256
|
+
}
|
|
257
|
+
: {}),
|
|
259
258
|
hostCapabilities: [
|
|
260
259
|
"sevro.host.native-goal",
|
|
260
|
+
"sevro.host.runtime",
|
|
261
|
+
"sevro.host.native-transcripts",
|
|
262
|
+
"sevro.host.hooks",
|
|
263
|
+
"sevro.host.skill-activations",
|
|
261
264
|
"sevro.claude.plugin-dirs",
|
|
262
265
|
"sevro.claude.explicit-invocation",
|
|
263
266
|
...(options.projectSettings
|
|
@@ -265,256 +268,363 @@ export function createClaudeHost(options: ClaudeHostOptions): HostAdapter {
|
|
|
265
268
|
: []),
|
|
266
269
|
"sevro.claude.tool-calls",
|
|
267
270
|
"sevro.host.native-controls",
|
|
268
|
-
"sevro.claude.nested-skills",
|
|
269
271
|
"sevro.host.continuation",
|
|
270
272
|
"sevro.claude.continuation",
|
|
271
273
|
],
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
request.followUpPrompt !== undefined &&
|
|
275
|
-
(typeof request.followUpPrompt !== "string" ||
|
|
276
|
-
!request.followUpPrompt.trim())
|
|
277
|
-
)
|
|
278
|
-
throw new Error("Claude follow-up prompt must be nonempty");
|
|
279
|
-
if (request.instrumentation?.length || request.condition !== "passive")
|
|
280
|
-
throw new Error("Claude enforcement instrumentation is unavailable");
|
|
281
|
-
if (
|
|
282
|
-
request.fixtureBinDir !== undefined &&
|
|
283
|
-
request.fixtureBinDir !== join(request.workspace, ".git", "fixture-bin")
|
|
284
|
-
)
|
|
285
|
-
throw new Error("fixture binary path is outside the workspace");
|
|
286
|
-
const pluginDirs = request.claudePluginDirs
|
|
287
|
-
? await pluginDirectories(request.workspace, request.claudePluginDirs)
|
|
288
|
-
: [];
|
|
289
|
-
await verifyInvocation(request, pluginDirs);
|
|
290
|
-
if (
|
|
291
|
-
request.explicitSkillInvocation?.scope === "repository" &&
|
|
292
|
-
!options.projectSettings
|
|
293
|
-
)
|
|
294
|
-
throw new Error(
|
|
295
|
-
"Claude repository invocation requires project setting sources",
|
|
296
|
-
);
|
|
297
|
-
const repositoryInvocation =
|
|
298
|
-
request.explicitSkillInvocation?.scope === "repository"
|
|
299
|
-
? await verifyClaudeRepositoryInvocation(request)
|
|
300
|
-
: null;
|
|
301
|
-
const stateRoot = await mkdtemp(join(tmpdir(), "sevro-claude-state-"));
|
|
302
|
-
try {
|
|
303
|
-
await mkdir(join(stateRoot, "private"), { mode: 0o700 });
|
|
304
|
-
const privateRoot = await realpath(join(stateRoot, "private"));
|
|
305
|
-
const home = join(stateRoot, "home");
|
|
306
|
-
const temp = join(stateRoot, "tmp");
|
|
307
|
-
await Promise.all(
|
|
308
|
-
[home, temp].map((path) => mkdir(path, { mode: 0o700 })),
|
|
309
|
-
);
|
|
310
|
-
const authentication = await stageClaudeAuthentication(
|
|
311
|
-
join(privateRoot, "config"),
|
|
312
|
-
options.credentialFile,
|
|
313
|
-
);
|
|
314
|
-
const credential = authentication.credentialFile;
|
|
315
|
-
const protectedRoots = minimalRoots(
|
|
316
|
-
await evaluationProtectedRoots({
|
|
317
|
-
workspace: request.workspace,
|
|
318
|
-
projectRoot: options.projectRoot,
|
|
319
|
-
resultsRoot: options.resultsRoot,
|
|
320
|
-
additionalRoots: options.additionalProtectedRoots,
|
|
321
|
-
}),
|
|
322
|
-
);
|
|
323
|
-
if (protectedRoots.some((root) => inside(root, credential)))
|
|
324
|
-
throw new Error("Claude private state overlaps a protected root");
|
|
325
|
-
const binaryRoot = await realpath(options.binary);
|
|
326
|
-
if (protectedRoots.some((root) => inside(root, binaryRoot)))
|
|
327
|
-
throw new Error("Claude executable resides inside a protected root");
|
|
328
|
-
const toolchainBinDir = options.toolchainBinDir
|
|
329
|
-
? await realpath(options.toolchainBinDir)
|
|
330
|
-
: null;
|
|
331
|
-
if (
|
|
332
|
-
toolchainBinDir &&
|
|
333
|
-
(protectedRoots.some((root) => inside(root, toolchainBinDir)) ||
|
|
334
|
-
!(await stat(toolchainBinDir)).isDirectory())
|
|
335
|
-
)
|
|
336
|
-
throw new Error(
|
|
337
|
-
"Claude toolchain directory is unavailable or protected",
|
|
338
|
-
);
|
|
339
|
-
let runtimeRoot: string | null = null;
|
|
340
|
-
if (options.uvCacheDir) {
|
|
341
|
-
const cacheRoot = await realpath(options.uvCacheDir);
|
|
342
|
-
if (
|
|
343
|
-
protectedRoots.some((root) => inside(root, cacheRoot)) ||
|
|
344
|
-
!(await stat(cacheRoot)).isDirectory()
|
|
345
|
-
)
|
|
346
|
-
throw new Error("Claude UV cache is unavailable or protected");
|
|
347
|
-
runtimeRoot = join(request.workspace, ".git", "sevro-runtime");
|
|
348
|
-
await mkdir(runtimeRoot, { recursive: true, mode: 0o700 });
|
|
349
|
-
await cp(cacheRoot, join(runtimeRoot, "uv-cache"), {
|
|
350
|
-
recursive: true,
|
|
351
|
-
force: false,
|
|
352
|
-
errorOnExist: true,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
const settingsPath = join(privateRoot, "settings.json");
|
|
356
|
-
await writeFile(
|
|
357
|
-
settingsPath,
|
|
358
|
-
JSON.stringify(
|
|
359
|
-
claudeHostSettings(
|
|
360
|
-
privateRoot,
|
|
361
|
-
credential,
|
|
362
|
-
pluginDirs,
|
|
363
|
-
protectedRoots,
|
|
364
|
-
),
|
|
365
|
-
),
|
|
366
|
-
{ flag: "wx", mode: 0o600 },
|
|
367
|
-
);
|
|
368
|
-
const env: Record<string, string> = {
|
|
369
|
-
...authentication.environment,
|
|
370
|
-
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
|
|
371
|
-
PATH: [
|
|
372
|
-
request.fixtureBinDir,
|
|
373
|
-
toolchainBinDir,
|
|
374
|
-
process.env.PATH ?? "/usr/bin:/bin",
|
|
375
|
-
]
|
|
376
|
-
.filter(Boolean)
|
|
377
|
-
.join(delimiter),
|
|
378
|
-
LANG: process.env.LANG ?? "C",
|
|
379
|
-
HOME: runtimeRoot ? join(runtimeRoot, "host-home") : home,
|
|
380
|
-
TMPDIR: temp,
|
|
381
|
-
CLAUDE_CONFIG_DIR: dirname(credential),
|
|
382
|
-
NO_COLOR: "1",
|
|
383
|
-
...(runtimeRoot
|
|
384
|
-
? {
|
|
385
|
-
UV_CACHE_DIR: join(runtimeRoot, "uv-cache"),
|
|
386
|
-
UV_PROJECT_ENVIRONMENT: join(
|
|
387
|
-
runtimeRoot,
|
|
388
|
-
"project-environment",
|
|
389
|
-
),
|
|
390
|
-
UV_OFFLINE: "1",
|
|
391
|
-
PYTHONDONTWRITEBYTECODE: "1",
|
|
392
|
-
}
|
|
393
|
-
: {}),
|
|
394
|
-
};
|
|
395
|
-
if (runtimeRoot) await mkdir(env.HOME!, { mode: 0o700 });
|
|
396
|
-
const runTurn = (prompt: string, session?: ClaudeSession) =>
|
|
397
|
-
runProcess({
|
|
398
|
-
argv: [
|
|
399
|
-
options.binary,
|
|
400
|
-
"-p",
|
|
401
|
-
prompt,
|
|
402
|
-
"--output-format",
|
|
403
|
-
"stream-json",
|
|
404
|
-
"--verbose",
|
|
405
|
-
"--model",
|
|
406
|
-
options.model,
|
|
407
|
-
"--effort",
|
|
408
|
-
options.effort,
|
|
409
|
-
"--permission-mode",
|
|
410
|
-
"dontAsk",
|
|
411
|
-
"--tools",
|
|
412
|
-
"Bash,Read,Edit,Skill,Agent",
|
|
413
|
-
"--setting-sources",
|
|
414
|
-
options.projectSettings ? "project" : "",
|
|
415
|
-
"--settings",
|
|
416
|
-
settingsPath,
|
|
417
|
-
"--strict-mcp-config",
|
|
418
|
-
"--mcp-config",
|
|
419
|
-
'{"mcpServers":{}}',
|
|
420
|
-
"--no-chrome",
|
|
421
|
-
...(session ? [session.option, session.id] : []),
|
|
422
|
-
...pluginDirs.flatMap((path) => ["--plugin-dir", path]),
|
|
423
|
-
],
|
|
424
|
-
cwd: request.workspace,
|
|
425
|
-
env,
|
|
426
|
-
timeoutMs,
|
|
427
|
-
signal: request.signal,
|
|
428
|
-
});
|
|
429
|
-
const execution = await runClaudeTurns({
|
|
430
|
-
prompt: request.prompt,
|
|
431
|
-
followUpPrompt: request.followUpPrompt,
|
|
432
|
-
workspace: request.workspace,
|
|
433
|
-
run: runTurn,
|
|
434
|
-
});
|
|
435
|
-
const summary = summarizeClaudeEvents(execution.out, execution.code);
|
|
436
|
-
const nestedSkills = await claudeNestedSkillsObservation(
|
|
437
|
-
execution.out,
|
|
438
|
-
dirname(credential),
|
|
439
|
-
request.workspace,
|
|
440
|
-
);
|
|
441
|
-
const toolCalls = claudeToolCallsObservation(
|
|
442
|
-
execution.out,
|
|
443
|
-
execution.code,
|
|
444
|
-
);
|
|
445
|
-
const repositoryObservation = repositoryInvocation
|
|
446
|
-
? await claudeRepositoryInvocationObservation({
|
|
447
|
-
invocation: repositoryInvocation,
|
|
448
|
-
stream: execution.initialOut ?? execution.out,
|
|
449
|
-
configRoot: dirname(credential),
|
|
450
|
-
workspace: request.workspace,
|
|
451
|
-
tools: execution.initialOut
|
|
452
|
-
? claudeToolCallsObservation(execution.initialOut, 0)
|
|
453
|
-
: toolCalls,
|
|
454
|
-
})
|
|
455
|
-
: null;
|
|
456
|
-
return {
|
|
457
|
-
finalMessage: summary.finalMessage,
|
|
458
|
-
complete: summary.complete,
|
|
459
|
-
executionFailed: !summary.complete,
|
|
460
|
-
observations: [
|
|
461
|
-
toolCalls,
|
|
462
|
-
await observeClaudeNativeGoal(
|
|
463
|
-
dirname(credential),
|
|
464
|
-
execution.followUpOut ?? execution.out,
|
|
465
|
-
),
|
|
466
|
-
nestedSkills,
|
|
467
|
-
claudeNativeControls(execution.out, execution.code),
|
|
468
|
-
...(repositoryObservation ? [repositoryObservation] : []),
|
|
469
|
-
...(execution.continuation ? [execution.continuation] : []),
|
|
470
|
-
],
|
|
471
|
-
artifacts: [
|
|
472
|
-
{
|
|
473
|
-
id: "sevro.claude.events",
|
|
474
|
-
bytes: Buffer.from(execution.out, "utf8"),
|
|
475
|
-
},
|
|
476
|
-
...(execution.initialOut !== undefined
|
|
477
|
-
? [
|
|
478
|
-
{
|
|
479
|
-
id: "sevro.claude.initial-events",
|
|
480
|
-
bytes: Buffer.from(execution.initialOut, "utf8"),
|
|
481
|
-
},
|
|
482
|
-
]
|
|
483
|
-
: []),
|
|
484
|
-
...(execution.followUpOut !== undefined
|
|
485
|
-
? [
|
|
486
|
-
{
|
|
487
|
-
id: "sevro.claude.follow-up-events",
|
|
488
|
-
bytes: Buffer.from(execution.followUpOut, "utf8"),
|
|
489
|
-
},
|
|
490
|
-
]
|
|
491
|
-
: []),
|
|
492
|
-
...(execution.err
|
|
493
|
-
? [
|
|
494
|
-
{
|
|
495
|
-
id: "sevro.claude.stderr",
|
|
496
|
-
bytes: Buffer.from(execution.err, "utf8"),
|
|
497
|
-
},
|
|
498
|
-
]
|
|
499
|
-
: []),
|
|
500
|
-
],
|
|
501
|
-
actualCondition: "passive" as const,
|
|
502
|
-
inputTokens:
|
|
503
|
-
execution.sessionResultsBound === false
|
|
504
|
-
? null
|
|
505
|
-
: summary.inputTokens,
|
|
506
|
-
outputTokens:
|
|
507
|
-
execution.sessionResultsBound === false
|
|
508
|
-
? null
|
|
509
|
-
: summary.outputTokens,
|
|
510
|
-
costUsd:
|
|
511
|
-
execution.sessionResultsBound === false ? null : summary.costUsd,
|
|
512
|
-
usageComplete:
|
|
513
|
-
execution.sessionResultsBound !== false && summary.usageComplete,
|
|
514
|
-
};
|
|
515
|
-
} finally {
|
|
516
|
-
await rm(stateRoot, { recursive: true, force: true });
|
|
517
|
-
}
|
|
274
|
+
run(request) {
|
|
275
|
+
return runClaudeRequest(options, timeoutMs, request);
|
|
518
276
|
},
|
|
519
277
|
};
|
|
520
278
|
}
|
|
279
|
+
|
|
280
|
+
function requireClaudeFollowUp(request: Request): void {
|
|
281
|
+
if (!isOptionalNonblankString(request.followUpPrompt))
|
|
282
|
+
throw new Error("Claude follow-up prompt must be nonempty");
|
|
283
|
+
}
|
|
284
|
+
function requirePassiveClaudeRequest(request: Request): void {
|
|
285
|
+
if (request.instrumentation?.length || request.condition !== "passive")
|
|
286
|
+
throw new Error("Claude enforcement instrumentation is unavailable");
|
|
287
|
+
requireClaudeFixtureBin(request);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function requireClaudeFixtureBin(request: Request): void {
|
|
291
|
+
if (
|
|
292
|
+
request.fixtureBinDir !== undefined &&
|
|
293
|
+
request.fixtureBinDir !== join(request.workspace, ".git", "fixture-bin")
|
|
294
|
+
)
|
|
295
|
+
throw new Error("fixture binary path is outside the workspace");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function repositoryMount(
|
|
299
|
+
request: Request,
|
|
300
|
+
options: ClaudeHostOptions,
|
|
301
|
+
): Promise<RepositoryInvocation | null> {
|
|
302
|
+
if (request.explicitSkillInvocation?.scope !== "repository") return null;
|
|
303
|
+
if (!options.projectSettings)
|
|
304
|
+
throw new Error(
|
|
305
|
+
"Claude repository invocation requires project setting sources",
|
|
306
|
+
);
|
|
307
|
+
return verifyClaudeRepositoryInvocation(request);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function runClaudeRequest(
|
|
311
|
+
options: ClaudeHostOptions,
|
|
312
|
+
timeoutMs: number,
|
|
313
|
+
request: Request,
|
|
314
|
+
): Promise<HostResult> {
|
|
315
|
+
const diagnostics = createHostDiagnostics(
|
|
316
|
+
"claude",
|
|
317
|
+
options.retainHostDiagnostics,
|
|
318
|
+
request,
|
|
319
|
+
);
|
|
320
|
+
return diagnoseHostRequest(diagnostics, async () => {
|
|
321
|
+
diagnosticStage(diagnostics, "request-validation");
|
|
322
|
+
await readDiagnosticCredentials(diagnostics, options.credentialFile);
|
|
323
|
+
return executeClaudeRequest(options, timeoutMs, request, diagnostics);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function executeClaudeRequest(
|
|
328
|
+
options: ClaudeHostOptions,
|
|
329
|
+
timeoutMs: number,
|
|
330
|
+
request: Request,
|
|
331
|
+
diagnostics?: HostDiagnostics,
|
|
332
|
+
): Promise<HostResult> {
|
|
333
|
+
const { pluginDirs, repositoryInvocation } = await validateClaudeRequest(
|
|
334
|
+
options,
|
|
335
|
+
request,
|
|
336
|
+
);
|
|
337
|
+
diagnosticStage(diagnostics, "state-allocation");
|
|
338
|
+
const stateRoot = await allocateNativeState(
|
|
339
|
+
"claude-",
|
|
340
|
+
request.runtimePolicy ? request.workspace : undefined,
|
|
341
|
+
);
|
|
342
|
+
let hooks: HookMounts | undefined;
|
|
343
|
+
try {
|
|
344
|
+
diagnosticStage(diagnostics, "state-preparation");
|
|
345
|
+
const state = await prepareClaudeState(
|
|
346
|
+
stateRoot,
|
|
347
|
+
options,
|
|
348
|
+
request,
|
|
349
|
+
pluginDirs,
|
|
350
|
+
);
|
|
351
|
+
await readDiagnosticCredentials(diagnostics, state.credential);
|
|
352
|
+
diagnosticCredentialEnvironment(
|
|
353
|
+
diagnostics,
|
|
354
|
+
state.env,
|
|
355
|
+
CLAUDE_AUTH_VARIABLES,
|
|
356
|
+
);
|
|
357
|
+
hooks = state.hooks;
|
|
358
|
+
const run = claudeProcessRunner(
|
|
359
|
+
options,
|
|
360
|
+
timeoutMs,
|
|
361
|
+
request,
|
|
362
|
+
state,
|
|
363
|
+
diagnostics,
|
|
364
|
+
);
|
|
365
|
+
const execution = await runClaudeTurns({
|
|
366
|
+
prompt: request.prompt,
|
|
367
|
+
followUpPrompt: request.followUpPrompt,
|
|
368
|
+
workspace: request.workspace,
|
|
369
|
+
run,
|
|
370
|
+
});
|
|
371
|
+
const result = await claudeResult(
|
|
372
|
+
execution,
|
|
373
|
+
claudeObservationRoot(stateRoot, state.credential, request),
|
|
374
|
+
request,
|
|
375
|
+
repositoryInvocation,
|
|
376
|
+
);
|
|
377
|
+
await retainReturnedHostFailure(
|
|
378
|
+
diagnostics,
|
|
379
|
+
result,
|
|
380
|
+
"Claude host execution did not complete",
|
|
381
|
+
);
|
|
382
|
+
await addHookObservation(result, state.hooks);
|
|
383
|
+
await retainEnabledNativeTranscripts(
|
|
384
|
+
result,
|
|
385
|
+
request.runtimePolicy,
|
|
386
|
+
join(stateRoot, "native-home", "projects"),
|
|
387
|
+
"claude",
|
|
388
|
+
request.runtimeRole,
|
|
389
|
+
);
|
|
390
|
+
return result;
|
|
391
|
+
} catch (cause) {
|
|
392
|
+
return await failedNativeTranscripts(
|
|
393
|
+
cause,
|
|
394
|
+
request.runtimePolicy,
|
|
395
|
+
join(stateRoot, "native-home", "projects"),
|
|
396
|
+
"claude",
|
|
397
|
+
request.runtimeRole,
|
|
398
|
+
diagnostics,
|
|
399
|
+
);
|
|
400
|
+
} finally {
|
|
401
|
+
await cleanupNativeState(diagnostics, async () => {
|
|
402
|
+
await releaseRuntimeHooks(hooks);
|
|
403
|
+
await rm(stateRoot, { recursive: true, force: true });
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function claudeProcessRunner(
|
|
409
|
+
options: ClaudeHostOptions,
|
|
410
|
+
timeoutMs: number,
|
|
411
|
+
request: Request,
|
|
412
|
+
state: Awaited<ReturnType<typeof prepareClaudeState>>,
|
|
413
|
+
diagnostics: HostDiagnostics | undefined,
|
|
414
|
+
) {
|
|
415
|
+
return (prompt: string, session?: ClaudeSession) => {
|
|
416
|
+
diagnosticStage(diagnostics, "claude-execution");
|
|
417
|
+
return runClaudeProcess({
|
|
418
|
+
diagnostics,
|
|
419
|
+
argv: claudeTurnArgs(
|
|
420
|
+
options,
|
|
421
|
+
prompt,
|
|
422
|
+
session,
|
|
423
|
+
state.settingsPath,
|
|
424
|
+
state.pluginDirs,
|
|
425
|
+
),
|
|
426
|
+
cwd: request.workspace,
|
|
427
|
+
env: state.env,
|
|
428
|
+
timeoutMs,
|
|
429
|
+
signal: request.signal,
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function validateClaudeRequest(
|
|
435
|
+
options: ClaudeHostOptions,
|
|
436
|
+
request: Request,
|
|
437
|
+
) {
|
|
438
|
+
requireClaudeFollowUp(request);
|
|
439
|
+
await requireNativeTranscriptView(request.candidateTranscriptRoot);
|
|
440
|
+
await requireCandidateRuntime(
|
|
441
|
+
request.candidateRuntimeRoot,
|
|
442
|
+
request.runtimeRole,
|
|
443
|
+
);
|
|
444
|
+
await requireCandidateArtifactRoots(request.candidateArtifactRoots);
|
|
445
|
+
requirePassiveClaudeRequest(request);
|
|
446
|
+
const pluginDirs = request.claudePluginDirs
|
|
447
|
+
? await pluginDirectories(request.workspace, request.claudePluginDirs)
|
|
448
|
+
: [];
|
|
449
|
+
await verifyInvocation(request, pluginDirs);
|
|
450
|
+
requireIsolatedHookSources(request, options);
|
|
451
|
+
const repositoryInvocation = await repositoryMount(request, options);
|
|
452
|
+
return { pluginDirs, repositoryInvocation };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function requireIsolatedHookSources(
|
|
456
|
+
request: Request,
|
|
457
|
+
options: ClaudeHostOptions,
|
|
458
|
+
): void {
|
|
459
|
+
if (runtimeHooksEnabled(request.runtimePolicy) && options.projectSettings)
|
|
460
|
+
throw new Error("native goals require isolated hook sources");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function addHookObservation(
|
|
464
|
+
result: HostResult,
|
|
465
|
+
hooks: Awaited<ReturnType<typeof prepareHookMounts>> | undefined,
|
|
466
|
+
): Promise<void> {
|
|
467
|
+
if (!hooks) return;
|
|
468
|
+
result.observations?.push(await runtimeHookObservation(hooks));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function claudeTurnArgs(
|
|
472
|
+
options: ClaudeHostOptions,
|
|
473
|
+
prompt: string,
|
|
474
|
+
session: ClaudeSession | undefined,
|
|
475
|
+
settingsPath: string,
|
|
476
|
+
pluginDirs: string[],
|
|
477
|
+
): string[] {
|
|
478
|
+
return [
|
|
479
|
+
options.binary,
|
|
480
|
+
"-p",
|
|
481
|
+
prompt,
|
|
482
|
+
"--output-format",
|
|
483
|
+
"stream-json",
|
|
484
|
+
"--verbose",
|
|
485
|
+
"--model",
|
|
486
|
+
options.model,
|
|
487
|
+
"--effort",
|
|
488
|
+
options.effort,
|
|
489
|
+
"--permission-mode",
|
|
490
|
+
"dontAsk",
|
|
491
|
+
"--tools",
|
|
492
|
+
"Bash,Read,Edit,Skill,Agent",
|
|
493
|
+
"--setting-sources",
|
|
494
|
+
options.projectSettings ? "project" : "",
|
|
495
|
+
"--settings",
|
|
496
|
+
settingsPath,
|
|
497
|
+
"--strict-mcp-config",
|
|
498
|
+
"--mcp-config",
|
|
499
|
+
'{"mcpServers":{}}',
|
|
500
|
+
"--no-chrome",
|
|
501
|
+
...(session ? [session.option, session.id] : []),
|
|
502
|
+
...pluginDirs.flatMap((path) => ["--plugin-dir", path]),
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function claudeObservationRoot(
|
|
507
|
+
stateRoot: string,
|
|
508
|
+
credential: string,
|
|
509
|
+
request: Request,
|
|
510
|
+
): string {
|
|
511
|
+
return request.runtimePolicy?.nativeTranscripts === true
|
|
512
|
+
? join(stateRoot, "native-home")
|
|
513
|
+
: dirname(credential);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function repositoryObservation(
|
|
517
|
+
execution: Execution,
|
|
518
|
+
configRoot: string,
|
|
519
|
+
request: Request,
|
|
520
|
+
invocation: RepositoryInvocation | null,
|
|
521
|
+
tools: ReturnType<typeof claudeToolCallsObservation>,
|
|
522
|
+
) {
|
|
523
|
+
if (!invocation) return null;
|
|
524
|
+
return claudeRepositoryInvocationObservation({
|
|
525
|
+
invocation,
|
|
526
|
+
stream: execution.initialOut ?? execution.out,
|
|
527
|
+
configRoot: configRoot,
|
|
528
|
+
workspace: request.workspace,
|
|
529
|
+
tools: execution.initialOut
|
|
530
|
+
? claudeToolCallsObservation(execution.initialOut, 0)
|
|
531
|
+
: tools,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function claudeObservations(
|
|
536
|
+
execution: Execution,
|
|
537
|
+
configRoot: string,
|
|
538
|
+
request: Request,
|
|
539
|
+
invocation: RepositoryInvocation | null,
|
|
540
|
+
) {
|
|
541
|
+
const tools = claudeToolCallsObservation(execution.out, execution.code);
|
|
542
|
+
const repository = await repositoryObservation(
|
|
543
|
+
execution,
|
|
544
|
+
configRoot,
|
|
545
|
+
request,
|
|
546
|
+
invocation,
|
|
547
|
+
tools,
|
|
548
|
+
);
|
|
549
|
+
return [
|
|
550
|
+
tools,
|
|
551
|
+
...runtimeObservations(request.runtimePolicy, request.runtimeRole),
|
|
552
|
+
await observeClaudeNativeGoal(
|
|
553
|
+
configRoot,
|
|
554
|
+
execution.followUpOut ?? execution.out,
|
|
555
|
+
),
|
|
556
|
+
claudeNativeControls(execution.out, execution.code),
|
|
557
|
+
...(repository ? [repository] : []),
|
|
558
|
+
...(execution.continuation ? [execution.continuation] : []),
|
|
559
|
+
claudeSkillActivationsObservation(execution.out, execution.code),
|
|
560
|
+
];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function claudeArtifacts(execution: Execution) {
|
|
564
|
+
return [
|
|
565
|
+
{ id: "sevro.claude.events", bytes: Buffer.from(execution.out, "utf8") },
|
|
566
|
+
...(execution.initialOut !== undefined
|
|
567
|
+
? [
|
|
568
|
+
{
|
|
569
|
+
id: "sevro.claude.initial-events",
|
|
570
|
+
bytes: Buffer.from(execution.initialOut, "utf8"),
|
|
571
|
+
},
|
|
572
|
+
]
|
|
573
|
+
: []),
|
|
574
|
+
...(execution.followUpOut !== undefined
|
|
575
|
+
? [
|
|
576
|
+
{
|
|
577
|
+
id: "sevro.claude.follow-up-events",
|
|
578
|
+
bytes: Buffer.from(execution.followUpOut, "utf8"),
|
|
579
|
+
},
|
|
580
|
+
]
|
|
581
|
+
: []),
|
|
582
|
+
...(execution.err
|
|
583
|
+
? [
|
|
584
|
+
{
|
|
585
|
+
id: "sevro.claude.stderr",
|
|
586
|
+
bytes: Buffer.from(execution.err, "utf8"),
|
|
587
|
+
},
|
|
588
|
+
]
|
|
589
|
+
: []),
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function measuredClaudeUsage(
|
|
594
|
+
summary: ReturnType<typeof summarizeClaudeEvents>,
|
|
595
|
+
execution: Execution,
|
|
596
|
+
) {
|
|
597
|
+
return {
|
|
598
|
+
inputTokens:
|
|
599
|
+
execution.sessionResultsBound === false ? null : summary.inputTokens,
|
|
600
|
+
outputTokens:
|
|
601
|
+
execution.sessionResultsBound === false ? null : summary.outputTokens,
|
|
602
|
+
costUsd: execution.sessionResultsBound === false ? null : summary.costUsd,
|
|
603
|
+
usageComplete:
|
|
604
|
+
execution.sessionResultsBound !== false && summary.usageComplete,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function claudeResult(
|
|
609
|
+
execution: Execution,
|
|
610
|
+
configRoot: string,
|
|
611
|
+
request: Request,
|
|
612
|
+
invocation: RepositoryInvocation | null,
|
|
613
|
+
): Promise<HostResult> {
|
|
614
|
+
const summary = summarizeClaudeEvents(execution.out, execution.code);
|
|
615
|
+
const observations = await claudeObservations(
|
|
616
|
+
execution,
|
|
617
|
+
configRoot,
|
|
618
|
+
request,
|
|
619
|
+
invocation,
|
|
620
|
+
);
|
|
621
|
+
return {
|
|
622
|
+
finalMessage: summary.finalMessage,
|
|
623
|
+
complete: summary.complete,
|
|
624
|
+
executionFailed: !summary.complete,
|
|
625
|
+
observations,
|
|
626
|
+
artifacts: claudeArtifacts(execution),
|
|
627
|
+
actualCondition: "passive",
|
|
628
|
+
...measuredClaudeUsage(summary, execution),
|
|
629
|
+
};
|
|
630
|
+
}
|