@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
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import { nativeCommandOutputs } from "./codex-command-output";
|
|
2
|
-
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
|
|
5
|
-
const MAX_SKILL_BYTES = 1024 * 1024;
|
|
6
|
-
const MAX_NATIVE_READ_ATTEMPTS = 64;
|
|
7
|
-
const SKILL_PATH = /^\.agents\/skills\/([A-Za-z0-9._-]+)\/SKILL\.md$/;
|
|
8
|
-
const PLUGIN_SKILL_PATH = /^skills\/([A-Za-z0-9._-]+)\/SKILL\.md$/;
|
|
9
|
-
|
|
10
|
-
interface CodexItem {
|
|
11
|
-
id?: unknown;
|
|
12
|
-
type?: unknown;
|
|
13
|
-
command?: unknown;
|
|
14
|
-
status?: unknown;
|
|
15
|
-
exit_code?: unknown;
|
|
16
|
-
aggregated_output?: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface CodexEvent {
|
|
20
|
-
type?: unknown;
|
|
21
|
-
item?: CodexItem;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function shellPayload(command: string): string {
|
|
25
|
-
let payload = command;
|
|
26
|
-
for (let depth = 0; depth < 2; depth++) {
|
|
27
|
-
const wrapper = payload.match(
|
|
28
|
-
/^(?:\/bin\/(?:ba|z)?sh\s+-l?c|lean-ctx\s+-c)\s+(["'])([\s\S]*)\1$/,
|
|
29
|
-
);
|
|
30
|
-
if (!wrapper) break;
|
|
31
|
-
payload = wrapper[2]!;
|
|
32
|
-
}
|
|
33
|
-
return payload;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface DirectRead {
|
|
37
|
-
path: string;
|
|
38
|
-
firstLine?: number;
|
|
39
|
-
lastLine?: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function directRead(command: string): DirectRead | null {
|
|
43
|
-
const payload = shellPayload(command).trim();
|
|
44
|
-
const path = "(?:\"([^\"]+)\"|'([^']+)'|([^\\s'\"`$;&|<>]+))";
|
|
45
|
-
const cat = payload.match(
|
|
46
|
-
new RegExp(`^(?:/bin/)?cat\\s+(?:--\\s+)?${path}$`),
|
|
47
|
-
);
|
|
48
|
-
if (cat) return { path: cat[1] ?? cat[2] ?? cat[3]! };
|
|
49
|
-
const sed = payload.match(
|
|
50
|
-
new RegExp(`^(?:/bin/)?sed\\s+-n\\s+['"]?(\\d+),(\\d+)p['"]?\\s+${path}$`),
|
|
51
|
-
);
|
|
52
|
-
if (!sed) return null;
|
|
53
|
-
const firstLine = Number(sed[1]);
|
|
54
|
-
const lastLine = Number(sed[2]);
|
|
55
|
-
if (
|
|
56
|
-
!Number.isSafeInteger(firstLine) ||
|
|
57
|
-
firstLine < 1 ||
|
|
58
|
-
!Number.isSafeInteger(lastLine) ||
|
|
59
|
-
lastLine < firstLine
|
|
60
|
-
)
|
|
61
|
-
return null;
|
|
62
|
-
return { path: sed[3] ?? sed[4] ?? sed[5]!, firstLine, lastLine };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function within(root: string, path: string): boolean {
|
|
66
|
-
const child = relative(root, path);
|
|
67
|
-
return child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function readRange(body: string, read: DirectRead): [number, number] {
|
|
71
|
-
if (read.firstLine === undefined || read.lastLine === undefined)
|
|
72
|
-
return [0, body.length];
|
|
73
|
-
const lines = body.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
74
|
-
const start = lines.slice(0, read.firstLine - 1).join("").length;
|
|
75
|
-
const end = lines.slice(0, read.lastLine).join("").length;
|
|
76
|
-
return [start, end];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function verifiedSkillRead(
|
|
80
|
-
item: CodexItem,
|
|
81
|
-
workspace: string,
|
|
82
|
-
pluginRoots: string[],
|
|
83
|
-
commandCwd = workspace,
|
|
84
|
-
): Promise<{
|
|
85
|
-
skill: string;
|
|
86
|
-
path: string;
|
|
87
|
-
range: [number, number];
|
|
88
|
-
bodyLength: number;
|
|
89
|
-
} | null> {
|
|
90
|
-
if (typeof item.command !== "string") return null;
|
|
91
|
-
const read = directRead(item.command);
|
|
92
|
-
if (!read || typeof item.aggregated_output !== "string") return null;
|
|
93
|
-
const resolved = resolve(commandCwd, read.path);
|
|
94
|
-
const actual = await realpath(resolved).catch(() => null);
|
|
95
|
-
if (!actual) return null;
|
|
96
|
-
const mountedMatch = relative(workspace, actual)
|
|
97
|
-
.split(sep)
|
|
98
|
-
.join("/")
|
|
99
|
-
.match(SKILL_PATH);
|
|
100
|
-
const pluginMatch = pluginRoots
|
|
101
|
-
.map((root) =>
|
|
102
|
-
relative(root, actual).split(sep).join("/").match(PLUGIN_SKILL_PATH),
|
|
103
|
-
)
|
|
104
|
-
.find((match) => match !== null);
|
|
105
|
-
const match = mountedMatch ?? pluginMatch;
|
|
106
|
-
if (!match) return null;
|
|
107
|
-
const body = await readFile(actual).catch(() => null);
|
|
108
|
-
if (!body || body.byteLength > MAX_SKILL_BYTES) return null;
|
|
109
|
-
const text = body.toString("utf8");
|
|
110
|
-
const skill = match[1]!;
|
|
111
|
-
if (!text.startsWith("---\n") || !text.includes(`\nname: ${skill}\n`))
|
|
112
|
-
return null;
|
|
113
|
-
const range = readRange(text, read);
|
|
114
|
-
if (range[0] === range[1]) return null;
|
|
115
|
-
if (!item.aggregated_output.includes(text.slice(...range))) return null;
|
|
116
|
-
return { skill, path: actual, range, bodyLength: text.length };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function fullCoverage(
|
|
120
|
-
ranges: Array<[number, number]>,
|
|
121
|
-
length: number,
|
|
122
|
-
): boolean {
|
|
123
|
-
const ordered = ranges.sort((left, right) => left[0] - right[0]);
|
|
124
|
-
let covered = 0;
|
|
125
|
-
for (const [start, end] of ordered) {
|
|
126
|
-
if (start > covered) return false;
|
|
127
|
-
covered = Math.max(covered, end);
|
|
128
|
-
}
|
|
129
|
-
return covered === length;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface NativeEntry {
|
|
133
|
-
ordinal: number;
|
|
134
|
-
payload: Record<string, unknown>;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export interface NativeReadDiagnostic {
|
|
138
|
-
completeness: "complete" | "partial";
|
|
139
|
-
observedSkills: string[];
|
|
140
|
-
completedReads: Array<{ skill: string; ordinal: number }>;
|
|
141
|
-
recoverySources: Array<{
|
|
142
|
-
ordinal: number;
|
|
143
|
-
nativeOutput: boolean;
|
|
144
|
-
yieldedChunks: number;
|
|
145
|
-
completedCall: boolean;
|
|
146
|
-
literalCommandCall: boolean;
|
|
147
|
-
}>;
|
|
148
|
-
commandExecutions: number;
|
|
149
|
-
readAttempts: number;
|
|
150
|
-
truncated: boolean;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function nativeCommandText(value: unknown): string | null {
|
|
154
|
-
if (typeof value === "string") return value;
|
|
155
|
-
if (!Array.isArray(value) || !value.every((part) => typeof part === "string"))
|
|
156
|
-
return null;
|
|
157
|
-
if (value.length >= 3 && (value[1] === "-lc" || value[1] === "-c"))
|
|
158
|
-
return value.slice(2).join(" ");
|
|
159
|
-
return value.join(" ");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Verify native skill-file reads without retaining commands or output. */
|
|
163
|
-
export async function codexNativeReadDiagnostic(
|
|
164
|
-
entries: NativeEntry[],
|
|
165
|
-
workspace: string,
|
|
166
|
-
installedPluginRoots: string[] = [],
|
|
167
|
-
): Promise<NativeReadDiagnostic> {
|
|
168
|
-
const canonicalWorkspace = await realpath(workspace);
|
|
169
|
-
const pluginRoots = await Promise.all(
|
|
170
|
-
installedPluginRoots.map((root) => realpath(root)),
|
|
171
|
-
);
|
|
172
|
-
const recoveredOutputs = nativeCommandOutputs(entries);
|
|
173
|
-
const attempted = new Map<
|
|
174
|
-
string,
|
|
175
|
-
{ ranges: Array<[number, number]>; bodyLength: number }
|
|
176
|
-
>();
|
|
177
|
-
const observedSkills: string[] = [];
|
|
178
|
-
const completedReads: Array<{ skill: string; ordinal: number }> = [];
|
|
179
|
-
const recoverySources: NativeReadDiagnostic["recoverySources"] = [];
|
|
180
|
-
let commandExecutions = 0;
|
|
181
|
-
let readAttempts = 0;
|
|
182
|
-
let partial = false;
|
|
183
|
-
let truncated = false;
|
|
184
|
-
for (const { ordinal, payload } of entries) {
|
|
185
|
-
const item = payload.item;
|
|
186
|
-
if (
|
|
187
|
-
payload.type !== "item_completed" ||
|
|
188
|
-
!item ||
|
|
189
|
-
typeof item !== "object" ||
|
|
190
|
-
Array.isArray(item) ||
|
|
191
|
-
(item as CodexItem).type !== "CommandExecution"
|
|
192
|
-
)
|
|
193
|
-
continue;
|
|
194
|
-
const commandItem = item as CodexItem & { cwd?: unknown };
|
|
195
|
-
commandExecutions++;
|
|
196
|
-
const command = nativeCommandText(commandItem.command);
|
|
197
|
-
if (!command) {
|
|
198
|
-
partial = true;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (!command.includes("SKILL.md")) continue;
|
|
202
|
-
readAttempts++;
|
|
203
|
-
if (readAttempts > MAX_NATIVE_READ_ATTEMPTS) {
|
|
204
|
-
readAttempts = MAX_NATIVE_READ_ATTEMPTS;
|
|
205
|
-
truncated = true;
|
|
206
|
-
partial = true;
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
const recovered = recoveredOutputs.get(ordinal);
|
|
210
|
-
recoverySources.push({
|
|
211
|
-
ordinal,
|
|
212
|
-
nativeOutput:
|
|
213
|
-
typeof commandItem.aggregated_output === "string" &&
|
|
214
|
-
commandItem.aggregated_output.length > 0,
|
|
215
|
-
yieldedChunks: recovered?.chunks ?? 0,
|
|
216
|
-
completedCall: recovered?.completedCall === true,
|
|
217
|
-
literalCommandCall: recovered?.literalCommandCall === true,
|
|
218
|
-
});
|
|
219
|
-
if (
|
|
220
|
-
typeof commandItem.exit_code !== "number" ||
|
|
221
|
-
commandItem.status !== "completed"
|
|
222
|
-
) {
|
|
223
|
-
partial = true;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
const cwd =
|
|
227
|
-
commandItem.cwd === undefined
|
|
228
|
-
? canonicalWorkspace
|
|
229
|
-
: typeof commandItem.cwd === "string" && isAbsolute(commandItem.cwd)
|
|
230
|
-
? await realpath(commandItem.cwd).catch(() => null)
|
|
231
|
-
: null;
|
|
232
|
-
if (!cwd) {
|
|
233
|
-
partial = true;
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
const read = await verifiedSkillRead(
|
|
237
|
-
{
|
|
238
|
-
...commandItem,
|
|
239
|
-
command,
|
|
240
|
-
aggregated_output:
|
|
241
|
-
(recoveredOutputs.get(ordinal)?.output ?? "") +
|
|
242
|
-
(typeof commandItem.aggregated_output === "string"
|
|
243
|
-
? commandItem.aggregated_output
|
|
244
|
-
: ""),
|
|
245
|
-
},
|
|
246
|
-
canonicalWorkspace,
|
|
247
|
-
pluginRoots,
|
|
248
|
-
cwd,
|
|
249
|
-
);
|
|
250
|
-
if (!read) {
|
|
251
|
-
partial = true;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
const prior = attempted.get(read.path);
|
|
255
|
-
const coverage =
|
|
256
|
-
prior && !fullCoverage(prior.ranges, prior.bodyLength)
|
|
257
|
-
? prior
|
|
258
|
-
: {
|
|
259
|
-
ranges: [],
|
|
260
|
-
bodyLength: read.bodyLength,
|
|
261
|
-
};
|
|
262
|
-
coverage.ranges.push(read.range);
|
|
263
|
-
attempted.set(read.path, coverage);
|
|
264
|
-
if (fullCoverage(coverage.ranges, coverage.bodyLength)) {
|
|
265
|
-
completedReads.push({ skill: read.skill, ordinal });
|
|
266
|
-
if (!observedSkills.includes(read.skill)) observedSkills.push(read.skill);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
completeness:
|
|
271
|
-
!partial &&
|
|
272
|
-
[...attempted.values()].every(({ ranges, bodyLength }) =>
|
|
273
|
-
fullCoverage(ranges, bodyLength),
|
|
274
|
-
)
|
|
275
|
-
? "complete"
|
|
276
|
-
: "partial",
|
|
277
|
-
observedSkills,
|
|
278
|
-
completedReads,
|
|
279
|
-
recoverySources,
|
|
280
|
-
commandExecutions,
|
|
281
|
-
readAttempts,
|
|
282
|
-
truncated,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Retain only ordered mounted-skill names, never commands or skill bodies. */
|
|
287
|
-
export async function codexSkillReadObservation(
|
|
288
|
-
stream: string,
|
|
289
|
-
workspace: string,
|
|
290
|
-
installedPluginRoots: string[] = [],
|
|
291
|
-
recoveredReads: Map<string, { command: string; output: string }> = new Map(),
|
|
292
|
-
): Promise<{
|
|
293
|
-
id: "sevro.codex.skill-reads";
|
|
294
|
-
completeness: "complete" | "partial";
|
|
295
|
-
data: {
|
|
296
|
-
method: "skill_file_read_probe";
|
|
297
|
-
primarySkill: string | null;
|
|
298
|
-
observedSkills: string[];
|
|
299
|
-
};
|
|
300
|
-
}> {
|
|
301
|
-
const observedSkills: string[] = [];
|
|
302
|
-
const attempted = new Map<
|
|
303
|
-
string,
|
|
304
|
-
{ ranges: Array<[number, number]>; bodyLength: number }
|
|
305
|
-
>();
|
|
306
|
-
const pending = new Set<string>();
|
|
307
|
-
let partial = false;
|
|
308
|
-
let completedTurn = false;
|
|
309
|
-
const canonicalWorkspace = await realpath(workspace);
|
|
310
|
-
const pluginRoots = await Promise.all(
|
|
311
|
-
installedPluginRoots.map((root) => realpath(root)),
|
|
312
|
-
);
|
|
313
|
-
for (const line of stream.split("\n")) {
|
|
314
|
-
if (!line.trim()) continue;
|
|
315
|
-
const event = JSON.parse(line) as CodexEvent;
|
|
316
|
-
let item = event.item;
|
|
317
|
-
if (typeof item?.id === "string") {
|
|
318
|
-
const recovered = recoveredReads.get(item.id);
|
|
319
|
-
if (
|
|
320
|
-
recovered &&
|
|
321
|
-
typeof item.command === "string" &&
|
|
322
|
-
shellPayload(item.command) === shellPayload(recovered.command)
|
|
323
|
-
)
|
|
324
|
-
item = {
|
|
325
|
-
...item,
|
|
326
|
-
aggregated_output:
|
|
327
|
-
recovered.output +
|
|
328
|
-
(typeof item.aggregated_output === "string"
|
|
329
|
-
? item.aggregated_output
|
|
330
|
-
: ""),
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
if (event.type === "turn.completed") completedTurn = true;
|
|
334
|
-
if (event.type === "turn.failed") partial = true;
|
|
335
|
-
if (!item || item.type !== "command_execution") continue;
|
|
336
|
-
if (event.type === "item.started") {
|
|
337
|
-
if (
|
|
338
|
-
typeof item.command === "string" &&
|
|
339
|
-
item.command.includes("SKILL.md")
|
|
340
|
-
) {
|
|
341
|
-
if (typeof item.id === "string") pending.add(item.id);
|
|
342
|
-
else partial = true;
|
|
343
|
-
}
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
if (event.type !== "item.completed") continue;
|
|
347
|
-
if (typeof item.id === "string") pending.delete(item.id);
|
|
348
|
-
if (typeof item.command !== "string") {
|
|
349
|
-
partial = true;
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
if (!item.command.includes("SKILL.md")) continue;
|
|
353
|
-
if (typeof item.exit_code !== "number" || item.status !== "completed") {
|
|
354
|
-
partial = true;
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const read = await verifiedSkillRead(item, canonicalWorkspace, pluginRoots);
|
|
358
|
-
if (!read) {
|
|
359
|
-
partial = true;
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
const coverage = attempted.get(read.path) ?? {
|
|
363
|
-
ranges: [],
|
|
364
|
-
bodyLength: read.bodyLength,
|
|
365
|
-
};
|
|
366
|
-
coverage.ranges.push(read.range);
|
|
367
|
-
attempted.set(read.path, coverage);
|
|
368
|
-
if (
|
|
369
|
-
fullCoverage(coverage.ranges, coverage.bodyLength) &&
|
|
370
|
-
!observedSkills.includes(read.skill)
|
|
371
|
-
)
|
|
372
|
-
observedSkills.push(read.skill);
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
id: "sevro.codex.skill-reads",
|
|
376
|
-
completeness:
|
|
377
|
-
completedTurn &&
|
|
378
|
-
!partial &&
|
|
379
|
-
pending.size === 0 &&
|
|
380
|
-
[...attempted.values()].every(({ ranges, bodyLength }) =>
|
|
381
|
-
fullCoverage(ranges, bodyLength),
|
|
382
|
-
)
|
|
383
|
-
? "complete"
|
|
384
|
-
: "partial",
|
|
385
|
-
data: {
|
|
386
|
-
method: "skill_file_read_probe",
|
|
387
|
-
primarySkill: observedSkills[0] ?? null,
|
|
388
|
-
observedSkills,
|
|
389
|
-
},
|
|
390
|
-
};
|
|
391
|
-
}
|
package/src/preparation.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute, join, relative, sep } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const MAX_ARTIFACT_BYTES = 8 * 1024 * 1024;
|
|
7
|
-
const MAX_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
8
|
-
|
|
9
|
-
export interface PreparationArtifact {
|
|
10
|
-
id: string;
|
|
11
|
-
relativePath: string;
|
|
12
|
-
sha256: string;
|
|
13
|
-
contentBase64?: string;
|
|
14
|
-
sourceRef?: string;
|
|
15
|
-
gitExclude?: boolean;
|
|
16
|
-
executable?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface InlineArtifact {
|
|
20
|
-
id: string;
|
|
21
|
-
relativePath: string;
|
|
22
|
-
sha256: string;
|
|
23
|
-
bytes: Uint8Array;
|
|
24
|
-
gitExclude?: boolean;
|
|
25
|
-
executable?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface PreparationSources {
|
|
29
|
-
root: string;
|
|
30
|
-
refs: Record<string, string>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function fixtureParts(relativePath: string): string[] {
|
|
34
|
-
if (
|
|
35
|
-
!relativePath ||
|
|
36
|
-
isAbsolute(relativePath) ||
|
|
37
|
-
relativePath.includes("\\") ||
|
|
38
|
-
/:/.test(relativePath)
|
|
39
|
-
)
|
|
40
|
-
throw new Error(`invalid fixture path: ${relativePath}`);
|
|
41
|
-
const parts = relativePath.split("/");
|
|
42
|
-
if (parts.some((part) => !part || part === "." || part === ".."))
|
|
43
|
-
throw new Error(`invalid fixture path: ${relativePath}`);
|
|
44
|
-
return parts;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Refuse fixture or setup-created links before mounting trusted artifact bytes. */
|
|
48
|
-
export async function safePreparationTarget(
|
|
49
|
-
workspace: string,
|
|
50
|
-
relativePath: string,
|
|
51
|
-
): Promise<string> {
|
|
52
|
-
const parts = fixtureParts(relativePath);
|
|
53
|
-
let parent = workspace;
|
|
54
|
-
for (const part of parts.slice(0, -1)) {
|
|
55
|
-
parent = join(parent, part);
|
|
56
|
-
try {
|
|
57
|
-
const entry = await lstat(parent);
|
|
58
|
-
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
59
|
-
throw new Error("preparation artifact traverses a non-directory");
|
|
60
|
-
} catch (error) {
|
|
61
|
-
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
62
|
-
throw error;
|
|
63
|
-
await mkdir(parent, { mode: 0o700 });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const target = join(workspace, ...parts);
|
|
67
|
-
try {
|
|
68
|
-
await lstat(target);
|
|
69
|
-
throw new Error("preparation artifact path already exists");
|
|
70
|
-
} catch (error) {
|
|
71
|
-
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
return target;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function validatePaths(paths: string[]): void {
|
|
78
|
-
const normalized = paths.map((path) =>
|
|
79
|
-
fixtureParts(path).join("/").toLowerCase(),
|
|
80
|
-
);
|
|
81
|
-
for (let index = 0; index < normalized.length; index++) {
|
|
82
|
-
for (let other = 0; other < index; other++) {
|
|
83
|
-
if (
|
|
84
|
-
normalized[index] === normalized[other] ||
|
|
85
|
-
normalized[index]!.startsWith(`${normalized[other]}/`) ||
|
|
86
|
-
normalized[other]!.startsWith(`${normalized[index]}/`)
|
|
87
|
-
)
|
|
88
|
-
throw new Error("fixture paths collide");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Decode declared inline artifacts before creating a candidate workspace. */
|
|
94
|
-
export function prepareInlineArtifacts(
|
|
95
|
-
declarations: PreparationArtifact[],
|
|
96
|
-
fixturePaths: string[],
|
|
97
|
-
): InlineArtifact[] {
|
|
98
|
-
validatePaths([
|
|
99
|
-
...fixturePaths,
|
|
100
|
-
...declarations.map((item) => item.relativePath),
|
|
101
|
-
]);
|
|
102
|
-
const ids = new Set<string>();
|
|
103
|
-
let totalBytes = 0;
|
|
104
|
-
return declarations.map((item) => {
|
|
105
|
-
if (!item.id || ids.has(item.id))
|
|
106
|
-
throw new Error("preparation artifact IDs must be unique and nonempty");
|
|
107
|
-
ids.add(item.id);
|
|
108
|
-
if (item.gitExclude !== undefined && typeof item.gitExclude !== "boolean")
|
|
109
|
-
throw new Error("preparation artifact gitExclude must be boolean");
|
|
110
|
-
if (item.executable !== undefined && typeof item.executable !== "boolean")
|
|
111
|
-
throw new Error("preparation artifact executable must be boolean");
|
|
112
|
-
if (item.gitExclude && /[\u0000-\u001f\u007f]/.test(item.relativePath))
|
|
113
|
-
throw new Error("Git-excluded artifact path contains control characters");
|
|
114
|
-
if (item.sourceRef !== undefined || typeof item.contentBase64 !== "string")
|
|
115
|
-
throw new Error("preparation source references are not supported");
|
|
116
|
-
const encoded = item.contentBase64;
|
|
117
|
-
if (
|
|
118
|
-
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
119
|
-
encoded,
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
throw new Error("preparation artifact content is not canonical base64");
|
|
123
|
-
const bytes = Buffer.from(encoded, "base64");
|
|
124
|
-
if (bytes.toString("base64") !== encoded)
|
|
125
|
-
throw new Error("preparation artifact content is not canonical base64");
|
|
126
|
-
totalBytes += bytes.byteLength;
|
|
127
|
-
if (bytes.byteLength > MAX_ARTIFACT_BYTES || totalBytes > MAX_TOTAL_BYTES)
|
|
128
|
-
throw new Error("preparation artifacts exceed the size limit");
|
|
129
|
-
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
130
|
-
if (actual !== item.sha256)
|
|
131
|
-
throw new Error("preparation artifact digest does not match content");
|
|
132
|
-
return {
|
|
133
|
-
id: item.id,
|
|
134
|
-
relativePath: item.relativePath,
|
|
135
|
-
sha256: item.sha256,
|
|
136
|
-
bytes,
|
|
137
|
-
...(item.gitExclude === undefined ? {} : { gitExclude: item.gitExclude }),
|
|
138
|
-
...(item.executable === undefined ? {} : { executable: item.executable }),
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Resolve opaque source IDs only through an explicitly declared case source map. */
|
|
144
|
-
export async function prepareArtifacts(
|
|
145
|
-
declarations: PreparationArtifact[],
|
|
146
|
-
fixturePaths: string[],
|
|
147
|
-
sources?: PreparationSources,
|
|
148
|
-
): Promise<InlineArtifact[]> {
|
|
149
|
-
if (!declarations.some((item) => item.sourceRef !== undefined))
|
|
150
|
-
return prepareInlineArtifacts(declarations, fixturePaths);
|
|
151
|
-
if (!sources || !isAbsolute(sources.root))
|
|
152
|
-
throw new Error("preparation source root is required and must be absolute");
|
|
153
|
-
const root = await realpath(sources.root).catch(() => {
|
|
154
|
-
throw new Error("preparation source root is unreadable");
|
|
155
|
-
});
|
|
156
|
-
const resolved: PreparationArtifact[] = [];
|
|
157
|
-
let sourceBytes = 0;
|
|
158
|
-
for (const item of declarations) {
|
|
159
|
-
if (item.sourceRef === undefined) {
|
|
160
|
-
resolved.push(item);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (item.contentBase64 !== undefined)
|
|
164
|
-
throw new Error("preparation artifact has two content sources");
|
|
165
|
-
if (!Object.hasOwn(sources.refs, item.sourceRef))
|
|
166
|
-
throw new Error("preparation source reference is not declared");
|
|
167
|
-
const url = sources.refs[item.sourceRef];
|
|
168
|
-
if (typeof url !== "string" || !url.startsWith("file:///"))
|
|
169
|
-
throw new Error("preparation source reference must be a file URL");
|
|
170
|
-
let path: string;
|
|
171
|
-
try {
|
|
172
|
-
path = await realpath(fileURLToPath(url));
|
|
173
|
-
} catch {
|
|
174
|
-
throw new Error("preparation source is unreadable");
|
|
175
|
-
}
|
|
176
|
-
const child = relative(root, path);
|
|
177
|
-
if (
|
|
178
|
-
!child ||
|
|
179
|
-
child === ".." ||
|
|
180
|
-
child.startsWith(`..${sep}`) ||
|
|
181
|
-
isAbsolute(child)
|
|
182
|
-
)
|
|
183
|
-
throw new Error("preparation source escapes its declared root");
|
|
184
|
-
let size: number;
|
|
185
|
-
try {
|
|
186
|
-
size = (await stat(path)).size;
|
|
187
|
-
} catch {
|
|
188
|
-
throw new Error("preparation source is unreadable");
|
|
189
|
-
}
|
|
190
|
-
if (size > MAX_ARTIFACT_BYTES || sourceBytes + size > MAX_TOTAL_BYTES)
|
|
191
|
-
throw new Error("preparation artifacts exceed the size limit");
|
|
192
|
-
let bytes: Buffer;
|
|
193
|
-
try {
|
|
194
|
-
bytes = await readFile(path);
|
|
195
|
-
} catch {
|
|
196
|
-
throw new Error("preparation source is unreadable");
|
|
197
|
-
}
|
|
198
|
-
sourceBytes += bytes.byteLength;
|
|
199
|
-
if (bytes.byteLength > MAX_ARTIFACT_BYTES || sourceBytes > MAX_TOTAL_BYTES)
|
|
200
|
-
throw new Error("preparation artifacts exceed the size limit");
|
|
201
|
-
resolved.push({
|
|
202
|
-
id: item.id,
|
|
203
|
-
relativePath: item.relativePath,
|
|
204
|
-
sha256: item.sha256,
|
|
205
|
-
contentBase64: bytes.toString("base64"),
|
|
206
|
-
...(item.gitExclude === undefined ? {} : { gitExclude: item.gitExclude }),
|
|
207
|
-
...(item.executable === undefined ? {} : { executable: item.executable }),
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
return prepareInlineArtifacts(resolved, fixturePaths);
|
|
211
|
-
}
|