@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/extension-client.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import validExchange from "./generated/extension.cjs";
|
|
3
|
+
import { isRecord, isStringArray } from "./value-guards";
|
|
4
|
+
import {
|
|
5
|
+
captureBoundedBytes,
|
|
6
|
+
ProcessDeadline,
|
|
7
|
+
terminateOwnedProcessGroup,
|
|
8
|
+
} from "./process-lifecycle";
|
|
4
9
|
|
|
5
10
|
const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
6
11
|
const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
|
|
7
12
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
13
|
const PROTOCOL = "sevro.extension.v1";
|
|
9
14
|
|
|
10
|
-
const ajv = new Ajv2020({
|
|
11
|
-
strict: true,
|
|
12
|
-
strictRequired: false,
|
|
13
|
-
strictTypes: false,
|
|
14
|
-
});
|
|
15
|
-
const validExchange = ajv.compile(extensionSchema);
|
|
16
|
-
|
|
17
15
|
export class ExtensionProtocolError extends Error {
|
|
18
|
-
constructor(message: string) {
|
|
19
|
-
super(message);
|
|
16
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
17
|
+
super(message, options);
|
|
20
18
|
this.name = "ExtensionProtocolError";
|
|
21
19
|
}
|
|
22
20
|
}
|
|
@@ -42,163 +40,218 @@ export interface ExchangeOptions {
|
|
|
42
40
|
signal?: AbortSignal;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
truncate: boolean,
|
|
49
|
-
): Promise<Uint8Array> {
|
|
50
|
-
const reader = stream.getReader();
|
|
51
|
-
const chunks: Uint8Array[] = [];
|
|
52
|
-
let size = 0;
|
|
53
|
-
while (true) {
|
|
54
|
-
const { value, done } = await reader.read();
|
|
55
|
-
if (done) break;
|
|
56
|
-
if (!truncate && size + value.byteLength > limit)
|
|
57
|
-
throw new ExtensionProtocolError("extension message exceeds 8 MiB");
|
|
58
|
-
const kept = value.subarray(0, Math.max(0, limit - size));
|
|
59
|
-
if (kept.byteLength) chunks.push(kept);
|
|
60
|
-
size += kept.byteLength;
|
|
61
|
-
}
|
|
62
|
-
const result = new Uint8Array(size);
|
|
63
|
-
let offset = 0;
|
|
64
|
-
for (const chunk of chunks) {
|
|
65
|
-
result.set(chunk, offset);
|
|
66
|
-
offset += chunk.byteLength;
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
43
|
+
interface SuccessfulExtensionResponse extends ExtensionResponse {
|
|
44
|
+
result: Record<string, unknown>;
|
|
45
|
+
error?: never;
|
|
69
46
|
}
|
|
70
47
|
|
|
71
|
-
function
|
|
72
|
-
if (process.platform !== "win32") {
|
|
73
|
-
try {
|
|
74
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
75
|
-
return;
|
|
76
|
-
} catch {
|
|
77
|
-
// The group may have exited while the runner was handling the error.
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
proc.kill("SIGKILL");
|
|
82
|
-
} catch {
|
|
83
|
-
// The process already exited.
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Run exactly one request in a fresh extension process. */
|
|
88
|
-
export async function exchangeExtension(
|
|
89
|
-
command: string[],
|
|
90
|
-
request: ExtensionRequest,
|
|
91
|
-
options: ExchangeOptions = {},
|
|
92
|
-
): Promise<ExtensionResponse> {
|
|
48
|
+
function validateExtensionCommand(command: string[]): void {
|
|
93
49
|
if (!command.length || command.some((part) => !part))
|
|
94
50
|
throw new ExtensionProtocolError(
|
|
95
51
|
"extension command must be a nonempty argv array",
|
|
96
52
|
);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extensionTimeout(value: number | undefined): number {
|
|
56
|
+
const timeout = value ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0)
|
|
103
58
|
throw new ExtensionProtocolError(
|
|
104
59
|
"extension timeout must be a positive integer",
|
|
105
60
|
);
|
|
61
|
+
return timeout;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extensionPayload(request: ExtensionRequest): Uint8Array {
|
|
106
65
|
const payload = new TextEncoder().encode(JSON.stringify(request));
|
|
107
66
|
if (payload.byteLength > MAX_MESSAGE_BYTES)
|
|
108
67
|
throw new ExtensionProtocolError("extension request exceeds 8 MiB");
|
|
68
|
+
return payload;
|
|
69
|
+
}
|
|
109
70
|
|
|
110
|
-
|
|
71
|
+
function spawnExtension(
|
|
72
|
+
command: string[],
|
|
73
|
+
options: ExchangeOptions,
|
|
74
|
+
): Bun.Subprocess {
|
|
111
75
|
try {
|
|
112
|
-
|
|
76
|
+
return Bun.spawn(command, {
|
|
113
77
|
cwd: options.cwd,
|
|
114
78
|
stdin: "pipe",
|
|
115
79
|
stdout: "pipe",
|
|
116
80
|
stderr: "pipe",
|
|
117
81
|
detached: process.platform !== "win32",
|
|
118
82
|
});
|
|
119
|
-
} catch {
|
|
120
|
-
throw new ExtensionProtocolError("could not start extension process"
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
throw new ExtensionProtocolError("could not start extension process", {
|
|
85
|
+
cause,
|
|
86
|
+
});
|
|
121
87
|
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extensionWriter(proc: Bun.Subprocess) {
|
|
91
|
+
const stdin = proc.stdin;
|
|
92
|
+
if (!stdin || typeof stdin === "number")
|
|
93
|
+
throw new ExtensionProtocolError("extension pipes are unavailable");
|
|
94
|
+
return stdin;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extensionReaders(proc: Bun.Subprocess) {
|
|
98
|
+
const { stdout, stderr } = proc;
|
|
99
|
+
if (
|
|
100
|
+
!stdout ||
|
|
101
|
+
typeof stdout === "number" ||
|
|
102
|
+
!stderr ||
|
|
103
|
+
typeof stderr === "number"
|
|
104
|
+
)
|
|
105
|
+
throw new ExtensionProtocolError("extension pipes are unavailable");
|
|
106
|
+
return { stdout, stderr };
|
|
107
|
+
}
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
async function writeExtensionRequest(
|
|
110
|
+
stdin: ReturnType<typeof extensionWriter>,
|
|
111
|
+
payload: Uint8Array,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
await stdin.write(payload);
|
|
114
|
+
await stdin.end();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasResponseBody(value: Record<string, unknown>): boolean {
|
|
118
|
+
return Object.hasOwn(value, "result") || Object.hasOwn(value, "error");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assertExtensionResponse(
|
|
122
|
+
value: unknown,
|
|
123
|
+
): asserts value is ExtensionResponse {
|
|
124
|
+
if (!validExchange(value) || !isRecord(value) || !hasResponseBody(value))
|
|
125
|
+
throw new ExtensionProtocolError("invalid extension response");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseExtensionResponse(bytes: Uint8Array): ExtensionResponse {
|
|
129
|
+
let response: unknown;
|
|
125
130
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
() => reject(new ExtensionProtocolError("extension timed out")),
|
|
181
|
-
timeoutMs,
|
|
182
|
-
);
|
|
183
|
-
});
|
|
184
|
-
const aborted = new Promise<never>((_resolve, reject) => {
|
|
185
|
-
if (!options.signal) return;
|
|
186
|
-
cancel = () =>
|
|
187
|
-
reject(new ExtensionProtocolError("extension request cancelled"));
|
|
188
|
-
if (options.signal.aborted) cancel();
|
|
189
|
-
else options.signal.addEventListener("abort", cancel, { once: true });
|
|
131
|
+
response = JSON.parse(
|
|
132
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
133
|
+
);
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
throw new ExtensionProtocolError(
|
|
136
|
+
"extension did not return one UTF-8 JSON response",
|
|
137
|
+
{ cause },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
assertExtensionResponse(response);
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateResponseIdentity(
|
|
145
|
+
response: ExtensionResponse,
|
|
146
|
+
request: ExtensionRequest,
|
|
147
|
+
): void {
|
|
148
|
+
if (
|
|
149
|
+
response.id !== request.id ||
|
|
150
|
+
response.protocol !== request.protocol ||
|
|
151
|
+
response.method !== request.method
|
|
152
|
+
)
|
|
153
|
+
throw new ExtensionProtocolError(
|
|
154
|
+
"extension response does not match request",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function assertSuccessfulResponse(
|
|
159
|
+
response: ExtensionResponse,
|
|
160
|
+
): asserts response is SuccessfulExtensionResponse {
|
|
161
|
+
if (response.error)
|
|
162
|
+
throw new ExtensionProtocolError(
|
|
163
|
+
`extension reported ${response.error.code}`,
|
|
164
|
+
);
|
|
165
|
+
if (response.result === undefined)
|
|
166
|
+
throw new ExtensionProtocolError("invalid extension response");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class ExtensionExchange {
|
|
170
|
+
private readonly deadline: ProcessDeadline;
|
|
171
|
+
constructor(
|
|
172
|
+
private readonly proc: Bun.Subprocess,
|
|
173
|
+
options: ExchangeOptions,
|
|
174
|
+
timeoutMs: number,
|
|
175
|
+
) {
|
|
176
|
+
this.deadline = new ProcessDeadline({
|
|
177
|
+
timeoutMs,
|
|
178
|
+
signal: options.signal,
|
|
179
|
+
interruption: (reason) =>
|
|
180
|
+
new ExtensionProtocolError(
|
|
181
|
+
reason === "timeout"
|
|
182
|
+
? "extension timed out"
|
|
183
|
+
: "extension request cancelled",
|
|
184
|
+
),
|
|
190
185
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async output(
|
|
189
|
+
request: ExtensionRequest,
|
|
190
|
+
payload: Uint8Array,
|
|
191
|
+
): Promise<SuccessfulExtensionResponse> {
|
|
192
|
+
const stdin = extensionWriter(this.proc);
|
|
193
|
+
const { stdout, stderr } = extensionReaders(this.proc);
|
|
194
|
+
const write = writeExtensionRequest(stdin, payload);
|
|
195
|
+
const [{ bytes }, , exitCode] = await Promise.all([
|
|
196
|
+
captureBoundedBytes(stdout, {
|
|
197
|
+
limit: MAX_MESSAGE_BYTES,
|
|
198
|
+
overflow: "reject",
|
|
199
|
+
exceeded: () =>
|
|
200
|
+
new ExtensionProtocolError("extension message exceeds 8 MiB"),
|
|
201
|
+
}),
|
|
202
|
+
captureBoundedBytes(stderr, {
|
|
203
|
+
limit: MAX_DIAGNOSTIC_BYTES,
|
|
204
|
+
overflow: "truncate",
|
|
205
|
+
}),
|
|
206
|
+
this.proc.exited,
|
|
207
|
+
write,
|
|
208
|
+
]);
|
|
209
|
+
if (exitCode !== 0)
|
|
210
|
+
throw new ExtensionProtocolError(
|
|
211
|
+
`extension exited with code ${exitCode}`,
|
|
212
|
+
);
|
|
213
|
+
const response = parseExtensionResponse(bytes);
|
|
214
|
+
validateResponseIdentity(response, request);
|
|
215
|
+
assertSuccessfulResponse(response);
|
|
216
|
+
return response;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async run(
|
|
220
|
+
request: ExtensionRequest,
|
|
221
|
+
payload: Uint8Array,
|
|
222
|
+
): Promise<SuccessfulExtensionResponse> {
|
|
223
|
+
try {
|
|
224
|
+
return await this.deadline.wait(this.output(request, payload));
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
terminateOwnedProcessGroup(this.proc);
|
|
227
|
+
if (cause instanceof ExtensionProtocolError) throw cause;
|
|
228
|
+
throw new ExtensionProtocolError("extension transport failed", { cause });
|
|
229
|
+
} finally {
|
|
230
|
+
this.deadline.dispose();
|
|
231
|
+
}
|
|
199
232
|
}
|
|
200
233
|
}
|
|
201
234
|
|
|
235
|
+
/** Run exactly one request in a fresh extension process. */
|
|
236
|
+
export async function exchangeExtension(
|
|
237
|
+
command: string[],
|
|
238
|
+
request: ExtensionRequest,
|
|
239
|
+
options: ExchangeOptions = {},
|
|
240
|
+
): Promise<SuccessfulExtensionResponse> {
|
|
241
|
+
validateExtensionCommand(command);
|
|
242
|
+
if (!validExchange(request))
|
|
243
|
+
throw new ExtensionProtocolError("invalid extension request");
|
|
244
|
+
if (options.signal?.aborted)
|
|
245
|
+
throw new ExtensionProtocolError("extension request cancelled");
|
|
246
|
+
const timeout = extensionTimeout(options.timeoutMs);
|
|
247
|
+
const payload = extensionPayload(request);
|
|
248
|
+
return new ExtensionExchange(
|
|
249
|
+
spawnExtension(command, options),
|
|
250
|
+
options,
|
|
251
|
+
timeout,
|
|
252
|
+
).run(request, payload);
|
|
253
|
+
}
|
|
254
|
+
|
|
202
255
|
export interface NegotiatedExtension {
|
|
203
256
|
id: string;
|
|
204
257
|
version: string;
|
|
@@ -208,6 +261,33 @@ export interface NegotiatedExtension {
|
|
|
208
261
|
taskVerdictPolicies: string[];
|
|
209
262
|
}
|
|
210
263
|
|
|
264
|
+
function descriptionStrings(value: unknown): string[] {
|
|
265
|
+
if (!isStringArray(value))
|
|
266
|
+
throw new ExtensionProtocolError("invalid extension response");
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function describedIdentity(value: unknown): { id: string; version: string } {
|
|
271
|
+
if (
|
|
272
|
+
!isRecord(value) ||
|
|
273
|
+
typeof value.id !== "string" ||
|
|
274
|
+
typeof value.version !== "string"
|
|
275
|
+
)
|
|
276
|
+
throw new ExtensionProtocolError("invalid extension response");
|
|
277
|
+
return { id: value.id, version: value.version };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function descriptionResult(value: Record<string, unknown>) {
|
|
281
|
+
return {
|
|
282
|
+
protocols: descriptionStrings(value.protocols),
|
|
283
|
+
requiredCapabilities: descriptionStrings(value.requiredCapabilities),
|
|
284
|
+
optionalCapabilities: descriptionStrings(value.optionalCapabilities),
|
|
285
|
+
extension: describedIdentity(value.extension),
|
|
286
|
+
graders: descriptionStrings(value.graders),
|
|
287
|
+
taskVerdictPolicies: descriptionStrings(value.taskVerdictPolicies),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
211
291
|
/** Discover an extension and select the v1 protocol and available capabilities. */
|
|
212
292
|
export async function negotiateExtension(
|
|
213
293
|
command: string[],
|
|
@@ -228,30 +308,30 @@ export async function negotiateExtension(
|
|
|
228
308
|
},
|
|
229
309
|
options,
|
|
230
310
|
);
|
|
231
|
-
const result = response.result
|
|
232
|
-
const offered = result.protocols
|
|
311
|
+
const result = descriptionResult(response.result);
|
|
312
|
+
const offered = result.protocols;
|
|
233
313
|
if (!offered.includes(PROTOCOL))
|
|
234
314
|
throw new ExtensionProtocolError("extension has no compatible protocol");
|
|
235
315
|
const supported = new Set([
|
|
236
316
|
...available.engineCapabilities,
|
|
237
317
|
...available.hostCapabilities,
|
|
238
318
|
]);
|
|
239
|
-
const required = result.requiredCapabilities
|
|
319
|
+
const required = result.requiredCapabilities;
|
|
240
320
|
for (const capability of required)
|
|
241
321
|
if (!supported.has(capability))
|
|
242
322
|
throw new ExtensionProtocolError(
|
|
243
323
|
`unsupported required capability: ${capability}`,
|
|
244
324
|
);
|
|
245
|
-
const optional =
|
|
325
|
+
const optional = result.optionalCapabilities.filter((item) =>
|
|
246
326
|
supported.has(item),
|
|
247
327
|
);
|
|
248
|
-
const extension = result.extension
|
|
328
|
+
const extension = result.extension;
|
|
249
329
|
return {
|
|
250
330
|
id: extension.id,
|
|
251
331
|
version: extension.version,
|
|
252
332
|
protocol: PROTOCOL,
|
|
253
333
|
capabilities: [...new Set([...required, ...optional])],
|
|
254
|
-
graders: result.graders
|
|
255
|
-
taskVerdictPolicies: result.taskVerdictPolicies
|
|
334
|
+
graders: result.graders,
|
|
335
|
+
taskVerdictPolicies: result.taskVerdictPolicies,
|
|
256
336
|
};
|
|
257
337
|
}
|