@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +170 -0
- package/LICENSE +63 -136
- package/README.md +83 -66
- package/docs/README.md +44 -0
- package/docs/acknowledgements.md +14 -0
- package/docs/architecture.md +146 -0
- package/docs/assets/README.md +27 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +1 -0
- package/docs/assets/sevro-wordmark.svg +1 -0
- package/docs/builtin-graders-v1.md +41 -13
- package/docs/codex-host.md +184 -52
- package/docs/contracts.md +19 -0
- package/docs/creating-cases.md +139 -0
- package/docs/development-cli.md +190 -30
- package/docs/documentation-quality.md +61 -0
- package/docs/evaluation-workflow.md +110 -0
- package/docs/evidence/code-mode-controls-71.md +64 -0
- package/docs/evidence/code-mode-programs-85.md +64 -0
- package/docs/evidence/codex-state-73/README.md +204 -0
- package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
- package/docs/evidence/codex-state-73/results.json +661 -0
- package/docs/evidence/local-http-74.md +104 -0
- package/docs/evidence/runtime-startup-30/README.md +185 -0
- package/docs/evidence/runtime-startup-30/after.json +319 -0
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
- package/docs/evidence/runtime-startup-30/before.json +403 -0
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
- package/docs/evidence/skill-reads-70/results.json +823 -0
- package/docs/evidence.md +21 -0
- package/docs/extension-protocol-v1.md +50 -16
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/getting-started.md +102 -0
- package/docs/guide-evaluation.md +89 -0
- package/docs/guide-verification.json +239 -0
- package/docs/identity-v1.md +35 -1
- package/docs/installing.md +88 -0
- package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
- package/docs/licensing.md +42 -0
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +228 -0
- package/docs/reading-results.md +141 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +81 -40
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +180 -0
- package/docs/run-settings-v1.md +147 -0
- package/docs/running-evaluations.md +31 -0
- package/docs/runtime-v1.md +614 -0
- package/docs/specs/repository-guide.md +58 -0
- package/docs/troubleshooting.md +30 -0
- package/docs/typescript-quality.md +234 -0
- package/docs/usability-study.md +114 -0
- package/examples/basic/host.ts +2 -2
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +39 -6
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +193 -0
- package/src/advisory.ts +10 -5
- package/src/candidate-artifacts.ts +208 -0
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +105 -0
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +81 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +59 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +222 -0
- package/src/cli/hosts.ts +59 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/cli/invocation.ts +627 -0
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +163 -0
- package/src/cli/presentation.ts +91 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +133 -0
- package/src/cli/run-options.ts +101 -0
- package/src/cli/run-overrides.ts +83 -0
- package/src/cli/settings.ts +160 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -843
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +19 -2373
- package/src/evaluation/advisory.ts +150 -0
- package/src/evaluation/check-plan.ts +510 -0
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/evaluation/grading-host.ts +39 -0
- package/src/evaluation/grading.ts +258 -0
- package/src/evaluation/invocation-plan.ts +548 -0
- package/src/evaluation/plan.ts +736 -0
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
- package/src/evaluation/semantic.ts +312 -0
- package/src/evaluation/trial.ts +866 -0
- package/src/evaluation/types.ts +207 -0
- package/src/evidence/host-results.ts +92 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +228 -148
- package/src/extension-session.ts +326 -195
- package/src/fixtures/advisory.ts +338 -0
- package/src/fixtures/bin.ts +57 -0
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +187 -0
- package/src/fixtures/generated.ts +300 -0
- package/src/fixtures/git-hooks.ts +24 -0
- package/src/fixtures/preparation.ts +270 -0
- package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
- package/src/fixtures/setup.ts +224 -0
- package/src/fixtures/tools.ts +88 -0
- package/src/fixtures/workspace-state.ts +212 -0
- package/src/generated/cli-result.cjs +2 -0
- package/src/generated/cli-result.d.cts +3 -0
- package/src/generated/extension.cjs +2 -0
- package/src/generated/extension.d.cts +3 -0
- package/src/generated/report.cjs +2 -0
- package/src/generated/report.d.cts +3 -0
- package/src/generated/run-evidence.cjs +2 -0
- package/src/generated/run-evidence.d.cts +3 -0
- package/src/generated/runtime.cjs +2 -0
- package/src/generated/runtime.d.cts +3 -0
- package/src/graders/artifact.ts +56 -23
- package/src/graders/git-head.ts +79 -75
- package/src/graders/output.ts +222 -140
- package/src/graders/semantic.ts +102 -43
- package/src/graders/shell.ts +379 -207
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +319 -0
- package/src/hosts/claude-continuation.ts +63 -18
- package/src/hosts/claude-credential.ts +43 -62
- package/src/hosts/claude-events.ts +86 -51
- package/src/hosts/claude-keychain.ts +59 -0
- package/src/hosts/claude-native-goal.ts +57 -33
- package/src/hosts/claude-process.ts +111 -0
- package/src/hosts/claude-repository-invocation.ts +208 -117
- package/src/hosts/claude-settings.ts +240 -21
- package/src/hosts/claude-skill-activations.ts +166 -0
- package/src/hosts/claude-state.ts +403 -0
- package/src/hosts/claude-tool-calls.ts +171 -106
- package/src/hosts/claude.ts +561 -451
- package/src/hosts/codex-app-server.ts +326 -178
- package/src/hosts/codex-configuration.ts +44 -18
- package/src/hosts/codex-events.ts +97 -60
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-invocation.ts +70 -0
- package/src/hosts/codex-marketplace.ts +283 -0
- package/src/hosts/codex-native-calls.ts +546 -500
- package/src/hosts/codex-process.ts +202 -0
- package/src/hosts/codex-profile.ts +257 -28
- package/src/hosts/codex-session-files.ts +106 -0
- package/src/hosts/codex-state.ts +492 -0
- package/src/hosts/codex.ts +783 -832
- package/src/hosts/isolation-common.ts +94 -0
- package/src/hosts/isolation-roots.ts +54 -27
- package/src/hosts/isolation.ts +10 -0
- package/src/hosts/linux-sandbox.ts +159 -0
- package/src/hosts/mac-sandbox.ts +187 -61
- package/src/hosts/native-controls.ts +152 -62
- package/src/hosts/process-lifecycle.ts +37 -0
- package/src/hosts/protected-worktrees.ts +51 -19
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +580 -0
- package/src/hosts/workspace-fingerprint.ts +55 -35
- package/src/identity.ts +102 -48
- package/src/instrumentation.ts +109 -48
- package/src/native-transcript-state.ts +131 -0
- package/src/native-transcript-view.ts +90 -0
- package/src/native-transcripts.ts +286 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +161 -81
- package/src/prune.ts +215 -0
- package/src/report.ts +172 -140
- package/src/resolved-case.ts +33 -0
- package/src/results-lock.ts +29 -0
- package/src/results.ts +133 -86
- package/src/run-owner.ts +154 -63
- package/src/runtime/config.ts +456 -0
- package/src/runtime/discovery-apple.ts +90 -0
- package/src/runtime/discovery-providers.ts +63 -0
- package/src/runtime/discovery.ts +70 -0
- package/src/runtime/hook-processes.ts +97 -0
- package/src/runtime/hooks.ts +476 -0
- package/src/runtime/paths.ts +313 -0
- package/src/runtime/query.ts +85 -0
- package/src/runtime/seeds.ts +134 -0
- package/src/runtime/shared-cache.ts +143 -0
- package/src/runtime/state.ts +143 -0
- package/src/schema-types.ts +198 -0
- package/src/schema.ts +15 -19
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +20 -0
- package/src/advisory-fixture.ts +0 -282
- package/src/fixture-bin.ts +0 -87
- package/src/fixture-setup.ts +0 -163
- package/src/generated-fixture.ts +0 -220
- package/src/git-hooks.ts +0 -63
- package/src/hosts/claude-nested-skills.ts +0 -218
- package/src/hosts/codex-command-output.ts +0 -399
- package/src/hosts/codex-executor-command.ts +0 -195
- package/src/hosts/codex-skill-reads.ts +0 -391
- package/src/preparation.ts +0 -211
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { gzipSync } from "node:zlib";
|
|
4
|
+
import { sha256 } from "./identity";
|
|
5
|
+
import { HostExecutionFailure, type HostResult } from "./evaluation/types";
|
|
6
|
+
import type { RuntimeRole } from "./runtime/state";
|
|
7
|
+
import type { RuntimePolicy } from "./runtime/config";
|
|
8
|
+
import { isRecord } from "./value-guards";
|
|
9
|
+
import {
|
|
10
|
+
rethrowDiagnosticPersistenceError,
|
|
11
|
+
type HostDiagnostics,
|
|
12
|
+
} from "./host-diagnostics";
|
|
13
|
+
|
|
14
|
+
export function candidateTranscriptRoots(root: string | undefined): string[] {
|
|
15
|
+
return root ? [root] : [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function candidateTranscriptEnvironment(
|
|
19
|
+
root: string | undefined,
|
|
20
|
+
): Record<string, string> {
|
|
21
|
+
return root ? { SEVRO_CANDIDATE_TRANSCRIPTS: root } : {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function retainEnabledNativeTranscripts(
|
|
25
|
+
result: HostResult,
|
|
26
|
+
policy: RuntimePolicy | undefined,
|
|
27
|
+
root: string,
|
|
28
|
+
host: "codex" | "claude",
|
|
29
|
+
role?: RuntimeRole,
|
|
30
|
+
rootSessionId?: string,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (policy?.nativeTranscripts === true)
|
|
33
|
+
await retainNativeTranscripts(
|
|
34
|
+
result,
|
|
35
|
+
root,
|
|
36
|
+
host,
|
|
37
|
+
role,
|
|
38
|
+
rootSessionId ?? observedRootSession(result),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function observedRootSession(result: HostResult): string | undefined {
|
|
43
|
+
const observation = result.observations?.find(
|
|
44
|
+
(item) => item.id === "sevro.host.native-goal",
|
|
45
|
+
);
|
|
46
|
+
const session = observation?.data.threadId;
|
|
47
|
+
return typeof session === "string" ? session : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function failedNativeTranscripts(
|
|
51
|
+
cause: unknown,
|
|
52
|
+
policy: RuntimePolicy | undefined,
|
|
53
|
+
root: string,
|
|
54
|
+
host: "codex" | "claude",
|
|
55
|
+
role?: RuntimeRole,
|
|
56
|
+
diagnostics?: HostDiagnostics,
|
|
57
|
+
): Promise<HostResult> {
|
|
58
|
+
rethrowDiagnosticPersistenceError(cause);
|
|
59
|
+
requireFailureCapture(cause, policy, diagnostics);
|
|
60
|
+
const result: HostResult =
|
|
61
|
+
cause instanceof HostExecutionFailure
|
|
62
|
+
? cause.result
|
|
63
|
+
: {
|
|
64
|
+
finalMessage: null,
|
|
65
|
+
complete: false,
|
|
66
|
+
executionFailed: true,
|
|
67
|
+
actualCondition: "passive",
|
|
68
|
+
};
|
|
69
|
+
await diagnostics?.retain(result, cause);
|
|
70
|
+
await retainEnabledNativeTranscripts(result, policy, root, host, role);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requireFailureCapture(
|
|
75
|
+
cause: unknown,
|
|
76
|
+
policy: RuntimePolicy | undefined,
|
|
77
|
+
diagnostics: HostDiagnostics | undefined,
|
|
78
|
+
): void {
|
|
79
|
+
if (policy?.nativeTranscripts !== true && !diagnostics) throw cause;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const MAX_FILES = 4096;
|
|
83
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
84
|
+
const MAX_ENCODED_FILES_BYTES = 7 * 1024 * 1024;
|
|
85
|
+
type NativeFile = { path: string; sha256: string; gzipBase64: string };
|
|
86
|
+
type Capture = {
|
|
87
|
+
files: NativeFile[];
|
|
88
|
+
bytes: number;
|
|
89
|
+
entries: number;
|
|
90
|
+
encodedBytes: number;
|
|
91
|
+
issues: string[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Capture only a fresh role's transcript tree; never follow native source links. */
|
|
95
|
+
export async function retainNativeTranscripts(
|
|
96
|
+
result: HostResult,
|
|
97
|
+
root: string,
|
|
98
|
+
host: "codex" | "claude",
|
|
99
|
+
role: RuntimeRole = "candidate",
|
|
100
|
+
rootSessionId: string | null = null,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const capture: Capture = {
|
|
103
|
+
files: [],
|
|
104
|
+
bytes: 0,
|
|
105
|
+
entries: 0,
|
|
106
|
+
encodedBytes: 0,
|
|
107
|
+
issues: [],
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
await captureDirectory(root, "", capture);
|
|
111
|
+
} catch {
|
|
112
|
+
capture.issues.push("source_unavailable");
|
|
113
|
+
}
|
|
114
|
+
observeCaptureRoot(capture, rootSessionId, host);
|
|
115
|
+
const completeness = captureCompleteness(capture, result);
|
|
116
|
+
const bundle = {
|
|
117
|
+
format: "sevro.native-transcripts.v2",
|
|
118
|
+
host,
|
|
119
|
+
role,
|
|
120
|
+
rootSessionId,
|
|
121
|
+
completeness,
|
|
122
|
+
issues: capture.issues,
|
|
123
|
+
files: capture.files,
|
|
124
|
+
};
|
|
125
|
+
const lists = nativeResultLists(result);
|
|
126
|
+
lists.artifacts.push({
|
|
127
|
+
id: "sevro.native-transcripts.bundle",
|
|
128
|
+
bytes: Buffer.from(JSON.stringify(bundle)),
|
|
129
|
+
});
|
|
130
|
+
lists.observations.push({
|
|
131
|
+
id: "sevro.host.native-transcripts",
|
|
132
|
+
completeness,
|
|
133
|
+
data: {
|
|
134
|
+
method: "native_files",
|
|
135
|
+
host,
|
|
136
|
+
role,
|
|
137
|
+
rootSessionId,
|
|
138
|
+
artifactId: "sevro.native-transcripts.bundle",
|
|
139
|
+
issues: capture.issues,
|
|
140
|
+
files: capture.files.map(({ path, sha256 }) => ({ path, sha256 })),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function nativeResultLists(result: HostResult) {
|
|
146
|
+
result.artifacts ??= [];
|
|
147
|
+
result.observations ??= [];
|
|
148
|
+
return { artifacts: result.artifacts, observations: result.observations };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function observeCaptureRoot(
|
|
152
|
+
capture: Capture,
|
|
153
|
+
session: string | null,
|
|
154
|
+
host: string,
|
|
155
|
+
): void {
|
|
156
|
+
if (session === null) {
|
|
157
|
+
capture.issues.push("root_session_unknown");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const suffix = host === "codex" ? `-${session}.jsonl` : `/${session}.jsonl`;
|
|
161
|
+
if (!capture.files.some((file) => file.path.endsWith(suffix)))
|
|
162
|
+
capture.issues.push("root_session_missing");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function captureCompleteness(capture: Capture, result: HostResult) {
|
|
166
|
+
if (!capture.files.some((file) => file.path.endsWith(".jsonl")))
|
|
167
|
+
return "unavailable" as const;
|
|
168
|
+
if (capture.issues.length || !result.complete || result.executionFailed)
|
|
169
|
+
return "partial" as const;
|
|
170
|
+
return "complete" as const;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function captureDirectory(
|
|
174
|
+
root: string,
|
|
175
|
+
path: string,
|
|
176
|
+
capture: Capture,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const directory = join(root, path);
|
|
179
|
+
if (!(await lstat(directory)).isDirectory())
|
|
180
|
+
throw new Error("invalid source directory");
|
|
181
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
182
|
+
capture.entries++;
|
|
183
|
+
if (capture.entries > MAX_FILES) {
|
|
184
|
+
capture.issues.push("entry_limit");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
await captureEntry(root, join(path, name), capture);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function captureEntry(
|
|
192
|
+
root: string,
|
|
193
|
+
path: string,
|
|
194
|
+
capture: Capture,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
try {
|
|
197
|
+
const info = await lstat(join(root, path));
|
|
198
|
+
if (info.isDirectory()) {
|
|
199
|
+
await captureDirectory(root, path, capture);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!info.isFile()) {
|
|
203
|
+
capture.issues.push("unsafe_entry");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
await captureFile(root, path, info.size, capture);
|
|
207
|
+
} catch {
|
|
208
|
+
capture.issues.push("entry_unavailable");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function captureFile(
|
|
213
|
+
root: string,
|
|
214
|
+
path: string,
|
|
215
|
+
size: number,
|
|
216
|
+
capture: Capture,
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
if (capture.bytes + size > MAX_BYTES) {
|
|
219
|
+
capture.issues.push("byte_limit");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const bytes = await readFile(join(root, path));
|
|
223
|
+
if (bytes.length !== size) {
|
|
224
|
+
capture.issues.push("source_changed");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const file = {
|
|
228
|
+
path,
|
|
229
|
+
sha256: sha256(bytes),
|
|
230
|
+
gzipBase64: gzipSync(bytes).toString("base64"),
|
|
231
|
+
};
|
|
232
|
+
const issue = nativeFileIssue(path, bytes);
|
|
233
|
+
if (issue && !capture.issues.includes(issue)) capture.issues.push(issue);
|
|
234
|
+
retainCaptureFile(file, bytes.length, capture);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function nativeFileIssue(path: string, bytes: Buffer): string | null {
|
|
238
|
+
if (path.endsWith(".json")) return nativeSidecarIssue(bytes);
|
|
239
|
+
if (!path.endsWith(".jsonl")) return null;
|
|
240
|
+
return nativeTranscriptIssue(bytes);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function nativeTranscriptIssue(bytes: Buffer): string | null {
|
|
244
|
+
try {
|
|
245
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
246
|
+
if (!text.trim()) return "empty_transcript";
|
|
247
|
+
const records = text.replace(/\r?\n$/, "").split("\n");
|
|
248
|
+
if (!records.every(validNativeRecord)) return "malformed_transcript";
|
|
249
|
+
return null;
|
|
250
|
+
} catch {
|
|
251
|
+
return "malformed_transcript";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function nativeSidecarIssue(bytes: Buffer): string | null {
|
|
256
|
+
try {
|
|
257
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
258
|
+
return null;
|
|
259
|
+
} catch {
|
|
260
|
+
return "malformed_sidecar";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validNativeRecord(line: string): boolean {
|
|
265
|
+
const record: unknown = JSON.parse(line);
|
|
266
|
+
return isRecord(record) && nativeRecordType(record.type);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function nativeRecordType(type: unknown): boolean {
|
|
270
|
+
return typeof type === "string" && type.trim().length > 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function retainCaptureFile(
|
|
274
|
+
file: NativeFile,
|
|
275
|
+
size: number,
|
|
276
|
+
capture: Capture,
|
|
277
|
+
): void {
|
|
278
|
+
const encoded = Buffer.byteLength(JSON.stringify(file));
|
|
279
|
+
if (capture.encodedBytes + encoded > MAX_ENCODED_FILES_BYTES) {
|
|
280
|
+
capture.issues.push("bundle_limit");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
capture.bytes += size;
|
|
284
|
+
capture.encodedBytes += encoded;
|
|
285
|
+
capture.files.push(file);
|
|
286
|
+
}
|
|
@@ -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
|
+
}
|