@bjoernrochel/sevro 0.1.0-rc.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +22 -6
- package/README.md +5 -1
- package/docs/README.md +19 -15
- package/docs/architecture.md +138 -17
- package/docs/assets/README.md +5 -4
- package/docs/builtin-graders-v1.md +9 -2
- package/docs/codex-host.md +155 -57
- package/docs/contracts.md +2 -1
- package/docs/creating-cases.md +9 -2
- package/docs/development-cli.md +187 -19
- package/docs/documentation-quality.md +12 -2
- package/docs/evaluation-workflow.md +110 -0
- package/docs/extension-protocol-v1.md +26 -3
- package/docs/getting-started.md +23 -2
- package/docs/guide-evaluation.md +33 -1
- package/docs/identity-v1.md +25 -2
- package/docs/installing.md +11 -11
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +196 -7
- package/docs/reading-results.md +102 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +31 -12
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +122 -4
- package/docs/run-settings-v1.md +147 -0
- package/docs/runtime-v1.md +255 -22
- package/docs/troubleshooting.md +12 -12
- package/docs/typescript-quality.md +89 -26
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +11 -5
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +43 -7
- package/src/advisory.ts +2 -2
- package/src/candidate-artifacts.ts +42 -11
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +73 -50
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +87 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +60 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +198 -0
- package/src/cli/hosts.ts +55 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +154 -0
- package/src/cli/presentation.ts +93 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +128 -0
- package/src/cli/run-options.ts +403 -0
- package/src/cli/run-overrides.ts +69 -0
- package/src/cli/settings.ts +154 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -591
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +9 -5
- package/src/evaluation/advisory.ts +150 -0
- package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
- package/src/evaluation/grading.ts +235 -0
- package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
- package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +77 -87
- package/src/evaluation/trial.ts +800 -0
- package/src/{evaluation-types.ts → evaluation/types.ts} +79 -29
- package/src/evidence/host-results.ts +85 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +34 -96
- package/src/extension-session.ts +24 -48
- package/src/{advisory-fixture.ts → fixtures/advisory.ts} +12 -72
- package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +183 -0
- package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
- package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
- package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
- package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
- package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
- package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
- package/src/{workspace-state.ts → fixtures/workspace-state.ts} +5 -5
- package/src/generated/cli-result.d.cts +2 -1
- package/src/generated/extension.d.cts +2 -1
- package/src/generated/report.cjs +1 -1
- package/src/generated/report.d.cts +2 -1
- package/src/generated/run-evidence.cjs +1 -1
- package/src/generated/run-evidence.d.cts +2 -1
- package/src/generated/runtime.cjs +1 -1
- package/src/generated/runtime.d.cts +2 -1
- package/src/git.ts +93 -0
- package/src/graders/git-head.ts +29 -84
- package/src/graders/shell.ts +50 -68
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +331 -0
- package/src/hosts/claude-continuation.ts +57 -40
- package/src/hosts/claude-credential.ts +160 -63
- package/src/hosts/claude-events.ts +34 -18
- package/src/hosts/claude-keychain.ts +78 -21
- package/src/hosts/claude-login-recovery.ts +85 -0
- package/src/hosts/claude-login-refresh.ts +178 -0
- package/src/hosts/claude-login-store.ts +162 -0
- package/src/hosts/claude-login.ts +107 -0
- package/src/hosts/claude-native-goal.ts +15 -12
- package/src/hosts/claude-process.ts +10 -50
- package/src/hosts/claude-refresh-lock.ts +81 -0
- package/src/hosts/claude-repository-invocation.ts +9 -34
- package/src/hosts/claude-settings.ts +46 -9
- package/src/hosts/claude-skill-activations.ts +169 -0
- package/src/hosts/claude-state.ts +28 -15
- package/src/hosts/claude-tool-calls.ts +13 -10
- package/src/hosts/claude.ts +121 -81
- package/src/hosts/codex-app-server.ts +78 -52
- package/src/hosts/codex-configuration.ts +5 -10
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-marketplace.ts +12 -10
- package/src/hosts/codex-native-calls.ts +15 -122
- package/src/hosts/codex-process.ts +10 -183
- package/src/hosts/codex-profile.ts +36 -21
- package/src/hosts/codex-session-files.ts +1 -9
- package/src/hosts/codex-state.ts +134 -18
- package/src/hosts/codex.ts +210 -183
- package/src/hosts/isolation-common.ts +8 -11
- package/src/hosts/isolation-roots.ts +2 -2
- package/src/hosts/linux-sandbox.ts +15 -2
- package/src/hosts/mac-sandbox.ts +87 -18
- package/src/hosts/native-controls.ts +72 -22
- package/src/hosts/native-request.ts +55 -0
- package/src/hosts/process-lifecycle.ts +216 -48
- package/src/hosts/protected-worktrees.ts +1 -1
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +577 -0
- package/src/instrumentation.ts +7 -8
- package/src/native-transcript-state.ts +22 -3
- package/src/native-transcript-view.ts +52 -13
- package/src/native-transcripts.ts +26 -11
- package/src/paths.ts +22 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +27 -7
- package/src/prune.ts +300 -0
- package/src/report.ts +63 -36
- package/src/resolved-case.ts +1 -1
- package/src/results-lock.ts +42 -0
- package/src/results.ts +14 -18
- package/src/{runtime-config.ts → runtime/config.ts} +160 -46
- package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
- package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
- package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
- package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
- package/src/{runtime-hooks.ts → runtime/hooks.ts} +32 -22
- package/src/{runtime-paths.ts → runtime/paths.ts} +5 -19
- package/src/{runtime-query.ts → runtime/query.ts} +5 -5
- package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
- package/src/runtime/shared-cache.ts +130 -0
- package/src/{runtime-state.ts → runtime/state.ts} +28 -11
- package/src/schema-types.ts +73 -161
- package/src/shell.ts +4 -0
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +22 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +0 -1
- package/docs/assets/sevro-wordmark.svg +0 -1
- package/docs/cache-ownership-validation.md +0 -49
- package/docs/claude-continuation-validation.md +0 -47
- package/docs/concurrency-validation.md +0 -86
- package/docs/configuration-root-validation.md +0 -70
- package/docs/engine-slice.md +0 -114
- package/docs/evidence/runtime-startup-30/README.md +0 -185
- package/docs/evidence/runtime-startup-30/after.json +0 -319
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
- package/docs/evidence/runtime-startup-30/before.json +0 -403
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
- package/docs/evidence.md +0 -17
- package/docs/fixture-cleanup-validation.md +0 -73
- package/docs/guide-verification.json +0 -239
- package/src/evaluation-context.ts +0 -259
- package/src/evaluation-fixture.ts +0 -302
- package/src/evaluation-run.ts +0 -200
- package/src/evaluation-trial.ts +0 -1016
- package/src/hosts/claude-nested-skills.ts +0 -272
- package/src/hosts/codex-command-output.ts +0 -510
- package/src/hosts/codex-executor-command.ts +0 -291
- package/src/hosts/codex-skill-reads.ts +0 -783
- /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import { validHostTimeout } from "./native-request";
|
|
2
|
+
import { ProcessDeadline } from "../process-lifecycle";
|
|
3
|
+
import {
|
|
4
|
+
captureBoundedBytes,
|
|
5
|
+
captureBoundedText,
|
|
6
|
+
terminateOwnedProcessGroup,
|
|
7
|
+
} from "../process-lifecycle";
|
|
8
|
+
import { HostExecutionFailure, type HostResult } from "../evaluation/types";
|
|
9
|
+
import {
|
|
10
|
+
HostProcessDiagnostics,
|
|
11
|
+
type HostDiagnostics,
|
|
12
|
+
} from "../host-diagnostics";
|
|
13
|
+
|
|
14
|
+
export const MAX_STDERR_BYTES = 64 * 1024;
|
|
1
15
|
export const MAX_EVENT_BYTES = 8 * 1024 * 1024;
|
|
2
|
-
|
|
16
|
+
class HostProcessInterruption extends Error {
|
|
3
17
|
constructor(
|
|
4
18
|
host: string,
|
|
5
19
|
readonly reason: "timeout" | "cancelled",
|
|
@@ -8,61 +22,215 @@ export class HostProcessInterruption extends Error {
|
|
|
8
22
|
}
|
|
9
23
|
}
|
|
10
24
|
|
|
11
|
-
/** Bound bytes before decoding a host's output with strict UTF-8. */
|
|
12
|
-
export async function boundedProcessText(
|
|
13
|
-
stream: ReadableStream<Uint8Array>,
|
|
14
|
-
limit: number,
|
|
15
|
-
exceededMessage: string,
|
|
16
|
-
): Promise<string> {
|
|
17
|
-
const reader = stream.getReader();
|
|
18
|
-
const chunks: Uint8Array[] = [];
|
|
19
|
-
let size = 0;
|
|
20
|
-
for (;;) {
|
|
21
|
-
const { value, done } = await reader.read();
|
|
22
|
-
if (done) break;
|
|
23
|
-
size += value.byteLength;
|
|
24
|
-
if (size > limit) throw new Error(exceededMessage);
|
|
25
|
-
chunks.push(value);
|
|
26
|
-
}
|
|
27
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
28
|
-
Buffer.concat(chunks),
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
/** Race completion against the host deadline and cancellation, then release listeners. */
|
|
33
|
-
|
|
26
|
+
async function awaitHostProcess<T>(
|
|
34
27
|
proc: Bun.Subprocess,
|
|
35
28
|
completed: Promise<T>,
|
|
36
|
-
options:
|
|
37
|
-
host: "Codex" | "Claude";
|
|
38
|
-
timeoutMs: number;
|
|
39
|
-
signal?: AbortSignal;
|
|
40
|
-
stop: (proc: Bun.Subprocess) => void;
|
|
41
|
-
},
|
|
29
|
+
options: HostProcessOptions,
|
|
42
30
|
): Promise<T> {
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
const deadline = new ProcessDeadline({
|
|
32
|
+
...options,
|
|
33
|
+
interruption: (reason) => new HostProcessInterruption(options.host, reason),
|
|
34
|
+
});
|
|
45
35
|
try {
|
|
46
|
-
|
|
47
|
-
timer = setTimeout(() => {
|
|
48
|
-
reject(new HostProcessInterruption(options.host, "timeout"));
|
|
49
|
-
}, options.timeoutMs);
|
|
50
|
-
});
|
|
51
|
-
const aborted = new Promise<never>((_resolve, reject) => {
|
|
52
|
-
if (!options.signal) return;
|
|
53
|
-
cancel = () => {
|
|
54
|
-
reject(new HostProcessInterruption(options.host, "cancelled"));
|
|
55
|
-
};
|
|
56
|
-
if (options.signal.aborted) cancel();
|
|
57
|
-
else options.signal.addEventListener("abort", cancel, { once: true });
|
|
58
|
-
});
|
|
59
|
-
return await Promise.race([completed, timeout, aborted]);
|
|
36
|
+
return await deadline.wait(completed);
|
|
60
37
|
} catch (error) {
|
|
61
|
-
|
|
38
|
+
terminateOwnedProcessGroup(proc);
|
|
62
39
|
await proc.exited;
|
|
63
40
|
throw error;
|
|
64
41
|
} finally {
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
deadline.dispose();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Phase = NonNullable<HostResult["processFailure"]>["phase"];
|
|
47
|
+
export type HostProcessResult = {
|
|
48
|
+
code: number;
|
|
49
|
+
out: string;
|
|
50
|
+
stderr: Uint8Array;
|
|
51
|
+
stderrTruncated: boolean;
|
|
52
|
+
signal: string | null;
|
|
53
|
+
durationMs: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function hostFailureMessage(
|
|
57
|
+
result: HostResult,
|
|
58
|
+
host: string,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
const failure = result.processFailure;
|
|
61
|
+
if (!failure) return undefined;
|
|
62
|
+
const labels = {
|
|
63
|
+
"isolation-preflight": `${host} isolation preflight`,
|
|
64
|
+
"executable-preflight": `${host} executable preflight`,
|
|
65
|
+
execution: `${host} run`,
|
|
66
|
+
};
|
|
67
|
+
if (!Object.hasOwn(labels, failure.phase)) return undefined;
|
|
68
|
+
return `${labels[failure.phase]} ${failureSuffix(failure)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function failureSuffix(
|
|
72
|
+
failure: NonNullable<HostResult["processFailure"]>,
|
|
73
|
+
): string {
|
|
74
|
+
if (failure.reason === "timeout" && validHostTimeout(failure.timeoutMs))
|
|
75
|
+
return `timed out after ${failure.timeoutMs / 1000} s`;
|
|
76
|
+
return failure.reason === "cancelled" ? "cancelled" : "failed";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class HostProcessFailure extends HostExecutionFailure {
|
|
80
|
+
constructor(
|
|
81
|
+
host: "Codex" | "Claude",
|
|
82
|
+
phase: Phase,
|
|
83
|
+
process: HostProcessResult,
|
|
84
|
+
reason: NonNullable<HostResult["processFailure"]>["reason"] = "exit",
|
|
85
|
+
timeoutMs = 60_000,
|
|
86
|
+
) {
|
|
87
|
+
const failure = { phase, reason, timeoutMs };
|
|
88
|
+
const result: HostResult = {
|
|
89
|
+
finalMessage: null,
|
|
90
|
+
complete: false,
|
|
91
|
+
executionFailed: true,
|
|
92
|
+
actualCondition: "passive",
|
|
93
|
+
processFailure: failure,
|
|
94
|
+
artifacts: [
|
|
95
|
+
{
|
|
96
|
+
id: `${host.toLowerCase()}.process`,
|
|
97
|
+
bytes: Buffer.from(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
...failure,
|
|
100
|
+
code: process.code,
|
|
101
|
+
signal: process.signal,
|
|
102
|
+
durationMs: process.durationMs,
|
|
103
|
+
stderrTruncated: process.stderrTruncated,
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
{ id: `${host.toLowerCase()}.stderr`, bytes: process.stderr },
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
super(hostFailureMessage(result, host), result);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface HostProcessOptions {
|
|
115
|
+
host: "Codex" | "Claude";
|
|
116
|
+
diagnostics: HostDiagnostics;
|
|
117
|
+
argv: string[];
|
|
118
|
+
cwd: string;
|
|
119
|
+
env: Record<string, string>;
|
|
120
|
+
input?: string;
|
|
121
|
+
timeoutMs: number;
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
phase?: Phase;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function writeHostInput(
|
|
127
|
+
proc: Bun.Subprocess,
|
|
128
|
+
input?: string,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
if (input === undefined) return;
|
|
131
|
+
if (!proc.stdin || typeof proc.stdin === "number")
|
|
132
|
+
throw new Error("Codex input pipe unavailable");
|
|
133
|
+
await proc.stdin.write(input);
|
|
134
|
+
await proc.stdin.end();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function spawnHostProcess(
|
|
138
|
+
options: HostProcessOptions,
|
|
139
|
+
capture: HostProcessDiagnostics,
|
|
140
|
+
) {
|
|
141
|
+
try {
|
|
142
|
+
return Bun.spawn(options.argv, {
|
|
143
|
+
cwd: options.cwd,
|
|
144
|
+
env: options.env,
|
|
145
|
+
detached: process.platform !== "win32",
|
|
146
|
+
stdin: options.input === undefined ? "ignore" : "pipe",
|
|
147
|
+
stdout: "pipe",
|
|
148
|
+
stderr: "pipe",
|
|
149
|
+
});
|
|
150
|
+
} catch (cause) {
|
|
151
|
+
capture.recordUnstarted();
|
|
152
|
+
throw cause;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function processPhase(options: HostProcessOptions): Phase {
|
|
157
|
+
return options.phase ?? "execution";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function requireHostProcess(options: HostProcessOptions): void {
|
|
161
|
+
if (options.signal?.aborted) throw new Error(`${options.host} run cancelled`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hostCompletion(
|
|
165
|
+
proc: ReturnType<typeof spawnHostProcess>,
|
|
166
|
+
options: HostProcessOptions,
|
|
167
|
+
capture: HostProcessDiagnostics,
|
|
168
|
+
) {
|
|
169
|
+
const started = performance.now();
|
|
170
|
+
const stdout = captureBoundedText(proc.stdout, {
|
|
171
|
+
limit: MAX_EVENT_BYTES,
|
|
172
|
+
overflow: "reject",
|
|
173
|
+
exceeded: () =>
|
|
174
|
+
new Error(`${options.host} event stream exceeds the size limit`),
|
|
175
|
+
observe: (bytes) => {
|
|
176
|
+
capture.stdout.add(bytes);
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const stderr = captureBoundedBytes(proc.stderr, {
|
|
180
|
+
limit: MAX_STDERR_BYTES,
|
|
181
|
+
overflow: "truncate",
|
|
182
|
+
cancelOnError: true,
|
|
183
|
+
observe: (bytes) => {
|
|
184
|
+
capture.stderr.add(bytes);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const input = writeHostInput(proc, options.input);
|
|
188
|
+
const completed = Promise.all([stdout, proc.exited, stderr, input]).then(
|
|
189
|
+
([out, code, err]) => ({
|
|
190
|
+
out,
|
|
191
|
+
code,
|
|
192
|
+
stderr: err.bytes,
|
|
193
|
+
stderrTruncated: err.truncated,
|
|
194
|
+
signal: proc.signalCode ?? null,
|
|
195
|
+
durationMs: performance.now() - started,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
return { completed, drains: [stdout, stderr, input], started };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function runHostProcess(
|
|
202
|
+
options: HostProcessOptions,
|
|
203
|
+
): Promise<HostProcessResult> {
|
|
204
|
+
requireHostProcess(options);
|
|
205
|
+
const capture = new HostProcessDiagnostics(options.diagnostics);
|
|
206
|
+
const proc = spawnHostProcess(options, capture);
|
|
207
|
+
const { completed, drains, started } = hostCompletion(proc, options, capture);
|
|
208
|
+
try {
|
|
209
|
+
const result = await awaitHostProcess(proc, completed, {
|
|
210
|
+
...options,
|
|
211
|
+
});
|
|
212
|
+
await capture.record(proc);
|
|
213
|
+
return result;
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
await Promise.allSettled(drains);
|
|
216
|
+
if (!(cause instanceof HostProcessInterruption)) {
|
|
217
|
+
await capture.record(proc, "error");
|
|
218
|
+
throw cause;
|
|
219
|
+
}
|
|
220
|
+
await capture.record(proc, cause.reason);
|
|
221
|
+
throw new HostProcessFailure(
|
|
222
|
+
options.host,
|
|
223
|
+
processPhase(options),
|
|
224
|
+
{
|
|
225
|
+
code: await proc.exited,
|
|
226
|
+
out: capture.stdout.bytes().toString("utf8"),
|
|
227
|
+
stderr: capture.stderr.bytes(),
|
|
228
|
+
stderrTruncated: capture.stderr.truncated,
|
|
229
|
+
signal: proc.signalCode ?? null,
|
|
230
|
+
durationMs: performance.now() - started,
|
|
231
|
+
},
|
|
232
|
+
cause.reason,
|
|
233
|
+
options.timeoutMs,
|
|
234
|
+
);
|
|
67
235
|
}
|
|
68
236
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import type { Dirent } from "node:fs";
|
|
5
|
+
import type { HookSkill } from "./skill-hook-handler";
|
|
6
|
+
import { isRecord } from "../value-guards";
|
|
7
|
+
|
|
8
|
+
const MAX_SKILLS = 128;
|
|
9
|
+
const MAX_SKILL_BYTES = 1024 * 1024;
|
|
10
|
+
const MAX_CATALOG_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
interface Catalog {
|
|
12
|
+
skills: HookSkill[];
|
|
13
|
+
complete: boolean;
|
|
14
|
+
bytes: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SkillCatalogRoot {
|
|
17
|
+
directory: string;
|
|
18
|
+
boundary: string;
|
|
19
|
+
namespace?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function codexSkillCatalogRoots(
|
|
23
|
+
workspace: string,
|
|
24
|
+
pluginRoots: string[],
|
|
25
|
+
nativeSkillRoots: SkillCatalogRoot[],
|
|
26
|
+
): Promise<SkillCatalogRoot[]> {
|
|
27
|
+
const roots: SkillCatalogRoot[] = [
|
|
28
|
+
{ directory: join(workspace, ".agents/skills"), boundary: workspace },
|
|
29
|
+
...nativeSkillRoots,
|
|
30
|
+
];
|
|
31
|
+
for (const root of pluginRoots) {
|
|
32
|
+
const manifest: unknown = JSON.parse(
|
|
33
|
+
await readFile(join(root, ".claude-plugin/plugin.json"), "utf8"),
|
|
34
|
+
);
|
|
35
|
+
if (!isRecord(manifest) || typeof manifest.name !== "string")
|
|
36
|
+
throw new Error("invalid skill catalog plugin manifest");
|
|
37
|
+
roots.push({
|
|
38
|
+
directory: join(root, "skills"),
|
|
39
|
+
boundary: root,
|
|
40
|
+
namespace: manifest.name,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return roots;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Snapshot only known skill mounts; shell syntax does not enter discovery. */
|
|
47
|
+
export async function skillHookCatalog(roots: SkillCatalogRoot[]) {
|
|
48
|
+
const catalog: Catalog = { skills: [], complete: true, bytes: 0 };
|
|
49
|
+
for (const root of roots) await readRoot(root, catalog);
|
|
50
|
+
return { skills: catalog.skills, catalogComplete: catalog.complete };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readRoot(root: SkillCatalogRoot, catalog: Catalog) {
|
|
54
|
+
try {
|
|
55
|
+
await requireCatalogRoot(root);
|
|
56
|
+
const entries = await readdir(root.directory, { withFileTypes: true });
|
|
57
|
+
const directories = catalogDirectories(entries, catalog);
|
|
58
|
+
for (const entry of directories.sort((a, b) =>
|
|
59
|
+
a.name.localeCompare(b.name),
|
|
60
|
+
)) {
|
|
61
|
+
await readSkill(root, entry.name, catalog);
|
|
62
|
+
}
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
if (!missing(cause)) catalog.complete = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function catalogDirectories(entries: Dirent[], catalog: Catalog): Dirent[] {
|
|
69
|
+
const directories: Dirent[] = [];
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (entry.isSymbolicLink()) catalog.complete = false;
|
|
72
|
+
if (entry.isDirectory() && /^[A-Za-z0-9._-]+$/.test(entry.name))
|
|
73
|
+
directories.push(entry);
|
|
74
|
+
}
|
|
75
|
+
return directories;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function requireCatalogRoot(root: SkillCatalogRoot) {
|
|
79
|
+
const expected = resolve(
|
|
80
|
+
await realpath(root.boundary),
|
|
81
|
+
relative(root.boundary, root.directory),
|
|
82
|
+
);
|
|
83
|
+
if ((await realpath(root.directory)) !== expected)
|
|
84
|
+
throw new Error("redirected skill catalog root");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function missing(cause: unknown): boolean {
|
|
88
|
+
return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readSkill(
|
|
92
|
+
root: SkillCatalogRoot,
|
|
93
|
+
name: string,
|
|
94
|
+
catalog: Catalog,
|
|
95
|
+
) {
|
|
96
|
+
try {
|
|
97
|
+
const path = join(root.directory, name, "SKILL.md");
|
|
98
|
+
const file = await lstat(path);
|
|
99
|
+
if (!readableSkill(file)) {
|
|
100
|
+
catalog.complete = false;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const bytes = await readFile(path);
|
|
104
|
+
if (bytes.length > MAX_SKILL_BYTES)
|
|
105
|
+
throw new Error("skill changed during catalog read");
|
|
106
|
+
addSkill(bytes, [root.namespace, name].filter(Boolean).join(":"), catalog);
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
if (!missing(cause)) catalog.complete = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readableSkill(file: Awaited<ReturnType<typeof lstat>>): boolean {
|
|
113
|
+
return file.isFile() && file.size <= MAX_SKILL_BYTES;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function addSkill(bytes: Buffer, invocation: string, catalog: Catalog) {
|
|
117
|
+
if (!bytes.length) return;
|
|
118
|
+
catalog.bytes += bytes.length;
|
|
119
|
+
if (
|
|
120
|
+
catalog.skills.length >= MAX_SKILLS ||
|
|
121
|
+
catalog.bytes > MAX_CATALOG_BYTES
|
|
122
|
+
) {
|
|
123
|
+
catalog.complete = false;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
catalog.skills.push({
|
|
127
|
+
invocation,
|
|
128
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
129
|
+
body: bytes.toString("utf8"),
|
|
130
|
+
});
|
|
131
|
+
}
|