@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/results.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
execution: { status: ExecutionStatus };
|
|
16
|
-
grading: { status: GradingStatus };
|
|
17
|
-
task: { verdict: TaskVerdict };
|
|
18
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
Assessment,
|
|
3
|
+
CheckOutcome,
|
|
4
|
+
ExecutionStatus,
|
|
5
|
+
GradingStatus,
|
|
6
|
+
TaskVerdict,
|
|
7
|
+
} from "./contract-types";
|
|
8
|
+
export type {
|
|
9
|
+
Assessment,
|
|
10
|
+
CheckOutcome,
|
|
11
|
+
ExecutionStatus,
|
|
12
|
+
GradingStatus,
|
|
13
|
+
TaskVerdict,
|
|
14
|
+
} from "./contract-types";
|
|
19
15
|
|
|
20
16
|
export interface TrialInput {
|
|
21
17
|
execution: ExecutionStatus;
|
|
@@ -27,9 +23,19 @@ export interface TrialInput {
|
|
|
27
23
|
|
|
28
24
|
/** Reduce one trial without treating process completion as task success. */
|
|
29
25
|
export function assessTrial(input: TrialInput): Assessment {
|
|
26
|
+
const { declared, seen } = validateCheckResults(input);
|
|
27
|
+
const grading = trialGrading(input, declared.size, seen.size);
|
|
28
|
+
const task = trialVerdict(input, grading);
|
|
29
|
+
return {
|
|
30
|
+
execution: { status: input.execution },
|
|
31
|
+
grading: { status: grading },
|
|
32
|
+
task: { verdict: task },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validateCheckResults(input: TrialInput) {
|
|
30
37
|
const declared = new Set(input.declaredChecks);
|
|
31
|
-
|
|
32
|
-
throw new Error("check declarations must have unique nonempty IDs");
|
|
38
|
+
validateCheckDeclarations(input.declaredChecks, declared);
|
|
33
39
|
const seen = new Set<string>();
|
|
34
40
|
for (const check of input.checks) {
|
|
35
41
|
if (!declared.has(check.id) || seen.has(check.id))
|
|
@@ -37,29 +43,36 @@ export function assessTrial(input: TrialInput): Assessment {
|
|
|
37
43
|
seen.add(check.id);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
return { declared, seen };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function trialGrading(
|
|
50
|
+
input: TrialInput,
|
|
51
|
+
declared: number,
|
|
52
|
+
seen: number,
|
|
53
|
+
): GradingStatus {
|
|
54
|
+
if (input.graderError) return "error";
|
|
55
|
+
if (input.execution !== "completed") return "not_requested";
|
|
56
|
+
if (evidenceUnavailable(input, declared, seen)) return "unavailable";
|
|
57
|
+
return declared === 0 ? "not_requested" : "completed";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function evidenceUnavailable(
|
|
61
|
+
input: TrialInput,
|
|
62
|
+
declared: number,
|
|
63
|
+
seen: number,
|
|
64
|
+
): boolean {
|
|
65
|
+
return (
|
|
66
|
+
Boolean(input.requiredEvidenceUnavailable) ||
|
|
67
|
+
input.checks.some((check) => check.status === "unavailable") ||
|
|
68
|
+
seen !== declared
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function trialVerdict(input: TrialInput, grading: GradingStatus): TaskVerdict {
|
|
73
|
+
if (input.execution !== "completed") return "not_assessed";
|
|
74
|
+
if (input.checks.some((check) => check.status === "failed")) return "failed";
|
|
75
|
+
return grading === "completed" ? "passed" : "not_assessed";
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
/** Apply an explicitly selected policy only to a fully assessed trial. */
|
|
@@ -83,6 +96,21 @@ export function summarizeAssessments(
|
|
|
83
96
|
assessments: Assessment[],
|
|
84
97
|
passThreshold = 1,
|
|
85
98
|
): Assessment {
|
|
99
|
+
validateSummaryInputs(assessments, passThreshold);
|
|
100
|
+
const execution = summaryExecution(assessments);
|
|
101
|
+
const grading = summaryGrading(assessments);
|
|
102
|
+
const task = summaryVerdict(assessments, passThreshold);
|
|
103
|
+
return {
|
|
104
|
+
execution: { status: execution },
|
|
105
|
+
grading: { status: grading },
|
|
106
|
+
task: { verdict: task },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateSummaryInputs(
|
|
111
|
+
assessments: Assessment[],
|
|
112
|
+
passThreshold: number,
|
|
113
|
+
): void {
|
|
86
114
|
if (!assessments.length)
|
|
87
115
|
throw new Error("at least one assessment is required");
|
|
88
116
|
if (
|
|
@@ -91,38 +119,36 @@ export function summarizeAssessments(
|
|
|
91
119
|
passThreshold > 1
|
|
92
120
|
)
|
|
93
121
|
throw new Error("pass threshold must be greater than zero and at most one");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
task: { verdict: task },
|
|
125
|
-
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function summaryExecution(assessments: Assessment[]): ExecutionStatus {
|
|
125
|
+
const priority: ExecutionStatus[] = ["failed", "cancelled", "not_run"];
|
|
126
|
+
return (
|
|
127
|
+
priority.find((status) =>
|
|
128
|
+
assessments.some((item) => item.execution.status === status),
|
|
129
|
+
) ?? "completed"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function summaryGrading(assessments: Assessment[]): GradingStatus {
|
|
134
|
+
const priority: GradingStatus[] = ["error", "unavailable", "completed"];
|
|
135
|
+
return (
|
|
136
|
+
priority.find((status) =>
|
|
137
|
+
assessments.some((item) => item.grading.status === status),
|
|
138
|
+
) ?? "not_requested"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function summaryVerdict(
|
|
143
|
+
assessments: Assessment[],
|
|
144
|
+
passThreshold: number,
|
|
145
|
+
): TaskVerdict {
|
|
146
|
+
if (assessments.some((item) => item.task.verdict === "not_assessed"))
|
|
147
|
+
return "not_assessed";
|
|
148
|
+
const passed = assessments.filter(
|
|
149
|
+
(item) => item.task.verdict === "passed",
|
|
150
|
+
).length;
|
|
151
|
+
return passed / assessments.length >= passThreshold ? "passed" : "failed";
|
|
126
152
|
}
|
|
127
153
|
|
|
128
154
|
/** Combine case verdicts after each case has applied its own threshold. */
|
|
@@ -144,17 +170,38 @@ export function exitCodeFor(
|
|
|
144
170
|
assessment: Assessment,
|
|
145
171
|
context: ExitContext = {},
|
|
146
172
|
): number {
|
|
173
|
+
const interruption = interruptionExit(context);
|
|
174
|
+
if (interruption !== null) return interruption;
|
|
175
|
+
if (executionFailed(assessment.execution.status, context)) return 2;
|
|
176
|
+
const gradingCodes: Partial<Record<GradingStatus, number>> = {
|
|
177
|
+
error: 3,
|
|
178
|
+
unavailable: 4,
|
|
179
|
+
};
|
|
180
|
+
return gradingCodes[assessment.grading.status] ?? taskExitCode(assessment);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function interruptionExit(context: ExitContext): number | null {
|
|
147
184
|
if (context.signal === "SIGINT") return 130;
|
|
148
185
|
if (context.signal === "SIGTERM") return 143;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
return context.internalFailure ? 70 : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function executionFailed(
|
|
190
|
+
status: ExecutionStatus,
|
|
191
|
+
context: ExitContext,
|
|
192
|
+
): boolean {
|
|
193
|
+
return (
|
|
194
|
+
status === "failed" ||
|
|
195
|
+
status === "cancelled" ||
|
|
196
|
+
(status === "not_run" && !context.dryRun)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function taskExitCode(assessment: Assessment): number {
|
|
201
|
+
return assessment.task.verdict === "failed" ? 1 : 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function validateCheckDeclarations(ids: string[], declared: Set<string>): void {
|
|
205
|
+
if (declared.size !== ids.length || declared.has(""))
|
|
206
|
+
throw new Error("check declarations must have unique nonempty IDs");
|
|
160
207
|
}
|
package/src/run-owner.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "./value-guards";
|
|
1
2
|
import { Database } from "bun:sqlite";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import {
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
import { hostname } from "node:os";
|
|
11
12
|
import { basename, dirname, join } from "node:path";
|
|
12
13
|
|
|
13
|
-
interface ProcessIdentity {
|
|
14
|
+
export interface ProcessIdentity {
|
|
14
15
|
pid: number;
|
|
15
16
|
startedAt: string;
|
|
16
17
|
host: string;
|
|
@@ -29,6 +30,60 @@ interface Claim {
|
|
|
29
30
|
finalizedAt?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
interface RetainedClaim {
|
|
34
|
+
format: "sevro.run-owner.v1";
|
|
35
|
+
evaluationDigest: unknown;
|
|
36
|
+
attemptId: unknown;
|
|
37
|
+
status: Claim["status"];
|
|
38
|
+
owner?: unknown;
|
|
39
|
+
activeRunPath: string;
|
|
40
|
+
evidenceDirectory: string;
|
|
41
|
+
artifactPath: string;
|
|
42
|
+
startedAt?: unknown;
|
|
43
|
+
finalizedAt?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function errorCode(error: unknown): unknown {
|
|
47
|
+
return isRecord(error) ? error.code : undefined;
|
|
48
|
+
}
|
|
49
|
+
function nonemptyText(value: unknown): value is string {
|
|
50
|
+
return typeof value === "string" && value.length > 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validClaimStatus(value: unknown): value is Claim["status"] {
|
|
54
|
+
return (
|
|
55
|
+
value === "active" ||
|
|
56
|
+
value === "complete" ||
|
|
57
|
+
value === "diagnostic" ||
|
|
58
|
+
value === "interrupted"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validClaimIdentity(value: Record<string, unknown>): boolean {
|
|
63
|
+
return (
|
|
64
|
+
value.format === "sevro.run-owner.v1" &&
|
|
65
|
+
/^[a-f0-9]{64}$/.test(String(value.evaluationDigest)) &&
|
|
66
|
+
Boolean(value.attemptId) &&
|
|
67
|
+
validClaimStatus(value.status)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validClaimPaths(value: Record<string, unknown>): boolean {
|
|
72
|
+
return (
|
|
73
|
+
nonemptyText(value.activeRunPath) &&
|
|
74
|
+
nonemptyText(value.evidenceDirectory) &&
|
|
75
|
+
nonemptyText(value.artifactPath)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertRetainedClaim(
|
|
80
|
+
value: unknown,
|
|
81
|
+
path: string,
|
|
82
|
+
): asserts value is RetainedClaim {
|
|
83
|
+
if (!isRecord(value) || !validClaimIdentity(value) || !validClaimPaths(value))
|
|
84
|
+
throw new Error(`unrecognized Sevro ownership record: ${path}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
32
87
|
export interface RunOwner {
|
|
33
88
|
claimPath: string;
|
|
34
89
|
checkpointPath: string;
|
|
@@ -62,41 +117,29 @@ function withClaimLock<T>(claimPath: string, action: () => T): T {
|
|
|
62
117
|
mkdirSync(locks, { recursive: true, mode: 0o700 });
|
|
63
118
|
const database = new Database(join(locks, `${basename(claimPath)}.sqlite`));
|
|
64
119
|
try {
|
|
65
|
-
database.
|
|
66
|
-
database.
|
|
120
|
+
database.run("PRAGMA busy_timeout = 5000");
|
|
121
|
+
database.run("CREATE TABLE IF NOT EXISTS mutex (id INTEGER PRIMARY KEY)");
|
|
67
122
|
return database.transaction(action).immediate();
|
|
68
123
|
} finally {
|
|
69
124
|
database.close();
|
|
70
125
|
}
|
|
71
126
|
}
|
|
72
127
|
|
|
73
|
-
function readClaim(path: string):
|
|
128
|
+
function readClaim(path: string): RetainedClaim | null {
|
|
74
129
|
let bytes: string;
|
|
75
130
|
try {
|
|
76
131
|
bytes = readFileSync(path, "utf8");
|
|
77
132
|
} catch (error) {
|
|
78
|
-
if ((error
|
|
133
|
+
if (errorCode(error) === "ENOENT") return null;
|
|
79
134
|
throw error;
|
|
80
135
|
}
|
|
81
|
-
const value = JSON.parse(bytes)
|
|
82
|
-
|
|
83
|
-
value?.format !== "sevro.run-owner.v1" ||
|
|
84
|
-
!/^[a-f0-9]{64}$/.test(value.evaluationDigest) ||
|
|
85
|
-
!value.attemptId ||
|
|
86
|
-
!["active", "complete", "diagnostic", "interrupted"].includes(
|
|
87
|
-
value.status,
|
|
88
|
-
) ||
|
|
89
|
-
!value.activeRunPath ||
|
|
90
|
-
!value.evidenceDirectory ||
|
|
91
|
-
!value.artifactPath
|
|
92
|
-
)
|
|
93
|
-
throw new Error(`unrecognized Sevro ownership record: ${path}`);
|
|
136
|
+
const value: unknown = JSON.parse(bytes);
|
|
137
|
+
assertRetainedClaim(value, path);
|
|
94
138
|
return value;
|
|
95
139
|
}
|
|
96
140
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
const argv = windows
|
|
141
|
+
function identityCommand(pid: number, windows: boolean): string[] {
|
|
142
|
+
return windows
|
|
100
143
|
? [
|
|
101
144
|
"powershell.exe",
|
|
102
145
|
"-NoProfile",
|
|
@@ -105,49 +148,79 @@ function processIdentity(pid: number): ProcessIdentity | null {
|
|
|
105
148
|
`(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
|
|
106
149
|
]
|
|
107
150
|
: ["/bin/ps", "-p", String(pid), "-o", "lstart="];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function identityEnvironment(windows: boolean) {
|
|
154
|
+
return windows
|
|
155
|
+
? {
|
|
156
|
+
PATH: process.env.PATH ?? "",
|
|
157
|
+
SystemRoot: process.env.SystemRoot ?? "C:\\Windows",
|
|
158
|
+
}
|
|
159
|
+
: { PATH: "/usr/bin:/bin", LC_ALL: "C" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function processStartTime(result: ReturnType<typeof Bun.spawnSync>): string {
|
|
163
|
+
return result.stdout?.toString().trim() ?? "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function processIdentity(pid: number): ProcessIdentity | null {
|
|
167
|
+
const windows = process.platform === "win32";
|
|
168
|
+
const argv = identityCommand(pid, windows);
|
|
108
169
|
let result: ReturnType<typeof Bun.spawnSync>;
|
|
109
170
|
try {
|
|
110
171
|
result = Bun.spawnSync(argv, {
|
|
111
|
-
env: windows
|
|
112
|
-
? {
|
|
113
|
-
PATH: process.env.PATH ?? "",
|
|
114
|
-
SystemRoot: process.env.SystemRoot ?? "C:\\Windows",
|
|
115
|
-
}
|
|
116
|
-
: { PATH: "/usr/bin:/bin", LC_ALL: "C" },
|
|
172
|
+
env: identityEnvironment(windows),
|
|
117
173
|
stdout: "pipe",
|
|
118
174
|
stderr: "ignore",
|
|
119
175
|
});
|
|
120
176
|
} catch {
|
|
121
177
|
return null;
|
|
122
178
|
}
|
|
123
|
-
const startedAt = result
|
|
179
|
+
const startedAt = processStartTime(result);
|
|
124
180
|
return result.exitCode === 0 && startedAt
|
|
125
181
|
? { pid, startedAt, host: hostname() }
|
|
126
182
|
: null;
|
|
127
183
|
}
|
|
128
184
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
185
|
+
function validProcessId(value: unknown): value is number {
|
|
186
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function validProcessIdentity(value: unknown): value is ProcessIdentity {
|
|
190
|
+
return (
|
|
191
|
+
isRecord(value) &&
|
|
192
|
+
value.host === hostname() &&
|
|
193
|
+
validProcessId(value.pid) &&
|
|
194
|
+
nonemptyText(value.startedAt)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function processLiveness(pid: number): "live" | "dead" | "unknown" {
|
|
138
199
|
try {
|
|
139
|
-
process.kill(
|
|
200
|
+
process.kill(pid, 0);
|
|
201
|
+
return "live";
|
|
140
202
|
} catch (error) {
|
|
141
|
-
return (error
|
|
142
|
-
? "dead"
|
|
143
|
-
: "unknown";
|
|
203
|
+
return errorCode(error) === "ESRCH" ? "dead" : "unknown";
|
|
144
204
|
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function ownerStatus(owner: unknown): "live" | "dead" | "unknown" {
|
|
208
|
+
if (!validProcessIdentity(owner)) return "unknown";
|
|
209
|
+
const liveness = processLiveness(owner.pid);
|
|
210
|
+
if (liveness !== "live") return liveness;
|
|
145
211
|
const current = processIdentity(owner.pid);
|
|
146
212
|
if (!current) return "unknown";
|
|
147
213
|
return current.startedAt === owner.startedAt ? "live" : "dead";
|
|
148
214
|
}
|
|
149
215
|
|
|
150
|
-
function
|
|
216
|
+
function activeRunFields(value: unknown): Record<string, unknown> {
|
|
217
|
+
if (value == null) return {};
|
|
218
|
+
// Preserve the existing object-spread representation of opaque active records.
|
|
219
|
+
const entries: [string, unknown][] = Object.entries(value);
|
|
220
|
+
return Object.fromEntries(entries);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function preserveAbandoned(claim: RetainedClaim): void {
|
|
151
224
|
const interrupted = {
|
|
152
225
|
...claim,
|
|
153
226
|
status: "interrupted" as const,
|
|
@@ -156,9 +229,12 @@ function preserveAbandoned(claim: Claim): void {
|
|
|
156
229
|
atomicJson(join(claim.evidenceDirectory, "run-owner.json"), interrupted);
|
|
157
230
|
let active: Record<string, unknown>;
|
|
158
231
|
try {
|
|
159
|
-
|
|
232
|
+
const parsed: unknown = JSON.parse(
|
|
233
|
+
readFileSync(claim.activeRunPath, "utf8"),
|
|
234
|
+
);
|
|
235
|
+
active = activeRunFields(parsed);
|
|
160
236
|
} catch (error) {
|
|
161
|
-
if ((error
|
|
237
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
162
238
|
active = {
|
|
163
239
|
format: "sevro.active-run.v1",
|
|
164
240
|
runId: claim.attemptId,
|
|
@@ -174,6 +250,37 @@ function preserveAbandoned(claim: Claim): void {
|
|
|
174
250
|
});
|
|
175
251
|
}
|
|
176
252
|
|
|
253
|
+
function validateClaimIdentityMatch(
|
|
254
|
+
prior: RetainedClaim | null,
|
|
255
|
+
evaluationDigest: string,
|
|
256
|
+
): void {
|
|
257
|
+
if (prior && prior.evaluationDigest !== evaluationDigest)
|
|
258
|
+
throw new Error("Sevro ownership record has a different identity");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function retainPreviousOwner(
|
|
262
|
+
prior: RetainedClaim | null,
|
|
263
|
+
claimPath: string,
|
|
264
|
+
): void {
|
|
265
|
+
if (prior === null || prior.status !== "active") return;
|
|
266
|
+
const status = ownerStatus(prior.owner);
|
|
267
|
+
if (status === "live")
|
|
268
|
+
throw new Error(`equivalent Sevro run is active: ${claimPath}`);
|
|
269
|
+
if (status === "unknown")
|
|
270
|
+
throw new Error(`Sevro run ownership is unverifiable: ${claimPath}`);
|
|
271
|
+
preserveAbandoned(prior);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function validateCheckpointOwner(context: RunOwner): void {
|
|
275
|
+
const current = readClaim(context.claimPath);
|
|
276
|
+
if (!current || current.attemptId !== context.claim.attemptId)
|
|
277
|
+
throw new Error("Sevro run ownership changed before checkpoint");
|
|
278
|
+
if (current.status !== "active")
|
|
279
|
+
throw new Error("Sevro run owner is already finalized");
|
|
280
|
+
if (ownerStatus(current.owner) !== "live")
|
|
281
|
+
throw new Error("Sevro run owner identity is no longer verifiable");
|
|
282
|
+
}
|
|
283
|
+
|
|
177
284
|
/** Claim one evaluation identity before candidate execution. */
|
|
178
285
|
export function startRunOwner(options: {
|
|
179
286
|
stateRoot: string;
|
|
@@ -193,16 +300,8 @@ export function startRunOwner(options: {
|
|
|
193
300
|
);
|
|
194
301
|
return withClaimLock(claimPath, () => {
|
|
195
302
|
const prior = readClaim(claimPath);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (prior?.status === "active") {
|
|
199
|
-
const status = ownerStatus(prior.owner);
|
|
200
|
-
if (status === "live")
|
|
201
|
-
throw new Error(`equivalent Sevro run is active: ${claimPath}`);
|
|
202
|
-
if (status === "unknown")
|
|
203
|
-
throw new Error(`Sevro run ownership is unverifiable: ${claimPath}`);
|
|
204
|
-
preserveAbandoned(prior);
|
|
205
|
-
}
|
|
303
|
+
validateClaimIdentityMatch(prior, options.evaluationDigest);
|
|
304
|
+
retainPreviousOwner(prior, claimPath);
|
|
206
305
|
const owner = processIdentity(process.pid);
|
|
207
306
|
if (!owner) throw new Error("cannot verify Sevro runner process identity");
|
|
208
307
|
const claim: Claim = {
|
|
@@ -234,15 +333,7 @@ export function checkpointRunOwner(
|
|
|
234
333
|
alreadyLocked = false,
|
|
235
334
|
): void {
|
|
236
335
|
const write = () => {
|
|
237
|
-
if (!alreadyLocked)
|
|
238
|
-
const current = readClaim(context.claimPath);
|
|
239
|
-
if (!current || current.attemptId !== context.claim.attemptId)
|
|
240
|
-
throw new Error("Sevro run ownership changed before checkpoint");
|
|
241
|
-
if (current.status !== "active")
|
|
242
|
-
throw new Error("Sevro run owner is already finalized");
|
|
243
|
-
if (ownerStatus(current.owner) !== "live")
|
|
244
|
-
throw new Error("Sevro run owner identity is no longer verifiable");
|
|
245
|
-
}
|
|
336
|
+
if (!alreadyLocked) validateCheckpointOwner(context);
|
|
246
337
|
const finalizedAt =
|
|
247
338
|
status === "active" ? undefined : new Date().toISOString();
|
|
248
339
|
const updatedClaim = {
|