@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +170 -0
- package/LICENSE +63 -136
- package/README.md +83 -66
- package/docs/README.md +44 -0
- package/docs/acknowledgements.md +14 -0
- package/docs/architecture.md +146 -0
- package/docs/assets/README.md +27 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +1 -0
- package/docs/assets/sevro-wordmark.svg +1 -0
- package/docs/builtin-graders-v1.md +41 -13
- package/docs/codex-host.md +184 -52
- package/docs/contracts.md +19 -0
- package/docs/creating-cases.md +139 -0
- package/docs/development-cli.md +190 -30
- package/docs/documentation-quality.md +61 -0
- package/docs/evaluation-workflow.md +110 -0
- package/docs/evidence/code-mode-controls-71.md +64 -0
- package/docs/evidence/code-mode-programs-85.md +64 -0
- package/docs/evidence/codex-state-73/README.md +204 -0
- package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
- package/docs/evidence/codex-state-73/results.json +661 -0
- package/docs/evidence/local-http-74.md +104 -0
- package/docs/evidence/runtime-startup-30/README.md +185 -0
- package/docs/evidence/runtime-startup-30/after.json +319 -0
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
- package/docs/evidence/runtime-startup-30/before.json +403 -0
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
- package/docs/evidence/skill-reads-70/results.json +823 -0
- package/docs/evidence.md +21 -0
- package/docs/extension-protocol-v1.md +50 -16
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/getting-started.md +102 -0
- package/docs/guide-evaluation.md +89 -0
- package/docs/guide-verification.json +239 -0
- package/docs/identity-v1.md +35 -1
- package/docs/installing.md +88 -0
- package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
- package/docs/licensing.md +42 -0
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +228 -0
- package/docs/reading-results.md +141 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +81 -40
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +180 -0
- package/docs/run-settings-v1.md +147 -0
- package/docs/running-evaluations.md +31 -0
- package/docs/runtime-v1.md +614 -0
- package/docs/specs/repository-guide.md +58 -0
- package/docs/troubleshooting.md +30 -0
- package/docs/typescript-quality.md +234 -0
- package/docs/usability-study.md +114 -0
- package/examples/basic/host.ts +2 -2
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +39 -6
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +193 -0
- package/src/advisory.ts +10 -5
- package/src/candidate-artifacts.ts +208 -0
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +105 -0
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +81 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +59 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +222 -0
- package/src/cli/hosts.ts +59 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/cli/invocation.ts +627 -0
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +163 -0
- package/src/cli/presentation.ts +91 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +133 -0
- package/src/cli/run-options.ts +101 -0
- package/src/cli/run-overrides.ts +83 -0
- package/src/cli/settings.ts +160 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -843
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +19 -2373
- package/src/evaluation/advisory.ts +150 -0
- package/src/evaluation/check-plan.ts +510 -0
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/evaluation/grading-host.ts +39 -0
- package/src/evaluation/grading.ts +258 -0
- package/src/evaluation/invocation-plan.ts +548 -0
- package/src/evaluation/plan.ts +736 -0
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
- package/src/evaluation/semantic.ts +312 -0
- package/src/evaluation/trial.ts +866 -0
- package/src/evaluation/types.ts +207 -0
- package/src/evidence/host-results.ts +92 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +228 -148
- package/src/extension-session.ts +326 -195
- package/src/fixtures/advisory.ts +338 -0
- package/src/fixtures/bin.ts +57 -0
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +187 -0
- package/src/fixtures/generated.ts +300 -0
- package/src/fixtures/git-hooks.ts +24 -0
- package/src/fixtures/preparation.ts +270 -0
- package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
- package/src/fixtures/setup.ts +224 -0
- package/src/fixtures/tools.ts +88 -0
- package/src/fixtures/workspace-state.ts +212 -0
- package/src/generated/cli-result.cjs +2 -0
- package/src/generated/cli-result.d.cts +3 -0
- package/src/generated/extension.cjs +2 -0
- package/src/generated/extension.d.cts +3 -0
- package/src/generated/report.cjs +2 -0
- package/src/generated/report.d.cts +3 -0
- package/src/generated/run-evidence.cjs +2 -0
- package/src/generated/run-evidence.d.cts +3 -0
- package/src/generated/runtime.cjs +2 -0
- package/src/generated/runtime.d.cts +3 -0
- package/src/graders/artifact.ts +56 -23
- package/src/graders/git-head.ts +79 -75
- package/src/graders/output.ts +222 -140
- package/src/graders/semantic.ts +102 -43
- package/src/graders/shell.ts +379 -207
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +319 -0
- package/src/hosts/claude-continuation.ts +63 -18
- package/src/hosts/claude-credential.ts +43 -62
- package/src/hosts/claude-events.ts +86 -51
- package/src/hosts/claude-keychain.ts +59 -0
- package/src/hosts/claude-native-goal.ts +57 -33
- package/src/hosts/claude-process.ts +111 -0
- package/src/hosts/claude-repository-invocation.ts +208 -117
- package/src/hosts/claude-settings.ts +240 -21
- package/src/hosts/claude-skill-activations.ts +166 -0
- package/src/hosts/claude-state.ts +403 -0
- package/src/hosts/claude-tool-calls.ts +171 -106
- package/src/hosts/claude.ts +561 -451
- package/src/hosts/codex-app-server.ts +326 -178
- package/src/hosts/codex-configuration.ts +44 -18
- package/src/hosts/codex-events.ts +97 -60
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-invocation.ts +70 -0
- package/src/hosts/codex-marketplace.ts +283 -0
- package/src/hosts/codex-native-calls.ts +546 -500
- package/src/hosts/codex-process.ts +202 -0
- package/src/hosts/codex-profile.ts +257 -28
- package/src/hosts/codex-session-files.ts +106 -0
- package/src/hosts/codex-state.ts +492 -0
- package/src/hosts/codex.ts +783 -832
- package/src/hosts/isolation-common.ts +94 -0
- package/src/hosts/isolation-roots.ts +54 -27
- package/src/hosts/isolation.ts +10 -0
- package/src/hosts/linux-sandbox.ts +159 -0
- package/src/hosts/mac-sandbox.ts +187 -61
- package/src/hosts/native-controls.ts +152 -62
- package/src/hosts/process-lifecycle.ts +37 -0
- package/src/hosts/protected-worktrees.ts +51 -19
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +580 -0
- package/src/hosts/workspace-fingerprint.ts +55 -35
- package/src/identity.ts +102 -48
- package/src/instrumentation.ts +109 -48
- package/src/native-transcript-state.ts +131 -0
- package/src/native-transcript-view.ts +90 -0
- package/src/native-transcripts.ts +286 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +161 -81
- package/src/prune.ts +215 -0
- package/src/report.ts +172 -140
- package/src/resolved-case.ts +33 -0
- package/src/results-lock.ts +29 -0
- package/src/results.ts +133 -86
- package/src/run-owner.ts +154 -63
- package/src/runtime/config.ts +456 -0
- package/src/runtime/discovery-apple.ts +90 -0
- package/src/runtime/discovery-providers.ts +63 -0
- package/src/runtime/discovery.ts +70 -0
- package/src/runtime/hook-processes.ts +97 -0
- package/src/runtime/hooks.ts +476 -0
- package/src/runtime/paths.ts +313 -0
- package/src/runtime/query.ts +85 -0
- package/src/runtime/seeds.ts +134 -0
- package/src/runtime/shared-cache.ts +143 -0
- package/src/runtime/state.ts +143 -0
- package/src/schema-types.ts +198 -0
- package/src/schema.ts +15 -19
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +20 -0
- package/src/advisory-fixture.ts +0 -282
- package/src/fixture-bin.ts +0 -87
- package/src/fixture-setup.ts +0 -163
- package/src/generated-fixture.ts +0 -220
- package/src/git-hooks.ts +0 -63
- package/src/hosts/claude-nested-skills.ts +0 -218
- package/src/hosts/codex-command-output.ts +0 -399
- package/src/hosts/codex-executor-command.ts +0 -195
- package/src/hosts/codex-skill-reads.ts +0 -391
- package/src/preparation.ts +0 -211
package/src/graders/shell.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
import { runtimeLocalHttpEnabled, runtimeSeedSources } from "../runtime/config";
|
|
2
|
+
import {
|
|
3
|
+
candidateArtifactEnvironment,
|
|
4
|
+
type CandidateArtifactRoots,
|
|
5
|
+
} from "../candidate-artifacts";
|
|
1
6
|
import { mkdir, stat } from "node:fs/promises";
|
|
2
7
|
import { delimiter, join } from "node:path";
|
|
3
|
-
import {
|
|
8
|
+
import { prepareIsolatedCommand } from "../hosts/isolation";
|
|
9
|
+
import type { RuntimePolicy } from "../runtime/config";
|
|
10
|
+
import { prepareRuntimeState } from "../runtime/state";
|
|
11
|
+
import { sharedCacheDenyRoots } from "../runtime/shared-cache";
|
|
12
|
+
import { hashJson } from "../identity";
|
|
13
|
+
import {
|
|
14
|
+
captureBoundedText,
|
|
15
|
+
ProcessDeadline,
|
|
16
|
+
terminateOwnedProcessGroup,
|
|
17
|
+
} from "../process-lifecycle";
|
|
4
18
|
|
|
5
19
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
20
|
const MAX_TIMEOUT_MS = 120_000;
|
|
@@ -23,91 +37,141 @@ export interface PreparedShellCheck {
|
|
|
23
37
|
captureStdout: boolean;
|
|
24
38
|
}
|
|
25
39
|
|
|
40
|
+
function requireShellDeclaration(
|
|
41
|
+
id: string,
|
|
42
|
+
value: unknown,
|
|
43
|
+
): asserts value is Record<string, unknown> {
|
|
44
|
+
if (!id || !value || typeof value !== "object")
|
|
45
|
+
throw new Error("invalid shell check declaration");
|
|
46
|
+
}
|
|
47
|
+
function requireShellKeys(config: Record<string, unknown>): void {
|
|
48
|
+
const allowed = [
|
|
49
|
+
"run",
|
|
50
|
+
"expectedExitCode",
|
|
51
|
+
"timeoutMs",
|
|
52
|
+
"expectExact",
|
|
53
|
+
"expectRegex",
|
|
54
|
+
"notRegex",
|
|
55
|
+
"flags",
|
|
56
|
+
];
|
|
57
|
+
if (Object.keys(config).some((key) => !allowed.includes(key)))
|
|
58
|
+
throw new Error("unsupported shell check configuration");
|
|
59
|
+
}
|
|
60
|
+
function requireShellCommand(run: unknown): asserts run is string {
|
|
61
|
+
if (typeof run !== "string" || !run.trim() || run.length > 4096)
|
|
62
|
+
throw new Error("shell check requires a bounded command");
|
|
63
|
+
}
|
|
64
|
+
function requireExpectedExit(code: unknown): asserts code is number {
|
|
65
|
+
if (
|
|
66
|
+
typeof code !== "number" ||
|
|
67
|
+
!Number.isSafeInteger(code) ||
|
|
68
|
+
code < 0 ||
|
|
69
|
+
code > 255
|
|
70
|
+
)
|
|
71
|
+
throw new Error("invalid expected shell exit code");
|
|
72
|
+
}
|
|
73
|
+
function requireShellTimeout(timeout: unknown): asserts timeout is number {
|
|
74
|
+
if (
|
|
75
|
+
typeof timeout !== "number" ||
|
|
76
|
+
!Number.isSafeInteger(timeout) ||
|
|
77
|
+
timeout < 1 ||
|
|
78
|
+
timeout > MAX_TIMEOUT_MS
|
|
79
|
+
)
|
|
80
|
+
throw new Error("invalid shell check timeout");
|
|
81
|
+
}
|
|
82
|
+
function requireExactShellOutput(
|
|
83
|
+
output: unknown,
|
|
84
|
+
): asserts output is string | undefined {
|
|
85
|
+
if (
|
|
86
|
+
output !== undefined &&
|
|
87
|
+
(typeof output !== "string" ||
|
|
88
|
+
Buffer.byteLength(output, "utf8") > MAX_OUTPUT_BYTES)
|
|
89
|
+
)
|
|
90
|
+
throw new Error("invalid exact shell output expectation");
|
|
91
|
+
}
|
|
92
|
+
function requireShellFlags(flags: unknown): asserts flags is string {
|
|
93
|
+
if (
|
|
94
|
+
flags !== "" &&
|
|
95
|
+
(typeof flags !== "string" ||
|
|
96
|
+
!/^[isu]*$/.test(flags) ||
|
|
97
|
+
new Set(flags).size !== flags.length)
|
|
98
|
+
)
|
|
99
|
+
throw new Error("invalid shell regex flags");
|
|
100
|
+
}
|
|
101
|
+
function requireFlagPattern(
|
|
102
|
+
flags: string,
|
|
103
|
+
expected: unknown,
|
|
104
|
+
forbidden: unknown,
|
|
105
|
+
): void {
|
|
106
|
+
if (flags && expected === undefined && forbidden === undefined)
|
|
107
|
+
throw new Error("shell regex flags require a pattern");
|
|
108
|
+
}
|
|
109
|
+
function shellOptions(config: Record<string, unknown>) {
|
|
110
|
+
const {
|
|
111
|
+
run,
|
|
112
|
+
expectedExitCode = 0,
|
|
113
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
114
|
+
expectExact,
|
|
115
|
+
expectRegex,
|
|
116
|
+
notRegex,
|
|
117
|
+
flags = "",
|
|
118
|
+
} = config;
|
|
119
|
+
requireShellCommand(run);
|
|
120
|
+
requireExpectedExit(expectedExitCode);
|
|
121
|
+
requireShellTimeout(timeoutMs);
|
|
122
|
+
requireExactShellOutput(expectExact);
|
|
123
|
+
requireShellFlags(flags);
|
|
124
|
+
requireFlagPattern(flags, expectRegex, notRegex);
|
|
125
|
+
return {
|
|
126
|
+
run,
|
|
127
|
+
expectedExitCode,
|
|
128
|
+
timeoutMs,
|
|
129
|
+
expectExact,
|
|
130
|
+
expectRegex,
|
|
131
|
+
notRegex,
|
|
132
|
+
flags,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function compileShellPattern(
|
|
136
|
+
value: unknown,
|
|
137
|
+
flags: string,
|
|
138
|
+
): RegExp | undefined {
|
|
139
|
+
if (value === undefined) return undefined;
|
|
140
|
+
if (typeof value !== "string" || value.length > 4096)
|
|
141
|
+
throw new Error("invalid shell regex pattern");
|
|
142
|
+
try {
|
|
143
|
+
return new RegExp(value, `m${flags}`);
|
|
144
|
+
} catch (cause) {
|
|
145
|
+
throw new Error("invalid shell regex pattern or flags", { cause });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function prepareShellCheck({
|
|
149
|
+
id,
|
|
150
|
+
configuration,
|
|
151
|
+
}: ShellCheckDeclaration): PreparedShellCheck {
|
|
152
|
+
requireShellDeclaration(id, configuration);
|
|
153
|
+
requireShellKeys(configuration);
|
|
154
|
+
const config = shellOptions(configuration);
|
|
155
|
+
const expectRegex = compileShellPattern(config.expectRegex, config.flags),
|
|
156
|
+
notRegex = compileShellPattern(config.notRegex, config.flags);
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
run: config.run,
|
|
160
|
+
expectedExitCode: config.expectedExitCode,
|
|
161
|
+
timeoutMs: config.timeoutMs,
|
|
162
|
+
expectExact: config.expectExact,
|
|
163
|
+
expectRegex,
|
|
164
|
+
notRegex,
|
|
165
|
+
captureStdout:
|
|
166
|
+
config.expectExact !== undefined ||
|
|
167
|
+
expectRegex !== undefined ||
|
|
168
|
+
notRegex !== undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
26
171
|
export function prepareShellChecks(
|
|
27
172
|
declarations: ShellCheckDeclaration[],
|
|
28
173
|
): PreparedShellCheck[] {
|
|
29
|
-
return declarations.map(
|
|
30
|
-
if (!id || !configuration || typeof configuration !== "object")
|
|
31
|
-
throw new Error("invalid shell check declaration");
|
|
32
|
-
if (
|
|
33
|
-
Object.keys(configuration).some(
|
|
34
|
-
(key) =>
|
|
35
|
-
![
|
|
36
|
-
"run",
|
|
37
|
-
"expectedExitCode",
|
|
38
|
-
"timeoutMs",
|
|
39
|
-
"expectExact",
|
|
40
|
-
"expectRegex",
|
|
41
|
-
"notRegex",
|
|
42
|
-
"flags",
|
|
43
|
-
].includes(key),
|
|
44
|
-
)
|
|
45
|
-
)
|
|
46
|
-
throw new Error("unsupported shell check configuration");
|
|
47
|
-
const {
|
|
48
|
-
run,
|
|
49
|
-
expectedExitCode = 0,
|
|
50
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
51
|
-
expectExact,
|
|
52
|
-
expectRegex,
|
|
53
|
-
notRegex,
|
|
54
|
-
flags = "",
|
|
55
|
-
} = configuration;
|
|
56
|
-
if (typeof run !== "string" || !run.trim() || run.length > 4096)
|
|
57
|
-
throw new Error("shell check requires a bounded command");
|
|
58
|
-
if (
|
|
59
|
-
!Number.isSafeInteger(expectedExitCode) ||
|
|
60
|
-
(expectedExitCode as number) < 0 ||
|
|
61
|
-
(expectedExitCode as number) > 255
|
|
62
|
-
)
|
|
63
|
-
throw new Error("invalid expected shell exit code");
|
|
64
|
-
if (
|
|
65
|
-
!Number.isSafeInteger(timeoutMs) ||
|
|
66
|
-
(timeoutMs as number) < 1 ||
|
|
67
|
-
(timeoutMs as number) > MAX_TIMEOUT_MS
|
|
68
|
-
)
|
|
69
|
-
throw new Error("invalid shell check timeout");
|
|
70
|
-
if (
|
|
71
|
-
expectExact !== undefined &&
|
|
72
|
-
(typeof expectExact !== "string" ||
|
|
73
|
-
Buffer.byteLength(expectExact, "utf8") > MAX_OUTPUT_BYTES)
|
|
74
|
-
)
|
|
75
|
-
throw new Error("invalid exact shell output expectation");
|
|
76
|
-
if (
|
|
77
|
-
flags !== "" &&
|
|
78
|
-
(typeof flags !== "string" ||
|
|
79
|
-
!/^[isu]*$/.test(flags) ||
|
|
80
|
-
new Set(flags).size !== flags.length)
|
|
81
|
-
)
|
|
82
|
-
throw new Error("invalid shell regex flags");
|
|
83
|
-
if (flags && expectRegex === undefined && notRegex === undefined)
|
|
84
|
-
throw new Error("shell regex flags require a pattern");
|
|
85
|
-
const pattern = (value: unknown): RegExp | undefined => {
|
|
86
|
-
if (value === undefined) return undefined;
|
|
87
|
-
if (typeof value !== "string" || value.length > 4096)
|
|
88
|
-
throw new Error("invalid shell regex pattern");
|
|
89
|
-
try {
|
|
90
|
-
return new RegExp(value, `m${flags}`);
|
|
91
|
-
} catch {
|
|
92
|
-
throw new Error("invalid shell regex pattern or flags");
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
const expectedPattern = pattern(expectRegex);
|
|
96
|
-
const forbiddenPattern = pattern(notRegex);
|
|
97
|
-
return {
|
|
98
|
-
id,
|
|
99
|
-
run,
|
|
100
|
-
expectedExitCode: expectedExitCode as number,
|
|
101
|
-
timeoutMs: timeoutMs as number,
|
|
102
|
-
expectExact: expectExact as string | undefined,
|
|
103
|
-
expectRegex: expectedPattern,
|
|
104
|
-
notRegex: forbiddenPattern,
|
|
105
|
-
captureStdout:
|
|
106
|
-
expectExact !== undefined ||
|
|
107
|
-
expectedPattern !== undefined ||
|
|
108
|
-
forbiddenPattern !== undefined,
|
|
109
|
-
};
|
|
110
|
-
});
|
|
174
|
+
return declarations.map(prepareShellCheck);
|
|
111
175
|
}
|
|
112
176
|
|
|
113
177
|
export interface ShellCheckResult {
|
|
@@ -125,153 +189,261 @@ export function assessShellCheck(
|
|
|
125
189
|
passed: false,
|
|
126
190
|
detail: `exit code ${result.exitCode} (expected ${check.expectedExitCode})`,
|
|
127
191
|
};
|
|
128
|
-
|
|
129
|
-
if (check.captureStdout && out === null)
|
|
192
|
+
if (check.captureStdout && result.stdout === null)
|
|
130
193
|
throw new Error("shell stdout observation is missing");
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
194
|
+
const detail = shellOutputFailure(check, result.stdout);
|
|
195
|
+
return assessedShellOutput(detail, result.exitCode);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function shellOutputFailure(
|
|
199
|
+
check: PreparedShellCheck,
|
|
200
|
+
out: string | null,
|
|
201
|
+
): string | undefined {
|
|
202
|
+
return (
|
|
203
|
+
exactOutputFailure(check, out) ??
|
|
204
|
+
expectedPatternFailure(check, out) ??
|
|
205
|
+
forbiddenPatternFailure(check, out)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assessedShellOutput(
|
|
210
|
+
detail: string | undefined,
|
|
211
|
+
exitCode: number,
|
|
212
|
+
): { passed: boolean; detail: string } {
|
|
213
|
+
return detail === undefined
|
|
214
|
+
? { passed: true, detail: `exit code ${exitCode}` }
|
|
215
|
+
: { passed: false, detail };
|
|
216
|
+
}
|
|
217
|
+
function exactOutputFailure(
|
|
218
|
+
check: PreparedShellCheck,
|
|
219
|
+
out: string | null,
|
|
220
|
+
): string | undefined {
|
|
221
|
+
if (check.expectExact === undefined) return undefined;
|
|
222
|
+
if (out === null) throw new Error("shell stdout observation is missing");
|
|
223
|
+
const actual = out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
224
|
+
return actual === check.expectExact
|
|
225
|
+
? undefined
|
|
226
|
+
: "exact shell output did not match";
|
|
227
|
+
}
|
|
228
|
+
function expectedPatternFailure(
|
|
229
|
+
check: PreparedShellCheck,
|
|
230
|
+
out: string | null,
|
|
231
|
+
): string | undefined {
|
|
232
|
+
return check.expectRegex && !check.expectRegex.test(String(out))
|
|
233
|
+
? "expected shell output pattern did not match"
|
|
234
|
+
: undefined;
|
|
235
|
+
}
|
|
236
|
+
function forbiddenPatternFailure(
|
|
237
|
+
check: PreparedShellCheck,
|
|
238
|
+
out: string | null,
|
|
239
|
+
): string | undefined {
|
|
240
|
+
return check.notRegex && check.notRegex.test(String(out))
|
|
241
|
+
? "forbidden shell output pattern matched"
|
|
242
|
+
: undefined;
|
|
144
243
|
}
|
|
145
244
|
|
|
146
245
|
async function boundedOutput(
|
|
147
246
|
stream: ReadableStream<Uint8Array>,
|
|
148
247
|
): Promise<string> {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
157
|
-
Buffer.concat(chunks),
|
|
158
|
-
);
|
|
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
|
+
});
|
|
159
254
|
}
|
|
160
255
|
|
|
161
|
-
|
|
162
|
-
|
|
256
|
+
interface ShellRunOptions {
|
|
257
|
+
candidateTranscriptRoot?: string;
|
|
258
|
+
candidateArtifactRoots?: CandidateArtifactRoots;
|
|
259
|
+
runtimePolicy?: RuntimePolicy;
|
|
260
|
+
workspace: string;
|
|
261
|
+
fixtureBinDir?: string;
|
|
262
|
+
toolchainBinDir?: string;
|
|
263
|
+
uvRuntimeCache?: boolean;
|
|
264
|
+
protectedRoots: string[];
|
|
265
|
+
protectedRootsCanonical?: boolean;
|
|
266
|
+
privateStateRoot: string;
|
|
267
|
+
signal?: AbortSignal;
|
|
268
|
+
}
|
|
269
|
+
async function shellRuntime(options: ShellRunOptions) {
|
|
270
|
+
if (options.runtimePolicy) {
|
|
271
|
+
const runtime = await prepareRuntimeState(
|
|
272
|
+
options.workspace,
|
|
273
|
+
options.runtimePolicy,
|
|
274
|
+
"checks",
|
|
275
|
+
join(options.privateStateRoot, "runtime", hashJson(options.workspace)),
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
...runtime,
|
|
279
|
+
uvCache: join(runtime.root, "uv-cache"),
|
|
280
|
+
privateRuntimeRoot: runtime.root,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const root = join(
|
|
284
|
+
options.privateStateRoot,
|
|
285
|
+
"runtime",
|
|
286
|
+
hashJson(options.workspace),
|
|
287
|
+
),
|
|
288
|
+
home = join(root, "check-home"),
|
|
289
|
+
temp = join(root, "check-tmp");
|
|
290
|
+
await Promise.all([
|
|
291
|
+
mkdir(home, { recursive: true, mode: 0o700 }),
|
|
292
|
+
mkdir(temp, { recursive: true, mode: 0o700 }),
|
|
293
|
+
]);
|
|
294
|
+
return {
|
|
295
|
+
root,
|
|
296
|
+
home,
|
|
297
|
+
temp,
|
|
298
|
+
uvCache: join(options.workspace, ".git", "sevro-runtime", "uv-cache"),
|
|
299
|
+
environment: {} as Record<string, string>,
|
|
300
|
+
privateRuntimeRoot: root,
|
|
301
|
+
writableCacheRoots: [],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
type ShellRuntime = Awaited<ReturnType<typeof shellRuntime>>;
|
|
305
|
+
type Isolation = Awaited<ReturnType<typeof prepareIsolatedCommand>>;
|
|
306
|
+
function shellRuntimeEnvironment(
|
|
307
|
+
runtime: ShellRuntime,
|
|
308
|
+
enabled: boolean | undefined,
|
|
309
|
+
): Record<string, string> {
|
|
310
|
+
return enabled
|
|
311
|
+
? {
|
|
312
|
+
UV_CACHE_DIR: runtime.uvCache,
|
|
313
|
+
UV_PROJECT_ENVIRONMENT: join(runtime.root, "project-environment"),
|
|
314
|
+
UV_OFFLINE: "1",
|
|
315
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
316
|
+
}
|
|
317
|
+
: {};
|
|
318
|
+
}
|
|
319
|
+
function shellEnvironment(
|
|
320
|
+
options: ShellRunOptions,
|
|
321
|
+
runtime: ShellRuntime,
|
|
322
|
+
): Record<string, string> {
|
|
323
|
+
return {
|
|
324
|
+
...runtime.environment,
|
|
325
|
+
...candidateArtifactEnvironment(options.candidateArtifactRoots),
|
|
326
|
+
...(options.candidateTranscriptRoot
|
|
327
|
+
? { SEVRO_CANDIDATE_TRANSCRIPTS: options.candidateTranscriptRoot }
|
|
328
|
+
: {}),
|
|
329
|
+
PATH: [
|
|
330
|
+
options.fixtureBinDir,
|
|
331
|
+
options.toolchainBinDir,
|
|
332
|
+
runtime.environment.PATH ?? "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
333
|
+
]
|
|
334
|
+
.filter(Boolean)
|
|
335
|
+
.join(delimiter),
|
|
336
|
+
HOME: runtime.home,
|
|
337
|
+
TMPDIR: runtime.temp,
|
|
338
|
+
LANG: process.env.LANG ?? "C",
|
|
339
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
340
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
341
|
+
...shellRuntimeEnvironment(runtime, options.uvRuntimeCache),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function requireShellRuntimeCache(
|
|
345
|
+
options: ShellRunOptions,
|
|
346
|
+
runtime: ShellRuntime,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
if (options.uvRuntimeCache && !(await stat(runtime.uvCache)).isDirectory())
|
|
349
|
+
throw new Error("isolated UV cache is missing");
|
|
350
|
+
}
|
|
351
|
+
function shellStdout(
|
|
352
|
+
check: PreparedShellCheck,
|
|
353
|
+
output: Bun.Subprocess["stdout"],
|
|
354
|
+
): ReadableStream<Uint8Array> | null {
|
|
355
|
+
if (!check.captureStdout) return null;
|
|
356
|
+
if (!(output instanceof ReadableStream))
|
|
357
|
+
throw new Error("shell stdout pipe is unavailable");
|
|
358
|
+
return output;
|
|
359
|
+
}
|
|
360
|
+
class ShellExecution {
|
|
361
|
+
private proc?: Bun.Subprocess;
|
|
362
|
+
private readonly deadline: ProcessDeadline;
|
|
363
|
+
constructor(
|
|
364
|
+
private check: PreparedShellCheck,
|
|
365
|
+
private options: ShellRunOptions,
|
|
366
|
+
private isolation: Isolation,
|
|
367
|
+
private runtime: ShellRuntime,
|
|
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
|
+
}
|
|
380
|
+
async run(): Promise<ShellCheckResult> {
|
|
163
381
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
382
|
+
const running = this.spawn();
|
|
383
|
+
const stream = shellStdout(this.check, running.stdout);
|
|
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
|
+
);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
await this.stop();
|
|
392
|
+
throw error;
|
|
393
|
+
} finally {
|
|
394
|
+
this.deadline.dispose();
|
|
395
|
+
await this.isolation.release();
|
|
168
396
|
}
|
|
169
397
|
}
|
|
170
|
-
|
|
171
|
-
proc.
|
|
172
|
-
|
|
173
|
-
|
|
398
|
+
private spawn() {
|
|
399
|
+
this.proc = Bun.spawn(this.isolation.argv, {
|
|
400
|
+
cwd: this.options.workspace,
|
|
401
|
+
env: shellEnvironment(this.options, this.runtime),
|
|
402
|
+
detached: true,
|
|
403
|
+
stdin: "ignore",
|
|
404
|
+
stdout: this.check.captureStdout ? "pipe" : "ignore",
|
|
405
|
+
stderr: "ignore",
|
|
406
|
+
});
|
|
407
|
+
return this.proc;
|
|
408
|
+
}
|
|
409
|
+
private async stop(): Promise<void> {
|
|
410
|
+
const proc = this.proc;
|
|
411
|
+
if (!proc) return;
|
|
412
|
+
terminateOwnedProcessGroup(proc);
|
|
413
|
+
await proc.exited;
|
|
174
414
|
}
|
|
175
415
|
}
|
|
176
416
|
|
|
177
417
|
/** Run a check without inherited credentials or retaining raw process output. */
|
|
178
418
|
export async function runShellCheck(
|
|
179
419
|
check: PreparedShellCheck,
|
|
180
|
-
options:
|
|
181
|
-
workspace: string;
|
|
182
|
-
fixtureBinDir?: string;
|
|
183
|
-
toolchainBinDir?: string;
|
|
184
|
-
uvRuntimeCache?: boolean;
|
|
185
|
-
protectedRoots: string[];
|
|
186
|
-
protectedRootsCanonical?: boolean;
|
|
187
|
-
privateStateRoot: string;
|
|
188
|
-
signal?: AbortSignal;
|
|
189
|
-
},
|
|
420
|
+
options: ShellRunOptions,
|
|
190
421
|
): Promise<ShellCheckResult> {
|
|
191
422
|
if (options.signal?.aborted) throw new Error("shell check cancelled");
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const temp = join(runtimeRoot, "check-tmp");
|
|
195
|
-
await Promise.all([
|
|
196
|
-
mkdir(home, { recursive: true, mode: 0o700 }),
|
|
197
|
-
mkdir(temp, { recursive: true, mode: 0o700 }),
|
|
198
|
-
]);
|
|
199
|
-
const isolated = await prepareMacSandboxCommand({
|
|
423
|
+
const runtime = await shellRuntime(options);
|
|
424
|
+
const isolated = await prepareIsolatedCommand({
|
|
200
425
|
argv: ["/bin/sh", "-e", "-c", check.run],
|
|
201
426
|
workspace: options.workspace,
|
|
202
|
-
protectedRoots: options.protectedRoots,
|
|
427
|
+
protectedRoots: [...options.protectedRoots, ...shellSeedSources(options)],
|
|
203
428
|
protectedRootsCanonical: options.protectedRootsCanonical,
|
|
204
429
|
privateStateRoot: options.privateStateRoot,
|
|
205
430
|
denyNetwork: true,
|
|
431
|
+
localHttp: runtimeLocalHttpEnabled(options.runtimePolicy, "checks"),
|
|
432
|
+
readOnlyRoots: options.runtimePolicy?.readOnlyRoots,
|
|
433
|
+
candidateArtifactRoots: options.candidateArtifactRoots,
|
|
434
|
+
transcriptReadRoots: options.candidateTranscriptRoot
|
|
435
|
+
? [options.candidateTranscriptRoot]
|
|
436
|
+
: [],
|
|
437
|
+
writableRuntimeRoot: runtime.privateRuntimeRoot,
|
|
438
|
+
writableCacheRoots: runtime.writableCacheRoots,
|
|
206
439
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
env: {
|
|
217
|
-
PATH: [
|
|
218
|
-
options.fixtureBinDir,
|
|
219
|
-
options.toolchainBinDir,
|
|
220
|
-
"/usr/bin:/bin:/usr/sbin:/sbin",
|
|
221
|
-
]
|
|
222
|
-
.filter(Boolean)
|
|
223
|
-
.join(delimiter),
|
|
224
|
-
HOME: home,
|
|
225
|
-
TMPDIR: temp,
|
|
226
|
-
LANG: process.env.LANG ?? "C",
|
|
227
|
-
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
228
|
-
GIT_CONFIG_NOSYSTEM: "1",
|
|
229
|
-
...(options.uvRuntimeCache
|
|
230
|
-
? {
|
|
231
|
-
UV_CACHE_DIR: uvCache,
|
|
232
|
-
UV_PROJECT_ENVIRONMENT: join(runtimeRoot, "project-environment"),
|
|
233
|
-
UV_OFFLINE: "1",
|
|
234
|
-
PYTHONDONTWRITEBYTECODE: "1",
|
|
235
|
-
}
|
|
236
|
-
: {}),
|
|
237
|
-
},
|
|
238
|
-
detached: true,
|
|
239
|
-
stdin: "ignore",
|
|
240
|
-
stdout: check.captureStdout ? "pipe" : "ignore",
|
|
241
|
-
stderr: "ignore",
|
|
242
|
-
});
|
|
243
|
-
const running = proc;
|
|
244
|
-
const outputStream = running.stdout;
|
|
245
|
-
if (check.captureStdout && !(outputStream instanceof ReadableStream))
|
|
246
|
-
throw new Error("shell stdout pipe is unavailable");
|
|
247
|
-
const timeout = new Promise<never>((_resolve, reject) => {
|
|
248
|
-
timer = setTimeout(
|
|
249
|
-
() => reject(new Error("shell check timed out")),
|
|
250
|
-
check.timeoutMs,
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
const aborted = new Promise<never>((_resolve, reject) => {
|
|
254
|
-
if (!options.signal) return;
|
|
255
|
-
cancel = () => reject(new Error("shell check cancelled"));
|
|
256
|
-
if (options.signal.aborted) cancel();
|
|
257
|
-
else options.signal.addEventListener("abort", cancel, { once: true });
|
|
258
|
-
});
|
|
259
|
-
const completed = Promise.all([
|
|
260
|
-
running.exited,
|
|
261
|
-
check.captureStdout
|
|
262
|
-
? boundedOutput(outputStream as ReadableStream<Uint8Array>)
|
|
263
|
-
: Promise.resolve(null),
|
|
264
|
-
]).then(([exitCode, stdout]) => ({ exitCode, stdout }));
|
|
265
|
-
return await Promise.race([completed, timeout, aborted]);
|
|
266
|
-
} catch (error) {
|
|
267
|
-
if (proc) {
|
|
268
|
-
stopProcess(proc);
|
|
269
|
-
await proc.exited;
|
|
270
|
-
}
|
|
271
|
-
throw error;
|
|
272
|
-
} finally {
|
|
273
|
-
if (timer) clearTimeout(timer);
|
|
274
|
-
if (cancel) options.signal?.removeEventListener("abort", cancel);
|
|
275
|
-
await isolated.release();
|
|
276
|
-
}
|
|
440
|
+
await requireShellRuntimeCache(options, runtime);
|
|
441
|
+
return new ShellExecution(check, options, isolated, runtime).run();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function shellSeedSources(options: ShellRunOptions): string[] {
|
|
445
|
+
return [
|
|
446
|
+
...runtimeSeedSources(options.runtimePolicy),
|
|
447
|
+
...sharedCacheDenyRoots(options.runtimePolicy, "checks"),
|
|
448
|
+
];
|
|
277
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
|
+
}
|