@bjoernrochel/sevro 0.1.0-rc.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +22 -6
- package/README.md +5 -1
- package/docs/README.md +19 -15
- package/docs/architecture.md +138 -17
- package/docs/assets/README.md +5 -4
- package/docs/builtin-graders-v1.md +9 -2
- package/docs/codex-host.md +155 -57
- package/docs/contracts.md +2 -1
- package/docs/creating-cases.md +9 -2
- package/docs/development-cli.md +187 -19
- package/docs/documentation-quality.md +12 -2
- package/docs/evaluation-workflow.md +110 -0
- package/docs/extension-protocol-v1.md +26 -3
- package/docs/getting-started.md +23 -2
- package/docs/guide-evaluation.md +33 -1
- package/docs/identity-v1.md +25 -2
- package/docs/installing.md +11 -11
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +196 -7
- package/docs/reading-results.md +102 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +31 -12
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +122 -4
- package/docs/run-settings-v1.md +147 -0
- package/docs/runtime-v1.md +255 -22
- package/docs/troubleshooting.md +12 -12
- package/docs/typescript-quality.md +89 -26
- 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 +11 -5
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +43 -7
- package/src/advisory.ts +2 -2
- package/src/candidate-artifacts.ts +42 -11
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +73 -50
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +87 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +60 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +198 -0
- package/src/cli/hosts.ts +55 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +154 -0
- package/src/cli/presentation.ts +93 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +128 -0
- package/src/cli/run-options.ts +403 -0
- package/src/cli/run-overrides.ts +69 -0
- package/src/cli/settings.ts +154 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -591
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +9 -5
- package/src/evaluation/advisory.ts +150 -0
- package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
- package/src/evaluation/grading.ts +235 -0
- package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
- package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +77 -87
- package/src/evaluation/trial.ts +800 -0
- package/src/{evaluation-types.ts → evaluation/types.ts} +79 -29
- package/src/evidence/host-results.ts +85 -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 +34 -96
- package/src/extension-session.ts +24 -48
- package/src/{advisory-fixture.ts → fixtures/advisory.ts} +12 -72
- package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +183 -0
- package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
- package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
- package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
- package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
- package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
- package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
- package/src/{workspace-state.ts → fixtures/workspace-state.ts} +5 -5
- package/src/generated/cli-result.d.cts +2 -1
- package/src/generated/extension.d.cts +2 -1
- package/src/generated/report.cjs +1 -1
- package/src/generated/report.d.cts +2 -1
- package/src/generated/run-evidence.cjs +1 -1
- package/src/generated/run-evidence.d.cts +2 -1
- package/src/generated/runtime.cjs +1 -1
- package/src/generated/runtime.d.cts +2 -1
- package/src/git.ts +93 -0
- package/src/graders/git-head.ts +29 -84
- package/src/graders/shell.ts +50 -68
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +331 -0
- package/src/hosts/claude-continuation.ts +57 -40
- package/src/hosts/claude-credential.ts +160 -63
- package/src/hosts/claude-events.ts +34 -18
- package/src/hosts/claude-keychain.ts +78 -21
- package/src/hosts/claude-login-recovery.ts +85 -0
- package/src/hosts/claude-login-refresh.ts +178 -0
- package/src/hosts/claude-login-store.ts +162 -0
- package/src/hosts/claude-login.ts +107 -0
- package/src/hosts/claude-native-goal.ts +15 -12
- package/src/hosts/claude-process.ts +10 -50
- package/src/hosts/claude-refresh-lock.ts +81 -0
- package/src/hosts/claude-repository-invocation.ts +9 -34
- package/src/hosts/claude-settings.ts +46 -9
- package/src/hosts/claude-skill-activations.ts +169 -0
- package/src/hosts/claude-state.ts +28 -15
- package/src/hosts/claude-tool-calls.ts +13 -10
- package/src/hosts/claude.ts +121 -81
- package/src/hosts/codex-app-server.ts +78 -52
- package/src/hosts/codex-configuration.ts +5 -10
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-marketplace.ts +12 -10
- package/src/hosts/codex-native-calls.ts +15 -122
- package/src/hosts/codex-process.ts +10 -183
- package/src/hosts/codex-profile.ts +36 -21
- package/src/hosts/codex-session-files.ts +1 -9
- package/src/hosts/codex-state.ts +134 -18
- package/src/hosts/codex.ts +210 -183
- package/src/hosts/isolation-common.ts +8 -11
- package/src/hosts/isolation-roots.ts +2 -2
- package/src/hosts/linux-sandbox.ts +15 -2
- package/src/hosts/mac-sandbox.ts +87 -18
- package/src/hosts/native-controls.ts +72 -22
- package/src/hosts/native-request.ts +55 -0
- package/src/hosts/process-lifecycle.ts +216 -48
- package/src/hosts/protected-worktrees.ts +1 -1
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +577 -0
- package/src/instrumentation.ts +7 -8
- package/src/native-transcript-state.ts +22 -3
- package/src/native-transcript-view.ts +52 -13
- package/src/native-transcripts.ts +26 -11
- package/src/paths.ts +22 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +27 -7
- package/src/prune.ts +300 -0
- package/src/report.ts +63 -36
- package/src/resolved-case.ts +1 -1
- package/src/results-lock.ts +42 -0
- package/src/results.ts +14 -18
- package/src/{runtime-config.ts → runtime/config.ts} +160 -46
- package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
- package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
- package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
- package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
- package/src/{runtime-hooks.ts → runtime/hooks.ts} +32 -22
- package/src/{runtime-paths.ts → runtime/paths.ts} +5 -19
- package/src/{runtime-query.ts → runtime/query.ts} +5 -5
- package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
- package/src/runtime/shared-cache.ts +130 -0
- package/src/{runtime-state.ts → runtime/state.ts} +28 -11
- package/src/schema-types.ts +73 -161
- package/src/shell.ts +4 -0
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +22 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +0 -1
- package/docs/assets/sevro-wordmark.svg +0 -1
- package/docs/cache-ownership-validation.md +0 -49
- package/docs/claude-continuation-validation.md +0 -47
- package/docs/concurrency-validation.md +0 -86
- package/docs/configuration-root-validation.md +0 -70
- package/docs/engine-slice.md +0 -114
- package/docs/evidence/runtime-startup-30/README.md +0 -185
- package/docs/evidence/runtime-startup-30/after.json +0 -319
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
- package/docs/evidence/runtime-startup-30/before.json +0 -403
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
- package/docs/evidence.md +0 -17
- package/docs/fixture-cleanup-validation.md +0 -73
- package/docs/guide-verification.json +0 -239
- package/src/evaluation-context.ts +0 -259
- package/src/evaluation-fixture.ts +0 -302
- package/src/evaluation-run.ts +0 -200
- package/src/evaluation-trial.ts +0 -1016
- package/src/hosts/claude-nested-skills.ts +0 -272
- package/src/hosts/codex-command-output.ts +0 -510
- package/src/hosts/codex-executor-command.ts +0 -291
- package/src/hosts/codex-skill-reads.ts +0 -783
- /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
|
-
.
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
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
|
|
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,17 +1,18 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { CLAUDE_AUTH_VARIABLES } from "./claude-credential";
|
|
3
|
-
import type { RuntimePolicy } from "../runtime
|
|
4
|
-
import { runtimeHooksEnabled } from "../runtime
|
|
5
|
-
import { requireRuntimeReadRoots, isRuntimeHomeRoot } from "../runtime
|
|
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
7
|
import {
|
|
8
8
|
workspaceStatePath,
|
|
9
9
|
workspaceBoundaryException,
|
|
10
|
-
} from "../workspace-state";
|
|
10
|
+
} from "../fixtures/workspace-state";
|
|
11
11
|
|
|
12
12
|
interface WritableRuntime {
|
|
13
13
|
root: string;
|
|
14
14
|
workspace: string;
|
|
15
|
+
cacheRoots?: string[];
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function absoluteRule(root: string, tool: "Read" | "Edit"): string {
|
|
@@ -53,6 +54,10 @@ export function claudeHostSettings(
|
|
|
53
54
|
throw new Error("Claude private state paths are invalid");
|
|
54
55
|
const readRoots = runtimeReadRoots(runtimePolicy);
|
|
55
56
|
requireRuntimeReadRoots(readRoots, [privateRoot, ...protectedRoots]);
|
|
57
|
+
requireRuntimeReadRoots(runtimeCacheRoots(runtime), [
|
|
58
|
+
privateRoot,
|
|
59
|
+
...protectedRoots,
|
|
60
|
+
]);
|
|
56
61
|
return {
|
|
57
62
|
disableAllHooks: !runtimeHooksEnabled(runtimePolicy),
|
|
58
63
|
sandbox: {
|
|
@@ -95,6 +100,7 @@ function claudeFilesystemPolicy(
|
|
|
95
100
|
workspace: string | undefined,
|
|
96
101
|
) {
|
|
97
102
|
const runtimeWriteRoot = runtime?.root;
|
|
103
|
+
const writableRoots = runtimeWriteRoots(runtime);
|
|
98
104
|
return {
|
|
99
105
|
denyRead: [privateRoot, ...protectedRoots],
|
|
100
106
|
allowRead: [
|
|
@@ -102,13 +108,18 @@ function claudeFilesystemPolicy(
|
|
|
102
108
|
...readRoots,
|
|
103
109
|
...transcriptReadRoots,
|
|
104
110
|
...pluginRoots,
|
|
105
|
-
...
|
|
111
|
+
...writableRoots,
|
|
106
112
|
],
|
|
107
|
-
...(
|
|
113
|
+
...(writableRoots.length ? { allowWrite: writableRoots } : {}),
|
|
108
114
|
denyWrite: [
|
|
109
115
|
privateRoot,
|
|
110
116
|
...pluginRoots,
|
|
111
|
-
...writableProtectedRoots(
|
|
117
|
+
...writableProtectedRoots(
|
|
118
|
+
protectedRoots,
|
|
119
|
+
runtimeWriteRoot,
|
|
120
|
+
workspace,
|
|
121
|
+
runtimeCacheRoots(runtime),
|
|
122
|
+
),
|
|
112
123
|
...readRoots,
|
|
113
124
|
...transcriptReadRoots,
|
|
114
125
|
],
|
|
@@ -139,7 +150,7 @@ function claudePermissions(
|
|
|
139
150
|
? roots.filter((root) => !isRuntimeHomeRoot(root))
|
|
140
151
|
: roots;
|
|
141
152
|
const toolRoots = [...runtimeReadRoots(policy), ...transcriptReadRoots];
|
|
142
|
-
const writableRoots =
|
|
153
|
+
const writableRoots = runtimeWriteRoots(runtime);
|
|
143
154
|
return {
|
|
144
155
|
allow: claudeToolAllowRules(policy, runtime, managedWorkspace),
|
|
145
156
|
...(policy || managedWorkspace
|
|
@@ -153,7 +164,12 @@ function claudePermissions(
|
|
|
153
164
|
}
|
|
154
165
|
: {}),
|
|
155
166
|
deny: [
|
|
156
|
-
...writableProtectedRoots(
|
|
167
|
+
...writableProtectedRoots(
|
|
168
|
+
roots,
|
|
169
|
+
runtimeWriteRoot,
|
|
170
|
+
workspace,
|
|
171
|
+
runtimeCacheRoots(runtime),
|
|
172
|
+
).flatMap((root) =>
|
|
157
173
|
protectedToolRules(
|
|
158
174
|
root,
|
|
159
175
|
readRoots.includes(root) &&
|
|
@@ -189,6 +205,7 @@ function claudeToolAllowRules(
|
|
|
189
205
|
"Agent",
|
|
190
206
|
absoluteRule(runtime.workspace, "Edit"),
|
|
191
207
|
absoluteRule(runtime.root, "Edit"),
|
|
208
|
+
...(runtime.cacheRoots ?? []).map((root) => absoluteRule(root, "Edit")),
|
|
192
209
|
];
|
|
193
210
|
}
|
|
194
211
|
|
|
@@ -197,15 +214,35 @@ function writableProtectedRoots(
|
|
|
197
214
|
roots: string[],
|
|
198
215
|
runtimeWriteRoot: string | undefined,
|
|
199
216
|
workspace?: string,
|
|
217
|
+
cacheRoots: string[] = [],
|
|
200
218
|
): string[] {
|
|
201
219
|
const namespace = nativeStatePath();
|
|
202
220
|
return roots.filter(
|
|
203
221
|
(root) =>
|
|
204
222
|
!workspaceBoundaryException(root, workspace).length &&
|
|
223
|
+
!sharedHomeGrant(root, cacheRoots) &&
|
|
205
224
|
keepNativeWriteDeny(root, namespace, runtimeWriteRoot),
|
|
206
225
|
);
|
|
207
226
|
}
|
|
208
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
|
+
|
|
209
246
|
function keepNativeWriteDeny(
|
|
210
247
|
root: string,
|
|
211
248
|
namespace: string,
|
|
@@ -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,6 @@
|
|
|
1
|
-
import { runtimeSeedSources } from "../runtime
|
|
1
|
+
import { runtimeSeedSources } from "../runtime/config";
|
|
2
|
+
import { minimalRoots } from "../paths";
|
|
3
|
+
import { shellQuote } from "../shell";
|
|
2
4
|
import {
|
|
3
5
|
candidateArtifactEnvironment,
|
|
4
6
|
candidateArtifactReadRoots,
|
|
@@ -17,17 +19,21 @@ import { nativeStateParent } from "../native-transcript-state";
|
|
|
17
19
|
import {
|
|
18
20
|
workspaceBoundaryException,
|
|
19
21
|
workspaceStatePath,
|
|
20
|
-
} from "../workspace-state";
|
|
22
|
+
} from "../fixtures/workspace-state";
|
|
21
23
|
import {
|
|
22
24
|
candidateTranscriptEnvironment,
|
|
23
25
|
candidateTranscriptRoots,
|
|
24
26
|
} from "../native-transcripts";
|
|
25
|
-
import { prepareHookMounts, runtimeHooksEnabled } from "../runtime
|
|
27
|
+
import { prepareHookMounts, runtimeHooksEnabled } from "../runtime/hooks";
|
|
26
28
|
import {
|
|
27
29
|
insideRuntimeRoot as inside,
|
|
28
30
|
runtimeExecutableProtection,
|
|
29
|
-
} from "../runtime
|
|
30
|
-
import { prepareRuntimeState } from "../runtime
|
|
31
|
+
} from "../runtime/paths";
|
|
32
|
+
import { prepareRuntimeState } from "../runtime/state";
|
|
33
|
+
import {
|
|
34
|
+
sharedCacheDenyRoots,
|
|
35
|
+
sharedCacheRoots,
|
|
36
|
+
} from "../runtime/shared-cache";
|
|
31
37
|
import { stageClaudeAuthentication } from "./claude-credential";
|
|
32
38
|
import { claudeHostSettings } from "./claude-settings";
|
|
33
39
|
import { evaluationProtectedRoots } from "./isolation-roots";
|
|
@@ -44,12 +50,7 @@ export interface ClaudeHostOptions {
|
|
|
44
50
|
toolchainBinDir?: string;
|
|
45
51
|
projectSettings?: boolean;
|
|
46
52
|
timeoutMs?: number;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function minimalRoots(roots: string[]): string[] {
|
|
50
|
-
return roots.filter(
|
|
51
|
-
(root) => !roots.some((other) => other !== root && inside(other, root)),
|
|
52
|
-
);
|
|
53
|
+
retainHostDiagnostics?: boolean;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function claudeSeedSources(request: Request): string[] {
|
|
@@ -59,6 +60,7 @@ function claudeSeedSources(request: Request): string[] {
|
|
|
59
60
|
async function createClaudePrivateState(
|
|
60
61
|
stateRoot: string,
|
|
61
62
|
options: ClaudeHostOptions,
|
|
63
|
+
loginLifetimeMs: number,
|
|
62
64
|
) {
|
|
63
65
|
await mkdir(join(stateRoot, "private"), { mode: 0o700 });
|
|
64
66
|
const privateRoot = await realpath(join(stateRoot, "private"));
|
|
@@ -68,6 +70,7 @@ async function createClaudePrivateState(
|
|
|
68
70
|
const authentication = await stageClaudeAuthentication(
|
|
69
71
|
join(privateRoot, "config"),
|
|
70
72
|
options.credentialFile,
|
|
73
|
+
{ binary: options.binary, lifetimeMs: loginLifetimeMs },
|
|
71
74
|
);
|
|
72
75
|
return {
|
|
73
76
|
privateRoot,
|
|
@@ -189,14 +192,16 @@ function claudeRuntimeEnvironment(
|
|
|
189
192
|
: {};
|
|
190
193
|
}
|
|
191
194
|
|
|
195
|
+
/** `lifetimeMs` is the access-token lifetime the complete execution needs. */
|
|
192
196
|
export async function prepareClaudeState(
|
|
193
197
|
stateRoot: string,
|
|
194
198
|
options: ClaudeHostOptions,
|
|
195
199
|
request: Request,
|
|
196
200
|
pluginDirs: string[],
|
|
201
|
+
lifetimeMs: number,
|
|
197
202
|
) {
|
|
198
203
|
await requireClaudeRuntimeNamespace(request, stateRoot);
|
|
199
|
-
const state = await createClaudePrivateState(stateRoot, options);
|
|
204
|
+
const state = await createClaudePrivateState(stateRoot, options, lifetimeMs);
|
|
200
205
|
const transcripts = await claudeTranscriptAccess(
|
|
201
206
|
stateRoot,
|
|
202
207
|
state.credential,
|
|
@@ -221,6 +226,11 @@ export async function prepareClaudeState(
|
|
|
221
226
|
runtime,
|
|
222
227
|
stateRoot,
|
|
223
228
|
);
|
|
229
|
+
const cacheRoots = await sharedCacheRoots(
|
|
230
|
+
request.runtimePolicy,
|
|
231
|
+
request.runtimeRole,
|
|
232
|
+
roots,
|
|
233
|
+
);
|
|
224
234
|
await writeFile(
|
|
225
235
|
settingsPath,
|
|
226
236
|
JSON.stringify(
|
|
@@ -228,7 +238,10 @@ export async function prepareClaudeState(
|
|
|
228
238
|
state.privateRoot,
|
|
229
239
|
state.credential,
|
|
230
240
|
[...pluginDirs, ...effectivePlugins],
|
|
231
|
-
|
|
241
|
+
[
|
|
242
|
+
...roots,
|
|
243
|
+
...sharedCacheDenyRoots(request.runtimePolicy, request.runtimeRole),
|
|
244
|
+
],
|
|
232
245
|
transcripts.policy,
|
|
233
246
|
[
|
|
234
247
|
...transcripts.readRoots,
|
|
@@ -236,7 +249,7 @@ export async function prepareClaudeState(
|
|
|
236
249
|
...candidateArtifactReadRoots(request.candidateArtifactRoots),
|
|
237
250
|
],
|
|
238
251
|
runtimeWriteRoot
|
|
239
|
-
? { root: runtimeWriteRoot, workspace: request.workspace }
|
|
252
|
+
? { root: runtimeWriteRoot, workspace: request.workspace, cacheRoots }
|
|
240
253
|
: undefined,
|
|
241
254
|
request.workspace,
|
|
242
255
|
),
|
|
@@ -308,7 +321,7 @@ async function claudeTranscriptAccess(
|
|
|
308
321
|
await mkdir(projects, { recursive: true, mode: 0o700 });
|
|
309
322
|
await symlink(projects, join(dirname(credential), "projects"));
|
|
310
323
|
const startup = join(view, "shell-env");
|
|
311
|
-
const script = `export CLAUDE_CONFIG_DIR
|
|
324
|
+
const script = `export CLAUDE_CONFIG_DIR=${shellQuote(view)}\n`;
|
|
312
325
|
await writeFile(startup, script, { flag: "wx", mode: 0o600 });
|
|
313
326
|
await writeFile(join(view, ".zshenv"), script, { flag: "wx", mode: 0o600 });
|
|
314
327
|
return {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { sha256 } from "../identity";
|
|
2
2
|
import { isRecord } from "../value-guards";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
summarizeClaudeEvents,
|
|
5
|
+
parseClaudeEvents,
|
|
6
|
+
type ClaudeEvents,
|
|
7
|
+
type ClaudeEventLine,
|
|
8
|
+
} from "./claude-events";
|
|
4
9
|
|
|
5
10
|
const MAX_RETAINED_CALLS = 128;
|
|
6
11
|
type Entry = Record<string, unknown>;
|
|
@@ -51,18 +56,18 @@ type Actor = { actor: "parent" | "nested"; parentToolUseId: string | null };
|
|
|
51
56
|
|
|
52
57
|
/** Retain ordered host call metadata, never tool prompts or results. */
|
|
53
58
|
export function claudeToolCallsObservation(
|
|
54
|
-
stream: string,
|
|
59
|
+
stream: string | ClaudeEvents,
|
|
55
60
|
exitCode: number,
|
|
56
61
|
): ClaudeToolCallsObservation {
|
|
57
|
-
const
|
|
62
|
+
const events = parseClaudeEvents(stream);
|
|
63
|
+
const summary = summarizeClaudeEvents(events, exitCode);
|
|
58
64
|
const state: CallState = {
|
|
59
65
|
calls: [],
|
|
60
66
|
malformed: false,
|
|
61
67
|
truncated: false,
|
|
62
68
|
ordinal: 0,
|
|
63
69
|
};
|
|
64
|
-
for (const line of
|
|
65
|
-
if (!line.trim()) continue;
|
|
70
|
+
for (const line of events.lines) {
|
|
66
71
|
collectEvent(line, state);
|
|
67
72
|
if (state.truncated) break;
|
|
68
73
|
}
|
|
@@ -86,14 +91,12 @@ function callCompleteness(
|
|
|
86
91
|
: "partial";
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
function collectEvent(line:
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
event = JSON.parse(line);
|
|
93
|
-
} catch {
|
|
94
|
+
function collectEvent(line: ClaudeEventLine, state: CallState): void {
|
|
95
|
+
if (!line.parsed) {
|
|
94
96
|
state.malformed = true;
|
|
95
97
|
return;
|
|
96
98
|
}
|
|
99
|
+
const event = line.value;
|
|
97
100
|
if (!isRecord(event) || event.type !== "assistant") return;
|
|
98
101
|
collectAssistant(event, state);
|
|
99
102
|
}
|