@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
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { gunzipSync } from "node:zlib";
|
|
3
4
|
import { allocateNativeState } from "./native-transcript-state";
|
|
4
|
-
import { fixtureParts } from "./preparation";
|
|
5
|
+
import { fixtureParts } from "./fixtures/preparation";
|
|
5
6
|
import { isRecord, isUnknownArray } from "./value-guards";
|
|
6
7
|
import { sha256 } from "./identity";
|
|
7
8
|
|
|
9
|
+
const MAX_NATIVE_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
type NativeFormat =
|
|
11
|
+
"sevro.native-transcripts.v1" | "sevro.native-transcripts.v2";
|
|
12
|
+
|
|
8
13
|
export async function materializeNativeTranscriptView(
|
|
9
14
|
bytes: Uint8Array,
|
|
10
15
|
): Promise<string> {
|
|
@@ -12,19 +17,33 @@ export async function materializeNativeTranscriptView(
|
|
|
12
17
|
const bundle = nativeBundle(value);
|
|
13
18
|
const root = await allocateNativeState("view-");
|
|
14
19
|
await writeFile(join(root, "index.json"), bytes, { flag: "wx", mode: 0o600 });
|
|
20
|
+
const filesRoot = join(root, bundle.host);
|
|
21
|
+
let remainingBytes = MAX_NATIVE_BYTES;
|
|
15
22
|
for (const item of bundle.files)
|
|
16
|
-
await materializeNativeFile(
|
|
23
|
+
remainingBytes -= await materializeNativeFile(
|
|
24
|
+
filesRoot,
|
|
25
|
+
item,
|
|
26
|
+
bundle.format,
|
|
27
|
+
remainingBytes,
|
|
28
|
+
);
|
|
17
29
|
return root;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
function nativeBundle(value: unknown) {
|
|
21
|
-
if (!isRecord(value) || value.format
|
|
33
|
+
if (!isRecord(value) || !nativeFormat(value.format))
|
|
22
34
|
throw new Error("invalid native transcript bundle");
|
|
23
35
|
if (!nativeHost(value.host))
|
|
24
36
|
throw new Error("invalid native transcript host");
|
|
25
37
|
if (!isUnknownArray(value.files))
|
|
26
38
|
throw new Error("invalid native transcript files");
|
|
27
|
-
return { host: value.host, files: value.files };
|
|
39
|
+
return { host: value.host, files: value.files, format: value.format };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function nativeFormat(value: unknown): value is NativeFormat {
|
|
43
|
+
return (
|
|
44
|
+
value === "sevro.native-transcripts.v1" ||
|
|
45
|
+
value === "sevro.native-transcripts.v2"
|
|
46
|
+
);
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
function nativeHost(value: unknown): value is "codex" | "claude" {
|
|
@@ -33,19 +52,39 @@ function nativeHost(value: unknown): value is "codex" | "claude" {
|
|
|
33
52
|
|
|
34
53
|
async function materializeNativeFile(
|
|
35
54
|
root: string,
|
|
36
|
-
host: string,
|
|
37
55
|
value: unknown,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
typeof value.bytesBase64 !== "string"
|
|
43
|
-
)
|
|
56
|
+
format: NativeFormat,
|
|
57
|
+
remainingBytes: number,
|
|
58
|
+
): Promise<number> {
|
|
59
|
+
if (!isRecord(value) || typeof value.path !== "string")
|
|
44
60
|
throw new Error("invalid native transcript file");
|
|
45
|
-
const path = join(root,
|
|
46
|
-
const bytes =
|
|
61
|
+
const path = join(root, ...fixtureParts(value.path));
|
|
62
|
+
const bytes = nativeFileBytes(value, format, remainingBytes);
|
|
63
|
+
if (bytes.length > remainingBytes)
|
|
64
|
+
throw new Error("native transcript byte limit exceeded");
|
|
47
65
|
if (sha256(bytes) !== value.sha256)
|
|
48
66
|
throw new Error("native transcript hash mismatch");
|
|
49
67
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
50
68
|
await writeFile(path, bytes, { flag: "wx", mode: 0o600 });
|
|
69
|
+
return bytes.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function nativeFileBytes(
|
|
73
|
+
value: Record<string, unknown>,
|
|
74
|
+
format: NativeFormat,
|
|
75
|
+
remainingBytes: number,
|
|
76
|
+
): Buffer {
|
|
77
|
+
const encoded =
|
|
78
|
+
format === "sevro.native-transcripts.v1"
|
|
79
|
+
? value.bytesBase64
|
|
80
|
+
: value.gzipBase64;
|
|
81
|
+
if (typeof encoded !== "string")
|
|
82
|
+
throw new Error("invalid native transcript file");
|
|
83
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
84
|
+
if (format === "sevro.native-transcripts.v1") return bytes;
|
|
85
|
+
try {
|
|
86
|
+
return gunzipSync(bytes, { maxOutputLength: Math.max(1, remainingBytes) });
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
throw new Error("invalid native transcript gzip", { cause });
|
|
89
|
+
}
|
|
51
90
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { gzipSync } from "node:zlib";
|
|
3
4
|
import { sha256 } from "./identity";
|
|
4
|
-
import type
|
|
5
|
-
import type { RuntimeRole } from "./runtime
|
|
6
|
-
import type { RuntimePolicy } from "./runtime
|
|
5
|
+
import { HostExecutionFailure, type HostResult } from "./evaluation/types";
|
|
6
|
+
import type { RuntimeRole } from "./runtime/state";
|
|
7
|
+
import type { RuntimePolicy } from "./runtime/config";
|
|
7
8
|
import { isRecord } from "./value-guards";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
rethrowDiagnosticPersistenceError,
|
|
11
|
+
type HostDiagnostics,
|
|
12
|
+
} from "./host-diagnostics";
|
|
9
13
|
|
|
10
14
|
export function candidateTranscriptRoots(root: string | undefined): string[] {
|
|
11
15
|
return root ? [root] : [];
|
|
@@ -48,11 +52,13 @@ export async function failedNativeTranscripts(
|
|
|
48
52
|
policy: RuntimePolicy | undefined,
|
|
49
53
|
root: string,
|
|
50
54
|
host: "codex" | "claude",
|
|
51
|
-
role
|
|
55
|
+
role: RuntimeRole | undefined,
|
|
56
|
+
diagnostics: HostDiagnostics,
|
|
52
57
|
): Promise<HostResult> {
|
|
53
|
-
|
|
58
|
+
rethrowDiagnosticPersistenceError(cause);
|
|
59
|
+
requireFailureCapture(cause, policy, diagnostics);
|
|
54
60
|
const result: HostResult =
|
|
55
|
-
cause instanceof
|
|
61
|
+
cause instanceof HostExecutionFailure
|
|
56
62
|
? cause.result
|
|
57
63
|
: {
|
|
58
64
|
finalMessage: null,
|
|
@@ -60,14 +66,23 @@ export async function failedNativeTranscripts(
|
|
|
60
66
|
executionFailed: true,
|
|
61
67
|
actualCondition: "passive",
|
|
62
68
|
};
|
|
63
|
-
await
|
|
69
|
+
await diagnostics.retain(result, cause);
|
|
70
|
+
await retainEnabledNativeTranscripts(result, policy, root, host, role);
|
|
64
71
|
return result;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
function requireFailureCapture(
|
|
75
|
+
cause: unknown,
|
|
76
|
+
policy: RuntimePolicy | undefined,
|
|
77
|
+
diagnostics: HostDiagnostics,
|
|
78
|
+
): void {
|
|
79
|
+
if (policy?.nativeTranscripts !== true && !diagnostics.enabled) throw cause;
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
const MAX_FILES = 4096;
|
|
68
83
|
const MAX_BYTES = 5 * 1024 * 1024;
|
|
69
84
|
const MAX_ENCODED_FILES_BYTES = 7 * 1024 * 1024;
|
|
70
|
-
type NativeFile = { path: string; sha256: string;
|
|
85
|
+
type NativeFile = { path: string; sha256: string; gzipBase64: string };
|
|
71
86
|
type Capture = {
|
|
72
87
|
files: NativeFile[];
|
|
73
88
|
bytes: number;
|
|
@@ -99,7 +114,7 @@ export async function retainNativeTranscripts(
|
|
|
99
114
|
observeCaptureRoot(capture, rootSessionId, host);
|
|
100
115
|
const completeness = captureCompleteness(capture, result);
|
|
101
116
|
const bundle = {
|
|
102
|
-
format: "sevro.native-transcripts.
|
|
117
|
+
format: "sevro.native-transcripts.v2",
|
|
103
118
|
host,
|
|
104
119
|
role,
|
|
105
120
|
rootSessionId,
|
|
@@ -212,7 +227,7 @@ async function captureFile(
|
|
|
212
227
|
const file = {
|
|
213
228
|
path,
|
|
214
229
|
sha256: sha256(bytes),
|
|
215
|
-
|
|
230
|
+
gzipBase64: gzipSync(bytes).toString("base64"),
|
|
216
231
|
};
|
|
217
232
|
const issue = nativeFileIssue(path, bytes);
|
|
218
233
|
if (issue && !capture.issues.includes(issue)) capture.issues.push(issue);
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** Lexical containment includes the root itself; callers resolve symlinks first. */
|
|
4
|
+
export function insideRoot(root: string, path: string): boolean {
|
|
5
|
+
const child = relative(root, path);
|
|
6
|
+
return (
|
|
7
|
+
child === "" ||
|
|
8
|
+
(child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Keep outermost roots in input order, retaining the first equivalent spelling. */
|
|
13
|
+
export function minimalRoots(roots: string[]): string[] {
|
|
14
|
+
return roots.filter(
|
|
15
|
+
(root, index) =>
|
|
16
|
+
!roots.some(
|
|
17
|
+
(other, otherIndex) =>
|
|
18
|
+
insideRoot(other, root) &&
|
|
19
|
+
(!insideRoot(root, other) || otherIndex < index),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export type ProcessInterruptionReason = "timeout" | "cancelled";
|
|
2
|
+
|
|
3
|
+
interface DeadlineOptions {
|
|
4
|
+
timeoutMs: number;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
interruption: (reason: ProcessInterruptionReason) => Error;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Keep the deadline active until the caller has finished failure cleanup. */
|
|
10
|
+
export class ProcessDeadline {
|
|
11
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
12
|
+
private cancel: (() => void) | undefined;
|
|
13
|
+
|
|
14
|
+
constructor(private readonly options: DeadlineOptions) {}
|
|
15
|
+
|
|
16
|
+
wait<T>(completed: Promise<T> | (() => Promise<T>)): Promise<T> {
|
|
17
|
+
const timeout = this.timeout();
|
|
18
|
+
const aborted = this.aborted();
|
|
19
|
+
const completion =
|
|
20
|
+
typeof completed === "function" ? completed() : completed;
|
|
21
|
+
return Promise.race([completion, timeout, aborted]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private timeout(): Promise<never> {
|
|
25
|
+
return new Promise((_resolve, reject) => {
|
|
26
|
+
this.timer = setTimeout(() => {
|
|
27
|
+
reject(this.options.interruption("timeout"));
|
|
28
|
+
}, this.options.timeoutMs);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private aborted(): Promise<never> {
|
|
33
|
+
return new Promise((_resolve, reject) => {
|
|
34
|
+
const signal = this.options.signal;
|
|
35
|
+
if (!signal) return;
|
|
36
|
+
this.cancel = () => {
|
|
37
|
+
reject(this.options.interruption("cancelled"));
|
|
38
|
+
};
|
|
39
|
+
if (signal.aborted) this.cancel();
|
|
40
|
+
else signal.addEventListener("abort", this.cancel, { once: true });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dispose(): void {
|
|
45
|
+
if (this.timer) clearTimeout(this.timer);
|
|
46
|
+
if (this.cancel)
|
|
47
|
+
this.options.signal?.removeEventListener("abort", this.cancel);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface OwnedProcess {
|
|
52
|
+
pid: number;
|
|
53
|
+
kill: (signal: "SIGKILL") => unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TerminationOptions {
|
|
57
|
+
alwaysGroup?: boolean;
|
|
58
|
+
fallbackError?: "ignore" | "propagate";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function killGroup(pid: number): boolean {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(-pid, "SIGKILL");
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
// The owned process group may already be gone.
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function useProcessGroup(options: TerminationOptions): boolean {
|
|
72
|
+
return options.alwaysGroup === true || process.platform !== "win32";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function killOwnedProcess(
|
|
76
|
+
proc: OwnedProcess,
|
|
77
|
+
options: TerminationOptions,
|
|
78
|
+
): void {
|
|
79
|
+
if (options.fallbackError === "propagate") {
|
|
80
|
+
proc.kill("SIGKILL");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
proc.kill("SIGKILL");
|
|
85
|
+
} catch {
|
|
86
|
+
// The owned process may already have exited.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Only use for children spawned in a new, runner-owned process group. */
|
|
91
|
+
export function terminateOwnedProcessGroup(
|
|
92
|
+
proc: OwnedProcess,
|
|
93
|
+
options: TerminationOptions = {},
|
|
94
|
+
): void {
|
|
95
|
+
if (useProcessGroup(options) && killGroup(proc.pid)) return;
|
|
96
|
+
killOwnedProcess(proc, options);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type RejectedCapture = {
|
|
100
|
+
overflow: "reject";
|
|
101
|
+
exceeded: () => Error;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
interface CaptureOptions {
|
|
105
|
+
limit: number;
|
|
106
|
+
observe?: (bytes: Uint8Array) => void;
|
|
107
|
+
cancelOnError?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type ByteCaptureOptions = CaptureOptions &
|
|
111
|
+
(RejectedCapture | { overflow: "truncate" });
|
|
112
|
+
|
|
113
|
+
function retainedChunk(
|
|
114
|
+
bytes: Uint8Array,
|
|
115
|
+
size: number,
|
|
116
|
+
options: ByteCaptureOptions,
|
|
117
|
+
): Uint8Array {
|
|
118
|
+
if (options.overflow === "reject" && size + bytes.byteLength > options.limit)
|
|
119
|
+
throw options.exceeded();
|
|
120
|
+
return bytes.subarray(0, Math.max(0, options.limit - size));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Truncation retains a byte prefix while still draining the entire stream. */
|
|
124
|
+
export async function captureBoundedBytes(
|
|
125
|
+
stream: ReadableStream<Uint8Array>,
|
|
126
|
+
options: ByteCaptureOptions,
|
|
127
|
+
): Promise<{ bytes: Uint8Array; truncated: boolean }> {
|
|
128
|
+
const chunks: Uint8Array[] = [];
|
|
129
|
+
let size = 0;
|
|
130
|
+
let truncated = false;
|
|
131
|
+
for await (const bytes of stream.values({
|
|
132
|
+
preventCancel: !options.cancelOnError,
|
|
133
|
+
})) {
|
|
134
|
+
options.observe?.(bytes);
|
|
135
|
+
const retained = retainedChunk(bytes, size, options);
|
|
136
|
+
if (retained.byteLength) chunks.push(retained);
|
|
137
|
+
size += retained.byteLength;
|
|
138
|
+
truncated ||= retained.byteLength !== bytes.byteLength;
|
|
139
|
+
}
|
|
140
|
+
return { bytes: Buffer.concat(chunks), truncated };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Reject oversize output before strictly decoding the complete UTF-8 stream. */
|
|
144
|
+
export async function captureBoundedText(
|
|
145
|
+
stream: ReadableStream<Uint8Array>,
|
|
146
|
+
options: CaptureOptions & RejectedCapture,
|
|
147
|
+
): Promise<string> {
|
|
148
|
+
const { bytes } = await captureBoundedBytes(stream, options);
|
|
149
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
150
|
+
}
|
package/src/provenance.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Stats } from "node:fs";
|
|
2
2
|
import { lstat, readFile, readdir, readlink, realpath } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname,
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { insideRoot } from "./paths";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
6
7
|
import packageJson from "../package.json";
|
|
7
8
|
import { hashJson, sha256 } from "./identity";
|
|
9
|
+
import { insideRuntimeRoot } from "./runtime/paths";
|
|
8
10
|
|
|
9
11
|
const MAX_DIRTY_BYTES = 32 * 1024 * 1024;
|
|
10
12
|
const MAX_BUILD_BYTES = 64 * 1024 * 1024;
|
|
@@ -147,7 +149,10 @@ function git(root: string, ...arguments_: string[]): Buffer {
|
|
|
147
149
|
return Buffer.from(result.stdout);
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
async function dirtyPatchDigest(
|
|
152
|
+
async function dirtyPatchDigest(
|
|
153
|
+
gitRoot: string,
|
|
154
|
+
excludedRoots: string[],
|
|
155
|
+
): Promise<string | null> {
|
|
151
156
|
const trackedPatch = git(gitRoot, "diff", "--binary", "HEAD", "--");
|
|
152
157
|
const untrackedPaths = git(
|
|
153
158
|
gitRoot,
|
|
@@ -159,6 +164,12 @@ async function dirtyPatchDigest(gitRoot: string): Promise<string | null> {
|
|
|
159
164
|
.toString("utf8")
|
|
160
165
|
.split("\0")
|
|
161
166
|
.filter(Boolean)
|
|
167
|
+
.filter(
|
|
168
|
+
(path) =>
|
|
169
|
+
!excludedRoots.some((root) =>
|
|
170
|
+
insideRuntimeRoot(root, resolve(gitRoot, path)),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
162
173
|
.sort();
|
|
163
174
|
let dirtyBytes = trackedPatch.byteLength;
|
|
164
175
|
const untracked: { path: string; sha256: string }[] = [];
|
|
@@ -175,8 +186,7 @@ async function dirtyPatchDigest(gitRoot: string): Promise<string | null> {
|
|
|
175
186
|
}
|
|
176
187
|
|
|
177
188
|
function requireSafeUntrackedPath(root: string, absolute: string): void {
|
|
178
|
-
|
|
179
|
-
if (isAbsolute(path) || path === ".." || path.startsWith(`..${sep}`))
|
|
189
|
+
if (!insideRoot(root, absolute))
|
|
180
190
|
throw new Error("Git checkout has an unsafe untracked path");
|
|
181
191
|
}
|
|
182
192
|
async function untrackedIdentityBytes(
|
|
@@ -209,7 +219,7 @@ export async function checkoutProvenance(root: string, buildDigest: string) {
|
|
|
209
219
|
source: "checkout" as const,
|
|
210
220
|
root: pathToFileURL(canonicalRoot).href,
|
|
211
221
|
revision,
|
|
212
|
-
dirtyPatchDigest: await dirtyPatchDigest(canonicalRoot),
|
|
222
|
+
dirtyPatchDigest: await dirtyPatchDigest(canonicalRoot, []),
|
|
213
223
|
buildDigest,
|
|
214
224
|
};
|
|
215
225
|
}
|
|
@@ -249,7 +259,10 @@ async function emptyTemporaryGitPlaceholder(
|
|
|
249
259
|
);
|
|
250
260
|
}
|
|
251
261
|
|
|
252
|
-
export async function projectProvenance(
|
|
262
|
+
export async function projectProvenance(
|
|
263
|
+
root: string,
|
|
264
|
+
excludedRoots?: string[],
|
|
265
|
+
) {
|
|
253
266
|
const canonicalRoot = await realpath(root);
|
|
254
267
|
const gitEntryRoot = await enclosingGitRoot(canonicalRoot);
|
|
255
268
|
if (!gitEntryRoot)
|
|
@@ -279,10 +292,17 @@ export async function projectProvenance(root: string) {
|
|
|
279
292
|
return {
|
|
280
293
|
root: pathToFileURL(canonicalRoot).href,
|
|
281
294
|
revision,
|
|
282
|
-
dirtyPatchDigest: await
|
|
295
|
+
dirtyPatchDigest: await projectDirtyDigest(gitRoot, excludedRoots),
|
|
283
296
|
};
|
|
284
297
|
}
|
|
285
298
|
|
|
299
|
+
async function projectDirtyDigest(
|
|
300
|
+
root: string,
|
|
301
|
+
excludedRoots: string[] | undefined,
|
|
302
|
+
) {
|
|
303
|
+
return dirtyPatchDigest(root, excludedRoots ?? []);
|
|
304
|
+
}
|
|
305
|
+
|
|
286
306
|
export async function runnerProvenance(
|
|
287
307
|
buildDigest: string,
|
|
288
308
|
checkoutRoot?: string,
|