@bjoernrochel/sevro 0.1.0-rc.7 → 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 +17 -3
- package/docs/codex-host.md +155 -49
- 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 +141 -4
- package/docs/run-settings-v1.md +147 -0
- package/docs/runtime-v1.md +307 -18
- 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 +63 -7
- package/src/advisory.ts +2 -2
- package/src/candidate-artifacts.ts +206 -0
- 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 +13 -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} +133 -115
- package/src/evaluation/trial.ts +800 -0
- package/src/evaluation/types.ts +207 -0
- 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} +15 -76
- 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/fixtures/workspace-state.ts +212 -0
- 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 +57 -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 +91 -14
- package/src/hosts/claude-skill-activations.ts +169 -0
- package/src/hosts/claude-state.ts +46 -16
- package/src/hosts/claude-tool-calls.ts +13 -10
- package/src/hosts/claude.ts +122 -73
- 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 -54
- package/src/hosts/codex-profile.ts +50 -21
- package/src/hosts/codex-session-files.ts +1 -9
- package/src/hosts/codex-state.ts +141 -19
- package/src/hosts/codex.ts +221 -179
- package/src/hosts/isolation-common.ts +15 -11
- package/src/hosts/isolation-roots.ts +13 -20
- package/src/hosts/linux-sandbox.ts +27 -3
- package/src/hosts/mac-sandbox.ts +111 -18
- package/src/hosts/native-controls.ts +72 -22
- package/src/hosts/native-request.ts +55 -0
- package/src/hosts/process-lifecycle.ts +221 -45
- 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 +34 -15
- 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/run-owner.ts +4 -4
- package/src/{runtime-config.ts → runtime/config.ts} +182 -44
- 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} +33 -22
- package/src/{runtime-paths.ts → runtime/paths.ts} +6 -18
- 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 -70
- package/docs/guide-verification.json +0 -239
- package/src/evaluation-context.ts +0 -257
- package/src/evaluation-fixture.ts +0 -313
- package/src/evaluation-run.ts +0 -198
- package/src/evaluation-trial.ts +0 -946
- package/src/evaluation-types.ts +0 -149
- 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 -609
- /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
package/src/graders/shell.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import { runtimeSeedSources } from "../runtime
|
|
1
|
+
import { runtimeLocalHttpEnabled, runtimeSeedSources } from "../runtime/config";
|
|
2
|
+
import {
|
|
3
|
+
candidateArtifactEnvironment,
|
|
4
|
+
type CandidateArtifactRoots,
|
|
5
|
+
} from "../candidate-artifacts";
|
|
2
6
|
import { mkdir, stat } from "node:fs/promises";
|
|
3
7
|
import { delimiter, join } from "node:path";
|
|
4
8
|
import { prepareIsolatedCommand } from "../hosts/isolation";
|
|
5
|
-
import type { RuntimePolicy } from "../runtime
|
|
6
|
-
import { prepareRuntimeState } from "../runtime
|
|
9
|
+
import type { RuntimePolicy } from "../runtime/config";
|
|
10
|
+
import { prepareRuntimeState } from "../runtime/state";
|
|
11
|
+
import { sharedCacheDenyRoots } from "../runtime/shared-cache";
|
|
7
12
|
import { hashJson } from "../identity";
|
|
13
|
+
import {
|
|
14
|
+
captureBoundedText,
|
|
15
|
+
ProcessDeadline,
|
|
16
|
+
terminateOwnedProcessGroup,
|
|
17
|
+
} from "../process-lifecycle";
|
|
8
18
|
|
|
9
19
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
10
20
|
const MAX_TIMEOUT_MS = 120_000;
|
|
@@ -235,36 +245,17 @@ function forbiddenPatternFailure(
|
|
|
235
245
|
async function boundedOutput(
|
|
236
246
|
stream: ReadableStream<Uint8Array>,
|
|
237
247
|
): Promise<string> {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
246
|
-
Buffer.concat(chunks),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function stopProcess(proc: Bun.Subprocess): void {
|
|
251
|
-
if (process.platform !== "win32") {
|
|
252
|
-
try {
|
|
253
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
254
|
-
return;
|
|
255
|
-
} catch {
|
|
256
|
-
// The group may already be gone.
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
proc.kill("SIGKILL");
|
|
261
|
-
} catch {
|
|
262
|
-
// The process already exited.
|
|
263
|
-
}
|
|
248
|
+
return captureBoundedText(stream, {
|
|
249
|
+
limit: MAX_OUTPUT_BYTES,
|
|
250
|
+
overflow: "reject",
|
|
251
|
+
exceeded: () => new Error("shell stdout exceeds 1 MiB"),
|
|
252
|
+
cancelOnError: true,
|
|
253
|
+
});
|
|
264
254
|
}
|
|
265
255
|
|
|
266
256
|
interface ShellRunOptions {
|
|
267
257
|
candidateTranscriptRoot?: string;
|
|
258
|
+
candidateArtifactRoots?: CandidateArtifactRoots;
|
|
268
259
|
runtimePolicy?: RuntimePolicy;
|
|
269
260
|
workspace: string;
|
|
270
261
|
fixtureBinDir?: string;
|
|
@@ -289,7 +280,11 @@ async function shellRuntime(options: ShellRunOptions) {
|
|
|
289
280
|
privateRuntimeRoot: runtime.root,
|
|
290
281
|
};
|
|
291
282
|
}
|
|
292
|
-
const root = join(
|
|
283
|
+
const root = join(
|
|
284
|
+
options.privateStateRoot,
|
|
285
|
+
"runtime",
|
|
286
|
+
hashJson(options.workspace),
|
|
287
|
+
),
|
|
293
288
|
home = join(root, "check-home"),
|
|
294
289
|
temp = join(root, "check-tmp");
|
|
295
290
|
await Promise.all([
|
|
@@ -300,9 +295,10 @@ async function shellRuntime(options: ShellRunOptions) {
|
|
|
300
295
|
root,
|
|
301
296
|
home,
|
|
302
297
|
temp,
|
|
303
|
-
uvCache: join(
|
|
298
|
+
uvCache: join(options.workspace, ".git", "sevro-runtime", "uv-cache"),
|
|
304
299
|
environment: {} as Record<string, string>,
|
|
305
|
-
privateRuntimeRoot:
|
|
300
|
+
privateRuntimeRoot: root,
|
|
301
|
+
writableCacheRoots: [],
|
|
306
302
|
};
|
|
307
303
|
}
|
|
308
304
|
type ShellRuntime = Awaited<ReturnType<typeof shellRuntime>>;
|
|
@@ -326,6 +322,7 @@ function shellEnvironment(
|
|
|
326
322
|
): Record<string, string> {
|
|
327
323
|
return {
|
|
328
324
|
...runtime.environment,
|
|
325
|
+
...candidateArtifactEnvironment(options.candidateArtifactRoots),
|
|
329
326
|
...(options.candidateTranscriptRoot
|
|
330
327
|
? { SEVRO_CANDIDATE_TRANSCRIPTS: options.candidateTranscriptRoot }
|
|
331
328
|
: {}),
|
|
@@ -362,30 +359,39 @@ function shellStdout(
|
|
|
362
359
|
}
|
|
363
360
|
class ShellExecution {
|
|
364
361
|
private proc?: Bun.Subprocess;
|
|
365
|
-
private
|
|
366
|
-
private cancel?: () => void;
|
|
362
|
+
private readonly deadline: ProcessDeadline;
|
|
367
363
|
constructor(
|
|
368
364
|
private check: PreparedShellCheck,
|
|
369
365
|
private options: ShellRunOptions,
|
|
370
366
|
private isolation: Isolation,
|
|
371
367
|
private runtime: ShellRuntime,
|
|
372
|
-
) {
|
|
368
|
+
) {
|
|
369
|
+
this.deadline = new ProcessDeadline({
|
|
370
|
+
timeoutMs: check.timeoutMs,
|
|
371
|
+
signal: options.signal,
|
|
372
|
+
interruption: (reason) =>
|
|
373
|
+
new Error(
|
|
374
|
+
reason === "timeout"
|
|
375
|
+
? "shell check timed out"
|
|
376
|
+
: "shell check cancelled",
|
|
377
|
+
),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
373
380
|
async run(): Promise<ShellCheckResult> {
|
|
374
381
|
try {
|
|
375
382
|
const running = this.spawn();
|
|
376
383
|
const stream = shellStdout(this.check, running.stdout);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return await Promise.race([completed, timeout, aborted]);
|
|
384
|
+
return await this.deadline.wait(() =>
|
|
385
|
+
Promise.all([
|
|
386
|
+
running.exited,
|
|
387
|
+
stream ? boundedOutput(stream) : Promise.resolve(null),
|
|
388
|
+
]).then(([exitCode, stdout]) => ({ exitCode, stdout })),
|
|
389
|
+
);
|
|
384
390
|
} catch (error) {
|
|
385
391
|
await this.stop();
|
|
386
392
|
throw error;
|
|
387
393
|
} finally {
|
|
388
|
-
this.
|
|
394
|
+
this.deadline.dispose();
|
|
389
395
|
await this.isolation.release();
|
|
390
396
|
}
|
|
391
397
|
}
|
|
@@ -400,35 +406,12 @@ class ShellExecution {
|
|
|
400
406
|
});
|
|
401
407
|
return this.proc;
|
|
402
408
|
}
|
|
403
|
-
private deadline(): Promise<never> {
|
|
404
|
-
return new Promise((_resolve, reject) => {
|
|
405
|
-
this.timer = setTimeout(() => {
|
|
406
|
-
reject(new Error("shell check timed out"));
|
|
407
|
-
}, this.check.timeoutMs);
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
private cancellation(): Promise<never> {
|
|
411
|
-
return new Promise((_resolve, reject) => {
|
|
412
|
-
const signal = this.options.signal;
|
|
413
|
-
if (!signal) return;
|
|
414
|
-
this.cancel = () => {
|
|
415
|
-
reject(new Error("shell check cancelled"));
|
|
416
|
-
};
|
|
417
|
-
if (signal.aborted) this.cancel();
|
|
418
|
-
else signal.addEventListener("abort", this.cancel, { once: true });
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
409
|
private async stop(): Promise<void> {
|
|
422
410
|
const proc = this.proc;
|
|
423
411
|
if (!proc) return;
|
|
424
|
-
|
|
412
|
+
terminateOwnedProcessGroup(proc);
|
|
425
413
|
await proc.exited;
|
|
426
414
|
}
|
|
427
|
-
private releaseListeners(): void {
|
|
428
|
-
if (this.timer) clearTimeout(this.timer);
|
|
429
|
-
if (this.cancel)
|
|
430
|
-
this.options.signal?.removeEventListener("abort", this.cancel);
|
|
431
|
-
}
|
|
432
415
|
}
|
|
433
416
|
|
|
434
417
|
/** Run a check without inherited credentials or retaining raw process output. */
|
|
@@ -445,16 +428,22 @@ export async function runShellCheck(
|
|
|
445
428
|
protectedRootsCanonical: options.protectedRootsCanonical,
|
|
446
429
|
privateStateRoot: options.privateStateRoot,
|
|
447
430
|
denyNetwork: true,
|
|
431
|
+
localHttp: runtimeLocalHttpEnabled(options.runtimePolicy, "checks"),
|
|
448
432
|
readOnlyRoots: options.runtimePolicy?.readOnlyRoots,
|
|
433
|
+
candidateArtifactRoots: options.candidateArtifactRoots,
|
|
449
434
|
transcriptReadRoots: options.candidateTranscriptRoot
|
|
450
435
|
? [options.candidateTranscriptRoot]
|
|
451
436
|
: [],
|
|
452
437
|
writableRuntimeRoot: runtime.privateRuntimeRoot,
|
|
438
|
+
writableCacheRoots: runtime.writableCacheRoots,
|
|
453
439
|
});
|
|
454
440
|
await requireShellRuntimeCache(options, runtime);
|
|
455
441
|
return new ShellExecution(check, options, isolated, runtime).run();
|
|
456
442
|
}
|
|
457
443
|
|
|
458
444
|
function shellSeedSources(options: ShellRunOptions): string[] {
|
|
459
|
-
return
|
|
445
|
+
return [
|
|
446
|
+
...runtimeSeedSources(options.runtimePolicy),
|
|
447
|
+
...sharedCacheDenyRoots(options.runtimePolicy, "checks"),
|
|
448
|
+
];
|
|
460
449
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { isRecord } from "./value-guards";
|
|
3
|
+
|
|
4
|
+
function credentialStrings(value: unknown): string[] {
|
|
5
|
+
const pending: unknown[] = [value];
|
|
6
|
+
const secrets: string[] = [];
|
|
7
|
+
for (let visited = 0; visited < 256; visited++) {
|
|
8
|
+
const item = pending.pop();
|
|
9
|
+
if (typeof item === "string") {
|
|
10
|
+
if (item.length >= 8) secrets.push(item);
|
|
11
|
+
} else if (isRecord(item)) pending.push(...Object.values(item));
|
|
12
|
+
}
|
|
13
|
+
return secrets;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Read credentials only for scrubbing; failures belong to the actual host preparation. */
|
|
17
|
+
export async function diagnosticSecrets(path: string): Promise<string[]> {
|
|
18
|
+
try {
|
|
19
|
+
if ((await stat(path)).size > 1024 * 1024) return [];
|
|
20
|
+
const text = await readFile(path, "utf8");
|
|
21
|
+
try {
|
|
22
|
+
const value: unknown = JSON.parse(text);
|
|
23
|
+
return credentialStrings(value);
|
|
24
|
+
} catch {
|
|
25
|
+
return text.trim() ? [text.trim()] : [];
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function redactDiagnosticText(text: string, secrets: string[]): string {
|
|
33
|
+
for (const secret of secrets) {
|
|
34
|
+
text = text.replaceAll(secret, "[redacted]");
|
|
35
|
+
text = redactCredentialSuffix(text, secret);
|
|
36
|
+
}
|
|
37
|
+
return text
|
|
38
|
+
.replace(/(authorization\s*:\s*)[^\r\n]+/gi, "$1[redacted]")
|
|
39
|
+
.replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
|
|
40
|
+
.replace(/\bsk-[A-Za-z0-9_-]+/g, "[redacted]")
|
|
41
|
+
.replace(
|
|
42
|
+
/((?:api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[=:]\s*)\S+/gi,
|
|
43
|
+
"$1[redacted]",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function redactCredentialSuffix(text: string, secret: string): string {
|
|
48
|
+
const start = text.lastIndexOf(secret.slice(0, Math.min(8, secret.length)));
|
|
49
|
+
if (start >= 0 && secret.startsWith(text.slice(start)))
|
|
50
|
+
return text.slice(0, start) + "[redacted]";
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import type { HostResult } from "./evaluation/types";
|
|
2
|
+
import type { RuntimeRole } from "./runtime/state";
|
|
3
|
+
import {
|
|
4
|
+
diagnosticSecrets,
|
|
5
|
+
redactDiagnosticText,
|
|
6
|
+
} from "./host-diagnostic-redaction";
|
|
7
|
+
|
|
8
|
+
const MAX_TEXT_BYTES = 64 * 1024;
|
|
9
|
+
interface ProcessDiagnostic {
|
|
10
|
+
code: number | null;
|
|
11
|
+
out: string;
|
|
12
|
+
stderr: Uint8Array;
|
|
13
|
+
stderrTruncated: boolean;
|
|
14
|
+
signal: string | null;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
stdoutTruncated?: boolean;
|
|
17
|
+
reason: "exit" | "timeout" | "cancelled" | "error" | "spawn";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ProcessFailureReason = ProcessDiagnostic["reason"];
|
|
21
|
+
|
|
22
|
+
export class HostDiagnosticPersistenceError extends Error {
|
|
23
|
+
constructor(persistenceCause: unknown, hostCause: unknown) {
|
|
24
|
+
super("host diagnostic persistence failed", {
|
|
25
|
+
cause: new AggregateError(
|
|
26
|
+
[hostCause, persistenceCause],
|
|
27
|
+
"host and persistence failures",
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function rethrowDiagnosticPersistenceError(cause: unknown): void {
|
|
34
|
+
if (cause instanceof HostDiagnosticPersistenceError) throw cause;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Drain observers capture only a prefix; completion records actual native termination. */
|
|
38
|
+
export class HostProcessDiagnostics {
|
|
39
|
+
readonly stdout = new DiagnosticStream();
|
|
40
|
+
readonly stderr = new DiagnosticStream();
|
|
41
|
+
private started = performance.now();
|
|
42
|
+
constructor(private diagnostics: HostDiagnostics) {}
|
|
43
|
+
recordUnstarted(): void {
|
|
44
|
+
if (!this.diagnostics.enabled) return;
|
|
45
|
+
this.diagnostics.process({
|
|
46
|
+
code: null,
|
|
47
|
+
signal: null,
|
|
48
|
+
durationMs: performance.now() - this.started,
|
|
49
|
+
out: "",
|
|
50
|
+
stderr: new Uint8Array(),
|
|
51
|
+
stderrTruncated: false,
|
|
52
|
+
reason: "spawn",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async record(
|
|
56
|
+
process: { exited: Promise<number | null>; signalCode?: string | null },
|
|
57
|
+
reason: ProcessFailureReason = "exit",
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (!this.diagnostics.enabled) return;
|
|
60
|
+
this.diagnostics.process({
|
|
61
|
+
code: await process.exited,
|
|
62
|
+
signal: process.signalCode ?? null,
|
|
63
|
+
durationMs: performance.now() - this.started,
|
|
64
|
+
out: this.stdout.bytes().toString("utf8"),
|
|
65
|
+
stdoutTruncated: this.stdout.truncated,
|
|
66
|
+
stderr: this.stderr.bytes(),
|
|
67
|
+
stderrTruncated: this.stderr.truncated,
|
|
68
|
+
reason,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function boundedText(bytes: Uint8Array, secrets: string[]) {
|
|
74
|
+
const redacted = Buffer.from(
|
|
75
|
+
redactDiagnosticText(
|
|
76
|
+
Buffer.from(bytes.subarray(0, MAX_TEXT_BYTES)).toString("utf8"),
|
|
77
|
+
secrets,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
return {
|
|
81
|
+
text: new TextDecoder().decode(redacted.subarray(0, MAX_TEXT_BYTES), {
|
|
82
|
+
stream: true,
|
|
83
|
+
}),
|
|
84
|
+
truncated:
|
|
85
|
+
bytes.byteLength > MAX_TEXT_BYTES || redacted.byteLength > MAX_TEXT_BYTES,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Keep a byte-bounded prefix while the owning transport continues draining. */
|
|
90
|
+
export class DiagnosticStream {
|
|
91
|
+
private chunks: Uint8Array[] = [];
|
|
92
|
+
private size = 0;
|
|
93
|
+
truncated = false;
|
|
94
|
+
add(bytes: Uint8Array): void {
|
|
95
|
+
const retained = bytes.subarray(0, Math.max(0, MAX_TEXT_BYTES - this.size));
|
|
96
|
+
if (retained.byteLength) this.chunks.push(Buffer.from(retained));
|
|
97
|
+
this.size += retained.byteLength;
|
|
98
|
+
this.truncated ||= retained.byteLength !== bytes.byteLength;
|
|
99
|
+
}
|
|
100
|
+
bytes(): Buffer {
|
|
101
|
+
return Buffer.concat(this.chunks);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function errorCode(cause: Error): { code?: string | number } {
|
|
106
|
+
if (!("code" in cause)) return {};
|
|
107
|
+
const code: unknown = cause.code;
|
|
108
|
+
return typeof code === "string" || typeof code === "number" ? { code } : {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function errorEvidence(
|
|
112
|
+
cause: unknown,
|
|
113
|
+
secrets: string[],
|
|
114
|
+
): Record<string, unknown> {
|
|
115
|
+
return errorChain(cause, secrets, new Set());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function errorChain(
|
|
119
|
+
cause: unknown,
|
|
120
|
+
secrets: string[],
|
|
121
|
+
seen: Set<Error>,
|
|
122
|
+
): Record<string, unknown> {
|
|
123
|
+
if (!(cause instanceof Error))
|
|
124
|
+
return {
|
|
125
|
+
name: "NonError",
|
|
126
|
+
message: redactDiagnosticText(String(cause).slice(0, 8192), secrets),
|
|
127
|
+
};
|
|
128
|
+
if (seen.has(cause) || seen.size === 8) return { truncated: true };
|
|
129
|
+
seen.add(cause);
|
|
130
|
+
return {
|
|
131
|
+
...errorDetails(cause, secrets),
|
|
132
|
+
...(cause.cause === undefined
|
|
133
|
+
? {}
|
|
134
|
+
: { cause: errorChain(cause.cause, secrets, seen) }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function errorDetails(cause: Error, secrets: string[]) {
|
|
139
|
+
return {
|
|
140
|
+
name: redactDiagnosticText(cause.name.slice(0, 200), secrets),
|
|
141
|
+
message: redactDiagnosticText(cause.message.slice(0, 8192), secrets),
|
|
142
|
+
stack: redactDiagnosticText(cause.stack?.slice(0, 8192) ?? "", secrets),
|
|
143
|
+
...errorCode(cause),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A cleanup exception must not replace an already captured host/persistence failure. */
|
|
148
|
+
export async function cleanupNativeState(
|
|
149
|
+
diagnostics: HostDiagnostics,
|
|
150
|
+
cleanup: () => Promise<void>,
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
beginNativeCleanup(diagnostics);
|
|
153
|
+
try {
|
|
154
|
+
await cleanup();
|
|
155
|
+
} catch (cause) {
|
|
156
|
+
if (!diagnostics.failureCaptured) throw cause;
|
|
157
|
+
console.warn(
|
|
158
|
+
"native state cleanup failed; original host failure preserved",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function beginNativeCleanup(diagnostics: HostDiagnostics): void {
|
|
164
|
+
if (!diagnostics.failureCaptured) diagnostics.stage = "cleanup";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function diagnosticConfiguration(enabled: boolean | undefined) {
|
|
168
|
+
return enabled ? { "sevro.host.retain-diagnostics": true } : {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createHostDiagnostics(
|
|
172
|
+
host: "codex" | "claude",
|
|
173
|
+
enabled: boolean | undefined,
|
|
174
|
+
request: {
|
|
175
|
+
runtimeRole?: RuntimeRole;
|
|
176
|
+
retainHostDiagnostics?: (bytes: Uint8Array) => Promise<void>;
|
|
177
|
+
},
|
|
178
|
+
): HostDiagnostics {
|
|
179
|
+
return enabled
|
|
180
|
+
? new RetainedHostDiagnostics(
|
|
181
|
+
host,
|
|
182
|
+
request.runtimeRole,
|
|
183
|
+
request.retainHostDiagnostics,
|
|
184
|
+
)
|
|
185
|
+
: new DisabledHostDiagnostics();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function diagnoseHostRequest(
|
|
189
|
+
diagnostics: HostDiagnostics,
|
|
190
|
+
execute: () => Promise<HostResult>,
|
|
191
|
+
): Promise<HostResult> {
|
|
192
|
+
try {
|
|
193
|
+
return await execute();
|
|
194
|
+
} catch (cause) {
|
|
195
|
+
if (cause instanceof HostDiagnosticPersistenceError) throw cause;
|
|
196
|
+
if (!diagnostics.enabled) throw cause;
|
|
197
|
+
const result: HostResult = {
|
|
198
|
+
finalMessage: null,
|
|
199
|
+
complete: false,
|
|
200
|
+
executionFailed: true,
|
|
201
|
+
actualCondition: "passive",
|
|
202
|
+
};
|
|
203
|
+
await diagnostics.retain(result, cause);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface HostDiagnostics {
|
|
209
|
+
readonly enabled: boolean;
|
|
210
|
+
stage: string;
|
|
211
|
+
failure: unknown;
|
|
212
|
+
readonly failureCaptured: boolean;
|
|
213
|
+
readCredentials(path: string | undefined): Promise<void>;
|
|
214
|
+
registerCredentials(
|
|
215
|
+
environment: Record<string, string>,
|
|
216
|
+
names: readonly string[],
|
|
217
|
+
): void;
|
|
218
|
+
process(result: ProcessDiagnostic): void;
|
|
219
|
+
retain(result: HostResult, cause: unknown): Promise<void>;
|
|
220
|
+
retainFailure(result: HostResult, message: string): Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class DisabledHostDiagnostics implements HostDiagnostics {
|
|
224
|
+
readonly enabled = false;
|
|
225
|
+
stage = "preparation";
|
|
226
|
+
failure: unknown;
|
|
227
|
+
readonly failureCaptured = false;
|
|
228
|
+
|
|
229
|
+
readCredentials(): Promise<void> {
|
|
230
|
+
return Promise.resolve();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
registerCredentials(): void {}
|
|
234
|
+
|
|
235
|
+
process(): void {}
|
|
236
|
+
|
|
237
|
+
retain(): Promise<void> {
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
retainFailure(): Promise<void> {
|
|
242
|
+
return Promise.resolve();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Private failure evidence, independent of native sessions and transcript access. */
|
|
247
|
+
class RetainedHostDiagnostics implements HostDiagnostics {
|
|
248
|
+
readonly enabled = true;
|
|
249
|
+
stage = "preparation";
|
|
250
|
+
failure: unknown;
|
|
251
|
+
failureCaptured = false;
|
|
252
|
+
private processes: Record<string, unknown>[] = [];
|
|
253
|
+
private processesDropped = 0;
|
|
254
|
+
private secrets: string[] = [];
|
|
255
|
+
constructor(
|
|
256
|
+
private host: "codex" | "claude",
|
|
257
|
+
private role: RuntimeRole = "candidate",
|
|
258
|
+
private persist?: (bytes: Uint8Array) => Promise<void>,
|
|
259
|
+
) {}
|
|
260
|
+
|
|
261
|
+
async readCredentials(path: string | undefined): Promise<void> {
|
|
262
|
+
if (path) this.secrets.push(...(await diagnosticSecrets(path)));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
registerCredentials(
|
|
266
|
+
environment: Record<string, string>,
|
|
267
|
+
names: readonly string[],
|
|
268
|
+
): void {
|
|
269
|
+
for (const name of names) {
|
|
270
|
+
const value = environment[name];
|
|
271
|
+
if (value) this.secrets.push(value);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
process(result: ProcessDiagnostic): void {
|
|
276
|
+
const stdout = boundedText(Buffer.from(result.out), this.secrets);
|
|
277
|
+
const stderr = boundedText(result.stderr, this.secrets);
|
|
278
|
+
this.addProcess({
|
|
279
|
+
stage: this.stage,
|
|
280
|
+
code: result.code,
|
|
281
|
+
signal: result.signal,
|
|
282
|
+
durationMs: result.durationMs,
|
|
283
|
+
reason: result.reason,
|
|
284
|
+
stdout: stdout.text,
|
|
285
|
+
stdoutTruncated: stdout.truncated || result.stdoutTruncated === true,
|
|
286
|
+
stderr: stderr.text,
|
|
287
|
+
stderrTruncated: stderr.truncated || result.stderrTruncated,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private addProcess(receipt: Record<string, unknown>): void {
|
|
292
|
+
this.processes.push(receipt);
|
|
293
|
+
while (
|
|
294
|
+
this.processes.length > 32 ||
|
|
295
|
+
Buffer.byteLength(JSON.stringify(this.processes)) > 2 * 1024 * 1024
|
|
296
|
+
) {
|
|
297
|
+
this.processes.shift();
|
|
298
|
+
this.processesDropped++;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async retainFailure(result: HostResult, message: string): Promise<void> {
|
|
303
|
+
if (result.executionFailed)
|
|
304
|
+
await this.retain(result, this.failure ?? new Error(message));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async retain(result: HostResult, cause: unknown): Promise<void> {
|
|
308
|
+
this.failureCaptured = true;
|
|
309
|
+
const bytes = Buffer.from(
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
format: "sevro.host-diagnostics.v1",
|
|
312
|
+
host: this.host,
|
|
313
|
+
role: this.role,
|
|
314
|
+
stage: this.stage,
|
|
315
|
+
error: errorEvidence(cause, this.secrets),
|
|
316
|
+
processes: this.processes,
|
|
317
|
+
processesDropped: this.processesDropped,
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
if (this.persist) {
|
|
321
|
+
try {
|
|
322
|
+
await this.persist(bytes);
|
|
323
|
+
} catch (persistenceCause) {
|
|
324
|
+
throw new HostDiagnosticPersistenceError(persistenceCause, cause);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
result.artifacts ??= [];
|
|
329
|
+
result.artifacts.push({ id: "sevro.host.diagnostics", bytes });
|
|
330
|
+
}
|
|
331
|
+
}
|