@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +170 -0
- package/LICENSE +63 -136
- package/README.md +83 -66
- package/docs/README.md +44 -0
- package/docs/acknowledgements.md +14 -0
- package/docs/architecture.md +146 -0
- package/docs/assets/README.md +27 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +1 -0
- package/docs/assets/sevro-wordmark.svg +1 -0
- package/docs/builtin-graders-v1.md +41 -13
- package/docs/codex-host.md +184 -52
- package/docs/contracts.md +19 -0
- package/docs/creating-cases.md +139 -0
- package/docs/development-cli.md +190 -30
- package/docs/documentation-quality.md +61 -0
- package/docs/evaluation-workflow.md +110 -0
- package/docs/evidence/code-mode-controls-71.md +64 -0
- package/docs/evidence/code-mode-programs-85.md +64 -0
- package/docs/evidence/codex-state-73/README.md +204 -0
- package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
- package/docs/evidence/codex-state-73/results.json +661 -0
- package/docs/evidence/local-http-74.md +104 -0
- package/docs/evidence/runtime-startup-30/README.md +185 -0
- package/docs/evidence/runtime-startup-30/after.json +319 -0
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
- package/docs/evidence/runtime-startup-30/before.json +403 -0
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
- package/docs/evidence/skill-reads-70/results.json +823 -0
- package/docs/evidence.md +21 -0
- package/docs/extension-protocol-v1.md +50 -16
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/getting-started.md +102 -0
- package/docs/guide-evaluation.md +89 -0
- package/docs/guide-verification.json +239 -0
- package/docs/identity-v1.md +35 -1
- package/docs/installing.md +88 -0
- package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
- package/docs/licensing.md +42 -0
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +228 -0
- package/docs/reading-results.md +141 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +81 -40
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +180 -0
- package/docs/run-settings-v1.md +147 -0
- package/docs/running-evaluations.md +31 -0
- package/docs/runtime-v1.md +614 -0
- package/docs/specs/repository-guide.md +58 -0
- package/docs/troubleshooting.md +30 -0
- package/docs/typescript-quality.md +234 -0
- package/docs/usability-study.md +114 -0
- package/examples/basic/host.ts +2 -2
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +39 -6
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +193 -0
- package/src/advisory.ts +10 -5
- package/src/candidate-artifacts.ts +208 -0
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +105 -0
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +81 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +59 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +222 -0
- package/src/cli/hosts.ts +59 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/cli/invocation.ts +627 -0
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +163 -0
- package/src/cli/presentation.ts +91 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +133 -0
- package/src/cli/run-options.ts +101 -0
- package/src/cli/run-overrides.ts +83 -0
- package/src/cli/settings.ts +160 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -843
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +19 -2373
- package/src/evaluation/advisory.ts +150 -0
- package/src/evaluation/check-plan.ts +510 -0
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/evaluation/grading-host.ts +39 -0
- package/src/evaluation/grading.ts +258 -0
- package/src/evaluation/invocation-plan.ts +548 -0
- package/src/evaluation/plan.ts +736 -0
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
- package/src/evaluation/semantic.ts +312 -0
- package/src/evaluation/trial.ts +866 -0
- package/src/evaluation/types.ts +207 -0
- package/src/evidence/host-results.ts +92 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +228 -148
- package/src/extension-session.ts +326 -195
- package/src/fixtures/advisory.ts +338 -0
- package/src/fixtures/bin.ts +57 -0
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +187 -0
- package/src/fixtures/generated.ts +300 -0
- package/src/fixtures/git-hooks.ts +24 -0
- package/src/fixtures/preparation.ts +270 -0
- package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
- package/src/fixtures/setup.ts +224 -0
- package/src/fixtures/tools.ts +88 -0
- package/src/fixtures/workspace-state.ts +212 -0
- package/src/generated/cli-result.cjs +2 -0
- package/src/generated/cli-result.d.cts +3 -0
- package/src/generated/extension.cjs +2 -0
- package/src/generated/extension.d.cts +3 -0
- package/src/generated/report.cjs +2 -0
- package/src/generated/report.d.cts +3 -0
- package/src/generated/run-evidence.cjs +2 -0
- package/src/generated/run-evidence.d.cts +3 -0
- package/src/generated/runtime.cjs +2 -0
- package/src/generated/runtime.d.cts +3 -0
- package/src/graders/artifact.ts +56 -23
- package/src/graders/git-head.ts +79 -75
- package/src/graders/output.ts +222 -140
- package/src/graders/semantic.ts +102 -43
- package/src/graders/shell.ts +379 -207
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +319 -0
- package/src/hosts/claude-continuation.ts +63 -18
- package/src/hosts/claude-credential.ts +43 -62
- package/src/hosts/claude-events.ts +86 -51
- package/src/hosts/claude-keychain.ts +59 -0
- package/src/hosts/claude-native-goal.ts +57 -33
- package/src/hosts/claude-process.ts +111 -0
- package/src/hosts/claude-repository-invocation.ts +208 -117
- package/src/hosts/claude-settings.ts +240 -21
- package/src/hosts/claude-skill-activations.ts +166 -0
- package/src/hosts/claude-state.ts +403 -0
- package/src/hosts/claude-tool-calls.ts +171 -106
- package/src/hosts/claude.ts +561 -451
- package/src/hosts/codex-app-server.ts +326 -178
- package/src/hosts/codex-configuration.ts +44 -18
- package/src/hosts/codex-events.ts +97 -60
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-invocation.ts +70 -0
- package/src/hosts/codex-marketplace.ts +283 -0
- package/src/hosts/codex-native-calls.ts +546 -500
- package/src/hosts/codex-process.ts +202 -0
- package/src/hosts/codex-profile.ts +257 -28
- package/src/hosts/codex-session-files.ts +106 -0
- package/src/hosts/codex-state.ts +492 -0
- package/src/hosts/codex.ts +783 -832
- package/src/hosts/isolation-common.ts +94 -0
- package/src/hosts/isolation-roots.ts +54 -27
- package/src/hosts/isolation.ts +10 -0
- package/src/hosts/linux-sandbox.ts +159 -0
- package/src/hosts/mac-sandbox.ts +187 -61
- package/src/hosts/native-controls.ts +152 -62
- package/src/hosts/process-lifecycle.ts +37 -0
- package/src/hosts/protected-worktrees.ts +51 -19
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +580 -0
- package/src/hosts/workspace-fingerprint.ts +55 -35
- package/src/identity.ts +102 -48
- package/src/instrumentation.ts +109 -48
- package/src/native-transcript-state.ts +131 -0
- package/src/native-transcript-view.ts +90 -0
- package/src/native-transcripts.ts +286 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +161 -81
- package/src/prune.ts +215 -0
- package/src/report.ts +172 -140
- package/src/resolved-case.ts +33 -0
- package/src/results-lock.ts +29 -0
- package/src/results.ts +133 -86
- package/src/run-owner.ts +154 -63
- package/src/runtime/config.ts +456 -0
- package/src/runtime/discovery-apple.ts +90 -0
- package/src/runtime/discovery-providers.ts +63 -0
- package/src/runtime/discovery.ts +70 -0
- package/src/runtime/hook-processes.ts +97 -0
- package/src/runtime/hooks.ts +476 -0
- package/src/runtime/paths.ts +313 -0
- package/src/runtime/query.ts +85 -0
- package/src/runtime/seeds.ts +134 -0
- package/src/runtime/shared-cache.ts +143 -0
- package/src/runtime/state.ts +143 -0
- package/src/schema-types.ts +198 -0
- package/src/schema.ts +15 -19
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +20 -0
- package/src/advisory-fixture.ts +0 -282
- package/src/fixture-bin.ts +0 -87
- package/src/fixture-setup.ts +0 -163
- package/src/generated-fixture.ts +0 -220
- package/src/git-hooks.ts +0 -63
- package/src/hosts/claude-nested-skills.ts +0 -218
- package/src/hosts/codex-command-output.ts +0 -399
- package/src/hosts/codex-executor-command.ts +0 -195
- package/src/hosts/codex-skill-reads.ts +0 -391
- package/src/preparation.ts +0 -211
|
@@ -0,0 +1,319 @@
|
|
|
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) 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) 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
|
+
export function diagnosticStage(
|
|
148
|
+
diagnostics: HostDiagnostics | undefined,
|
|
149
|
+
stage: string,
|
|
150
|
+
): void {
|
|
151
|
+
if (diagnostics) diagnostics.stage = stage;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A cleanup exception must not replace an already captured host/persistence failure. */
|
|
155
|
+
export async function cleanupNativeState(
|
|
156
|
+
diagnostics: HostDiagnostics | undefined,
|
|
157
|
+
cleanup: () => Promise<void>,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
beginNativeCleanup(diagnostics);
|
|
160
|
+
try {
|
|
161
|
+
await cleanup();
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
if (!diagnostics?.failureCaptured) throw cause;
|
|
164
|
+
console.warn(
|
|
165
|
+
"native state cleanup failed; original host failure preserved",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function beginNativeCleanup(diagnostics: HostDiagnostics | undefined): void {
|
|
171
|
+
if (diagnostics && !diagnostics.failureCaptured)
|
|
172
|
+
diagnosticStage(diagnostics, "cleanup");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function diagnosticConfiguration(enabled: boolean | undefined) {
|
|
176
|
+
return enabled ? { "sevro.host.retain-diagnostics": true } : {};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createHostDiagnostics(
|
|
180
|
+
host: "codex" | "claude",
|
|
181
|
+
enabled: boolean | undefined,
|
|
182
|
+
request: {
|
|
183
|
+
runtimeRole?: RuntimeRole;
|
|
184
|
+
retainHostDiagnostics?: (bytes: Uint8Array) => Promise<void>;
|
|
185
|
+
},
|
|
186
|
+
): HostDiagnostics | undefined {
|
|
187
|
+
return enabled
|
|
188
|
+
? new HostDiagnostics(
|
|
189
|
+
host,
|
|
190
|
+
request.runtimeRole,
|
|
191
|
+
request.retainHostDiagnostics,
|
|
192
|
+
)
|
|
193
|
+
: undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function readDiagnosticCredentials(
|
|
197
|
+
diagnostics: HostDiagnostics | undefined,
|
|
198
|
+
path: string | undefined,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
if (diagnostics && path) await diagnostics.readCredentials(path);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function diagnosticCredentialEnvironment(
|
|
204
|
+
diagnostics: HostDiagnostics | undefined,
|
|
205
|
+
environment: Record<string, string>,
|
|
206
|
+
names: readonly string[],
|
|
207
|
+
): void {
|
|
208
|
+
if (diagnostics) diagnostics.registerCredentials(environment, names);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function retainReturnedHostFailure(
|
|
212
|
+
diagnostics: HostDiagnostics | undefined,
|
|
213
|
+
result: HostResult,
|
|
214
|
+
message: string,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
if (diagnostics && result.executionFailed)
|
|
217
|
+
await diagnostics.retain(result, diagnostics.failure ?? new Error(message));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function diagnoseHostRequest(
|
|
221
|
+
diagnostics: HostDiagnostics | undefined,
|
|
222
|
+
execute: () => Promise<HostResult>,
|
|
223
|
+
): Promise<HostResult> {
|
|
224
|
+
try {
|
|
225
|
+
return await execute();
|
|
226
|
+
} catch (cause) {
|
|
227
|
+
if (cause instanceof HostDiagnosticPersistenceError) throw cause;
|
|
228
|
+
if (!diagnostics) throw cause;
|
|
229
|
+
const result: HostResult = {
|
|
230
|
+
finalMessage: null,
|
|
231
|
+
complete: false,
|
|
232
|
+
executionFailed: true,
|
|
233
|
+
actualCondition: "passive",
|
|
234
|
+
};
|
|
235
|
+
await diagnostics.retain(result, cause);
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Private failure evidence, independent of native sessions and transcript access. */
|
|
241
|
+
export class HostDiagnostics {
|
|
242
|
+
stage = "preparation";
|
|
243
|
+
failure: unknown;
|
|
244
|
+
failureCaptured = false;
|
|
245
|
+
private processes: Record<string, unknown>[] = [];
|
|
246
|
+
private processesDropped = 0;
|
|
247
|
+
private secrets: string[] = [];
|
|
248
|
+
constructor(
|
|
249
|
+
private host: "codex" | "claude",
|
|
250
|
+
private role: RuntimeRole = "candidate",
|
|
251
|
+
private persist?: (bytes: Uint8Array) => Promise<void>,
|
|
252
|
+
) {}
|
|
253
|
+
|
|
254
|
+
async readCredentials(path: string): Promise<void> {
|
|
255
|
+
this.secrets.push(...(await diagnosticSecrets(path)));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
registerCredentials(
|
|
259
|
+
environment: Record<string, string>,
|
|
260
|
+
names: readonly string[],
|
|
261
|
+
): void {
|
|
262
|
+
for (const name of names) {
|
|
263
|
+
const value = environment[name];
|
|
264
|
+
if (value) this.secrets.push(value);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
process(result: ProcessDiagnostic): void {
|
|
269
|
+
const stdout = boundedText(Buffer.from(result.out), this.secrets);
|
|
270
|
+
const stderr = boundedText(result.stderr, this.secrets);
|
|
271
|
+
this.addProcess({
|
|
272
|
+
stage: this.stage,
|
|
273
|
+
code: result.code,
|
|
274
|
+
signal: result.signal,
|
|
275
|
+
durationMs: result.durationMs,
|
|
276
|
+
reason: result.reason,
|
|
277
|
+
stdout: stdout.text,
|
|
278
|
+
stdoutTruncated: stdout.truncated || result.stdoutTruncated === true,
|
|
279
|
+
stderr: stderr.text,
|
|
280
|
+
stderrTruncated: stderr.truncated || result.stderrTruncated,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private addProcess(receipt: Record<string, unknown>): void {
|
|
285
|
+
this.processes.push(receipt);
|
|
286
|
+
while (
|
|
287
|
+
this.processes.length > 32 ||
|
|
288
|
+
Buffer.byteLength(JSON.stringify(this.processes)) > 2 * 1024 * 1024
|
|
289
|
+
) {
|
|
290
|
+
this.processes.shift();
|
|
291
|
+
this.processesDropped++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async retain(result: HostResult, cause: unknown): Promise<void> {
|
|
296
|
+
this.failureCaptured = true;
|
|
297
|
+
const bytes = Buffer.from(
|
|
298
|
+
JSON.stringify({
|
|
299
|
+
format: "sevro.host-diagnostics.v1",
|
|
300
|
+
host: this.host,
|
|
301
|
+
role: this.role,
|
|
302
|
+
stage: this.stage,
|
|
303
|
+
error: errorEvidence(cause, this.secrets),
|
|
304
|
+
processes: this.processes,
|
|
305
|
+
processesDropped: this.processesDropped,
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
if (this.persist) {
|
|
309
|
+
try {
|
|
310
|
+
await this.persist(bytes);
|
|
311
|
+
} catch (persistenceCause) {
|
|
312
|
+
throw new HostDiagnosticPersistenceError(persistenceCause, cause);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
result.artifacts ??= [];
|
|
317
|
+
result.artifacts.push({ id: "sevro.host.diagnostics", bytes });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "../value-guards";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { summarizeClaudeEvents } from "./claude-events";
|
|
3
4
|
import { workspaceFingerprint } from "./workspace-fingerprint";
|
|
@@ -21,9 +22,15 @@ function boundResult(stream: string, session: string): boolean {
|
|
|
21
22
|
const results = stream
|
|
22
23
|
.split("\n")
|
|
23
24
|
.filter((line) => line.trim())
|
|
24
|
-
.map((line) => JSON.parse(line))
|
|
25
|
-
.filter(
|
|
26
|
-
|
|
25
|
+
.map((line): unknown => JSON.parse(line))
|
|
26
|
+
.filter(isRecord)
|
|
27
|
+
.filter((entry) => entry.type === "result");
|
|
28
|
+
const [result] = results;
|
|
29
|
+
return (
|
|
30
|
+
results.length === 1 &&
|
|
31
|
+
result !== undefined &&
|
|
32
|
+
result.session_id === session
|
|
33
|
+
);
|
|
27
34
|
} catch {
|
|
28
35
|
return false;
|
|
29
36
|
}
|
|
@@ -59,10 +66,19 @@ export async function runClaudeTurns(options: {
|
|
|
59
66
|
option: "--resume",
|
|
60
67
|
id: sessionId,
|
|
61
68
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
return combinedTurns(
|
|
70
|
+
initial,
|
|
71
|
+
followUp,
|
|
72
|
+
sessionId,
|
|
73
|
+
initialFingerprint,
|
|
74
|
+
beforeFollowUp,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function combinedOutput(
|
|
79
|
+
initial: ClaudeProcessResult,
|
|
80
|
+
followUp: ClaudeProcessResult,
|
|
81
|
+
) {
|
|
66
82
|
const out = `${initial.out.trimEnd()}\n${followUp.out.trimStart()}`;
|
|
67
83
|
const err = [initial.err.trimEnd(), followUp.err.trimStart()]
|
|
68
84
|
.filter(Boolean)
|
|
@@ -72,6 +88,40 @@ export async function runClaudeTurns(options: {
|
|
|
72
88
|
Buffer.byteLength(err, "utf8") > 64 * 1024
|
|
73
89
|
)
|
|
74
90
|
throw new Error("Claude combined process output exceeds its limit");
|
|
91
|
+
return { out, err };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function continuationObservation(
|
|
95
|
+
bound: boolean,
|
|
96
|
+
sessionId: string,
|
|
97
|
+
initialFingerprint: string | null,
|
|
98
|
+
beforeFollowUp: string | null,
|
|
99
|
+
): Observation {
|
|
100
|
+
const measured = initialFingerprint !== null && beforeFollowUp !== null;
|
|
101
|
+
return {
|
|
102
|
+
id: "sevro.claude.continuation",
|
|
103
|
+
completeness: bound && measured ? "complete" : "partial",
|
|
104
|
+
data: {
|
|
105
|
+
method: "same_session_resume",
|
|
106
|
+
sessionId,
|
|
107
|
+
preFollowUpWorktreeUnchanged: measured
|
|
108
|
+
? initialFingerprint === beforeFollowUp
|
|
109
|
+
: null,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function combinedTurns(
|
|
115
|
+
initial: ClaudeProcessResult,
|
|
116
|
+
followUp: ClaudeProcessResult,
|
|
117
|
+
sessionId: string,
|
|
118
|
+
initialFingerprint: string | null,
|
|
119
|
+
beforeFollowUp: string | null,
|
|
120
|
+
): ClaudeTurns {
|
|
121
|
+
const followUpSummary = summarizeClaudeEvents(followUp.out, followUp.code);
|
|
122
|
+
const sessionResultsBound = boundResult(followUp.out, sessionId);
|
|
123
|
+
const bound = followUpSummary.complete && sessionResultsBound;
|
|
124
|
+
const { out, err } = combinedOutput(initial, followUp);
|
|
75
125
|
return {
|
|
76
126
|
code: bound ? 0 : followUp.code || 1,
|
|
77
127
|
out,
|
|
@@ -79,16 +129,11 @@ export async function runClaudeTurns(options: {
|
|
|
79
129
|
initialOut: initial.out,
|
|
80
130
|
followUpOut: followUp.out,
|
|
81
131
|
sessionResultsBound,
|
|
82
|
-
continuation:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
preFollowUpWorktreeUnchanged: measured
|
|
89
|
-
? initialFingerprint === beforeFollowUp
|
|
90
|
-
: null,
|
|
91
|
-
},
|
|
92
|
-
},
|
|
132
|
+
continuation: continuationObservation(
|
|
133
|
+
bound,
|
|
134
|
+
sessionId,
|
|
135
|
+
initialFingerprint,
|
|
136
|
+
beforeFollowUp,
|
|
137
|
+
),
|
|
93
138
|
};
|
|
94
139
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
@@ -15,7 +14,9 @@ async function savedClaudeCredential(): Promise<string | undefined> {
|
|
|
15
14
|
return path;
|
|
16
15
|
} catch (error) {
|
|
17
16
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
18
|
-
throw new Error("Claude saved credential is unreadable or unavailable"
|
|
17
|
+
throw new Error("Claude saved credential is unreadable or unavailable", {
|
|
18
|
+
cause: error,
|
|
19
|
+
});
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -29,19 +30,7 @@ export async function stageClaudeAuthentication(
|
|
|
29
30
|
configRoot: string,
|
|
30
31
|
sourceFile?: string,
|
|
31
32
|
) {
|
|
32
|
-
const environment
|
|
33
|
-
if (sourceFile === undefined) {
|
|
34
|
-
for (const name of CLAUDE_AUTH_VARIABLES) {
|
|
35
|
-
const value = process.env[name];
|
|
36
|
-
if (value) {
|
|
37
|
-
if (Buffer.byteLength(value, "utf8") > MAX_CREDENTIAL_BYTES)
|
|
38
|
-
throw new Error(
|
|
39
|
-
"Claude environment credential exceeds the size limit",
|
|
40
|
-
);
|
|
41
|
-
environment[name] = value;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
33
|
+
const environment = authenticationEnvironment(sourceFile);
|
|
45
34
|
if (Object.keys(environment).length) {
|
|
46
35
|
await mkdir(configRoot, { recursive: true, mode: 0o700 });
|
|
47
36
|
return {
|
|
@@ -55,45 +44,23 @@ export async function stageClaudeAuthentication(
|
|
|
55
44
|
};
|
|
56
45
|
}
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
throw new Error("Claude
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
const timer = setTimeout(() => proc.kill("SIGKILL"), 10_000);
|
|
74
|
-
const reader = proc.stdout.getReader();
|
|
75
|
-
const chunks: Uint8Array[] = [];
|
|
76
|
-
let size = 0;
|
|
77
|
-
try {
|
|
78
|
-
while (true) {
|
|
79
|
-
const { value, done } = await reader.read();
|
|
80
|
-
if (done) break;
|
|
81
|
-
size += value.byteLength;
|
|
82
|
-
if (size > MAX_CREDENTIAL_BYTES)
|
|
83
|
-
throw new Error("Claude credential exceeds the size limit");
|
|
84
|
-
chunks.push(value);
|
|
47
|
+
function boundedEnvironmentCredential(value: string): string {
|
|
48
|
+
if (Buffer.byteLength(value, "utf8") > MAX_CREDENTIAL_BYTES)
|
|
49
|
+
throw new Error("Claude environment credential exceeds the size limit");
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function authenticationEnvironment(sourceFile: string | undefined) {
|
|
54
|
+
const environment: Record<string, string> = {};
|
|
55
|
+
if (sourceFile === undefined) {
|
|
56
|
+
for (const name of CLAUDE_AUTH_VARIABLES) {
|
|
57
|
+
const value = process.env[name];
|
|
58
|
+
if (value) {
|
|
59
|
+
environment[name] = boundedEnvironmentCredential(value);
|
|
60
|
+
}
|
|
85
61
|
}
|
|
86
|
-
if ((await proc.exited) !== 0)
|
|
87
|
-
throw new Error("Claude keychain credential is unavailable");
|
|
88
|
-
return Buffer.concat(chunks);
|
|
89
|
-
} catch {
|
|
90
|
-
proc.kill("SIGKILL");
|
|
91
|
-
await proc.exited;
|
|
92
|
-
throw new Error("Claude keychain credential is unavailable");
|
|
93
|
-
} finally {
|
|
94
|
-
clearTimeout(timer);
|
|
95
|
-
reader.releaseLock();
|
|
96
62
|
}
|
|
63
|
+
return environment;
|
|
97
64
|
}
|
|
98
65
|
|
|
99
66
|
/** Copy only a bounded login credential into one private host state. */
|
|
@@ -105,21 +72,35 @@ export async function stageClaudeCredential(
|
|
|
105
72
|
await mkdir(configRoot, { recursive: true, mode: 0o700 });
|
|
106
73
|
let credential: Buffer;
|
|
107
74
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
75
|
+
credential = await readCredential(sourceFile);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error("Claude credential is unreadable or unavailable", {
|
|
78
|
+
cause: error,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
requireBoundedCredential(credential);
|
|
82
|
+
const target = join(configRoot, ".credentials.json");
|
|
83
|
+
await writeFile(target, credential, { flag: "wx", mode: 0o600 });
|
|
84
|
+
return target;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readCredential(sourceFile: string | undefined) {
|
|
88
|
+
if (!sourceFile) {
|
|
89
|
+
if (process.platform !== "darwin")
|
|
90
|
+
throw new Error("Claude keychain credential is unavailable");
|
|
91
|
+
const { keychainCredential } = await import("./claude-keychain");
|
|
92
|
+
return keychainCredential();
|
|
115
93
|
}
|
|
94
|
+
if ((await stat(sourceFile)).size > MAX_CREDENTIAL_BYTES)
|
|
95
|
+
throw new Error("oversized credential");
|
|
96
|
+
return readFile(sourceFile);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requireBoundedCredential(credential: Buffer): void {
|
|
116
100
|
if (
|
|
117
101
|
credential.byteLength === 0 ||
|
|
118
102
|
credential.byteLength > MAX_CREDENTIAL_BYTES ||
|
|
119
103
|
!credential.toString("utf8").trim()
|
|
120
104
|
)
|
|
121
105
|
throw new Error("Claude credential is empty or oversized");
|
|
122
|
-
const target = join(configRoot, ".credentials.json");
|
|
123
|
-
await writeFile(target, credential, { flag: "wx", mode: 0o600 });
|
|
124
|
-
return target;
|
|
125
106
|
}
|