@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,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
awaitHostProcess,
|
|
3
|
+
MAX_EVENT_BYTES,
|
|
4
|
+
HostProcessInterruption,
|
|
5
|
+
} from "./process-lifecycle";
|
|
6
|
+
import {
|
|
7
|
+
captureBoundedBytes,
|
|
8
|
+
captureBoundedText,
|
|
9
|
+
terminateOwnedProcessGroup,
|
|
10
|
+
} from "../process-lifecycle";
|
|
11
|
+
import { HostExecutionFailure, type HostResult } from "../evaluation/types";
|
|
12
|
+
import {
|
|
13
|
+
HostProcessDiagnostics,
|
|
14
|
+
type HostDiagnostics,
|
|
15
|
+
} from "../host-diagnostics";
|
|
16
|
+
|
|
17
|
+
type Phase = NonNullable<HostResult["processFailure"]>["phase"];
|
|
18
|
+
type Result = {
|
|
19
|
+
code: number;
|
|
20
|
+
out: string;
|
|
21
|
+
stderr: Uint8Array;
|
|
22
|
+
stderrTruncated: boolean;
|
|
23
|
+
signal: string | null;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function codexFailureMessage(result: HostResult): string | undefined {
|
|
28
|
+
const failure = result.processFailure;
|
|
29
|
+
if (!failure) return undefined;
|
|
30
|
+
const labels = {
|
|
31
|
+
"isolation-preflight": "Codex isolation preflight",
|
|
32
|
+
"executable-preflight": "Codex executable preflight",
|
|
33
|
+
execution: "Codex run",
|
|
34
|
+
};
|
|
35
|
+
if (!Object.hasOwn(labels, failure.phase)) return undefined;
|
|
36
|
+
return `${labels[failure.phase]} ${failureSuffix(failure)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validTimeout(timeoutMs: number): boolean {
|
|
40
|
+
return (
|
|
41
|
+
Number.isSafeInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= 30 * 60_000
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function failureSuffix(
|
|
46
|
+
failure: NonNullable<HostResult["processFailure"]>,
|
|
47
|
+
): string {
|
|
48
|
+
if (failure.reason === "timeout" && validTimeout(failure.timeoutMs))
|
|
49
|
+
return `timed out after ${failure.timeoutMs / 1000} s`;
|
|
50
|
+
return failure.reason === "cancelled" ? "cancelled" : "failed";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class CodexProcessFailure extends HostExecutionFailure {
|
|
54
|
+
constructor(
|
|
55
|
+
phase: Phase,
|
|
56
|
+
process: Result,
|
|
57
|
+
reason: NonNullable<HostResult["processFailure"]>["reason"] = "exit",
|
|
58
|
+
timeoutMs = 60_000,
|
|
59
|
+
) {
|
|
60
|
+
const failure = { phase, reason, timeoutMs };
|
|
61
|
+
const result: HostResult = {
|
|
62
|
+
finalMessage: null,
|
|
63
|
+
complete: false,
|
|
64
|
+
executionFailed: true,
|
|
65
|
+
actualCondition: "passive",
|
|
66
|
+
processFailure: failure,
|
|
67
|
+
artifacts: [
|
|
68
|
+
{
|
|
69
|
+
id: "codex.process",
|
|
70
|
+
bytes: Buffer.from(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
...failure,
|
|
73
|
+
code: process.code,
|
|
74
|
+
signal: process.signal,
|
|
75
|
+
durationMs: process.durationMs,
|
|
76
|
+
stderrTruncated: process.stderrTruncated,
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
{ id: "codex.stderr", bytes: process.stderr },
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
super(codexFailureMessage(result), result);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CodexProcessOptions {
|
|
88
|
+
diagnostics?: HostDiagnostics;
|
|
89
|
+
argv: string[];
|
|
90
|
+
cwd: string;
|
|
91
|
+
env: Record<string, string>;
|
|
92
|
+
input?: string;
|
|
93
|
+
timeoutMs: number;
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
phase?: Phase;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function writeCodexInput(
|
|
99
|
+
proc: Bun.Subprocess,
|
|
100
|
+
input?: string,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
if (input === undefined) return;
|
|
103
|
+
if (!proc.stdin || typeof proc.stdin === "number")
|
|
104
|
+
throw new Error("Codex input pipe unavailable");
|
|
105
|
+
await proc.stdin.write(input);
|
|
106
|
+
await proc.stdin.end();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function spawnCodexProcess(
|
|
110
|
+
options: CodexProcessOptions,
|
|
111
|
+
capture: HostProcessDiagnostics,
|
|
112
|
+
) {
|
|
113
|
+
try {
|
|
114
|
+
return Bun.spawn(options.argv, {
|
|
115
|
+
cwd: options.cwd,
|
|
116
|
+
env: options.env,
|
|
117
|
+
detached: process.platform !== "win32",
|
|
118
|
+
stdin: options.input === undefined ? "ignore" : "pipe",
|
|
119
|
+
stdout: "pipe",
|
|
120
|
+
stderr: "pipe",
|
|
121
|
+
});
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
capture.recordUnstarted();
|
|
124
|
+
throw cause;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function processPhase(options: CodexProcessOptions): Phase {
|
|
129
|
+
return options.phase ?? "execution";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function requireCodexProcess(options: CodexProcessOptions): void {
|
|
133
|
+
if (options.signal?.aborted) throw new Error("Codex run cancelled");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function codexCompletion(
|
|
137
|
+
proc: ReturnType<typeof spawnCodexProcess>,
|
|
138
|
+
options: CodexProcessOptions,
|
|
139
|
+
capture: HostProcessDiagnostics,
|
|
140
|
+
) {
|
|
141
|
+
const started = performance.now();
|
|
142
|
+
const stdout = captureBoundedText(proc.stdout, {
|
|
143
|
+
limit: MAX_EVENT_BYTES,
|
|
144
|
+
overflow: "reject",
|
|
145
|
+
exceeded: () => new Error("Codex event stream exceeds the size limit"),
|
|
146
|
+
observe: (bytes) => {
|
|
147
|
+
capture.stdout.add(bytes);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
const stderr = captureBoundedBytes(proc.stderr, {
|
|
151
|
+
limit: 64 * 1024,
|
|
152
|
+
overflow: "truncate",
|
|
153
|
+
cancelOnError: true,
|
|
154
|
+
observe: (bytes) => {
|
|
155
|
+
capture.stderr.add(bytes);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const input = writeCodexInput(proc, options.input);
|
|
159
|
+
const completed = Promise.all([stdout, proc.exited, stderr, input]).then(
|
|
160
|
+
([out, code, err]) => ({
|
|
161
|
+
out,
|
|
162
|
+
code,
|
|
163
|
+
stderr: err.bytes,
|
|
164
|
+
stderrTruncated: err.truncated,
|
|
165
|
+
signal: proc.signalCode ?? null,
|
|
166
|
+
durationMs: performance.now() - started,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
return { completed, drains: [stdout, stderr, input] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function runCodexProcess(
|
|
173
|
+
options: CodexProcessOptions,
|
|
174
|
+
): Promise<Result> {
|
|
175
|
+
requireCodexProcess(options);
|
|
176
|
+
const capture = new HostProcessDiagnostics(options.diagnostics);
|
|
177
|
+
const proc = spawnCodexProcess(options, capture);
|
|
178
|
+
const { completed, drains } = codexCompletion(proc, options, capture);
|
|
179
|
+
try {
|
|
180
|
+
const result = await awaitHostProcess(proc, completed, {
|
|
181
|
+
...options,
|
|
182
|
+
host: "Codex",
|
|
183
|
+
stop: terminateOwnedProcessGroup,
|
|
184
|
+
});
|
|
185
|
+
await capture.record(proc);
|
|
186
|
+
return result;
|
|
187
|
+
} catch (cause) {
|
|
188
|
+
if (!(cause instanceof HostProcessInterruption)) {
|
|
189
|
+
await Promise.allSettled(drains);
|
|
190
|
+
await capture.record(proc, "error");
|
|
191
|
+
throw cause;
|
|
192
|
+
}
|
|
193
|
+
const result = await completed;
|
|
194
|
+
await capture.record(proc, cause.reason);
|
|
195
|
+
throw new CodexProcessFailure(
|
|
196
|
+
processPhase(options),
|
|
197
|
+
result,
|
|
198
|
+
cause.reason,
|
|
199
|
+
options.timeoutMs,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
import { isAbsolute } from "node:path";
|
|
1
|
+
import { isAbsolute, join, sep } from "node:path";
|
|
2
|
+
import { insideRuntimeRoot, isRuntimeHomeRoot } from "../runtime/paths";
|
|
3
|
+
import { workspaceBoundaryException } from "../fixtures/workspace-state";
|
|
4
|
+
import {
|
|
5
|
+
nativeStatePath,
|
|
6
|
+
nativeStateProtectedRoots,
|
|
7
|
+
} from "../native-transcript-state";
|
|
8
|
+
|
|
9
|
+
const LINUX_MINIMAL_READ_ROOTS = [
|
|
10
|
+
"/bin",
|
|
11
|
+
"/sbin",
|
|
12
|
+
"/usr",
|
|
13
|
+
"/etc",
|
|
14
|
+
"/lib",
|
|
15
|
+
"/lib64",
|
|
16
|
+
"/nix/store",
|
|
17
|
+
"/run/current-system/sw",
|
|
18
|
+
];
|
|
2
19
|
|
|
3
20
|
function toml(value: string): string {
|
|
4
|
-
if (
|
|
21
|
+
if (
|
|
22
|
+
Array.from(value).some(
|
|
23
|
+
(character) =>
|
|
24
|
+
character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
|
|
25
|
+
)
|
|
26
|
+
)
|
|
5
27
|
throw new Error("Codex profile value contains a control character");
|
|
6
28
|
return JSON.stringify(value);
|
|
7
29
|
}
|
|
@@ -15,38 +37,36 @@ export function codexPermissionProfile(options: {
|
|
|
15
37
|
executableReadRoots: string[];
|
|
16
38
|
pluginReadRoot?: string;
|
|
17
39
|
protectedRoots: string[];
|
|
40
|
+
commandEnvironment?: Record<string, string>;
|
|
41
|
+
runtimeReadRoots?: string[];
|
|
42
|
+
nativeReadRoots?: string[];
|
|
43
|
+
nativeWriteRoots?: string[];
|
|
44
|
+
runtimeWriteRoot?: string;
|
|
45
|
+
localHttp?: boolean;
|
|
18
46
|
}): string {
|
|
19
47
|
if (!/^[a-z][a-z0-9_]*$/.test(options.id))
|
|
20
48
|
throw new Error("invalid Codex permission profile ID");
|
|
21
|
-
if (
|
|
22
|
-
!isAbsolute(options.workspace) ||
|
|
23
|
-
!isAbsolute(options.commandHome) ||
|
|
24
|
-
!isAbsolute(options.commandTemp) ||
|
|
25
|
-
!options.executableReadRoots.length ||
|
|
26
|
-
options.executableReadRoots.some(
|
|
27
|
-
(path) => !isAbsolute(path) || path === "/",
|
|
28
|
-
) ||
|
|
29
|
-
(options.pluginReadRoot !== undefined &&
|
|
30
|
-
(!isAbsolute(options.pluginReadRoot) ||
|
|
31
|
-
options.pluginReadRoot === "/")) ||
|
|
32
|
-
!options.protectedRoots.length ||
|
|
33
|
-
options.protectedRoots.some((root) => !isAbsolute(root))
|
|
34
|
-
)
|
|
49
|
+
if (!validProfilePaths(options))
|
|
35
50
|
throw new Error("Codex profile paths must be absolute and protected");
|
|
36
51
|
const name = options.id;
|
|
37
52
|
return [
|
|
38
53
|
`default_permissions = ${toml(name)}`,
|
|
39
54
|
'approval_policy = "never"',
|
|
40
55
|
"allow_login_shell = false",
|
|
56
|
+
...codexFeatureLines(options.localHttp),
|
|
41
57
|
"",
|
|
42
58
|
"[shell_environment_policy]",
|
|
43
59
|
'inherit = "none"',
|
|
44
60
|
"ignore_default_excludes = false",
|
|
45
61
|
"",
|
|
46
62
|
"[shell_environment_policy.set]",
|
|
47
|
-
|
|
63
|
+
...commandEnvironmentLines(
|
|
64
|
+
options.commandEnvironment,
|
|
65
|
+
options.executableReadRoots,
|
|
66
|
+
),
|
|
48
67
|
`HOME = ${toml(options.commandHome)}`,
|
|
49
68
|
`TMPDIR = ${toml(options.commandTemp)}`,
|
|
69
|
+
`TMPPREFIX = ${toml(join(options.commandTemp, sep))}`,
|
|
50
70
|
"",
|
|
51
71
|
`[permissions.${name}]`,
|
|
52
72
|
'extends = ":workspace"',
|
|
@@ -54,25 +74,234 @@ export function codexPermissionProfile(options: {
|
|
|
54
74
|
`[permissions.${name}.filesystem]`,
|
|
55
75
|
'":root" = "deny"',
|
|
56
76
|
'":minimal" = "read"',
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
...(process.platform === "linux"
|
|
78
|
+
? []
|
|
79
|
+
: ['":tmpdir" = "deny"', '":slash_tmp" = "deny"']),
|
|
80
|
+
...minimalProtectedRoots(profileProtectedRoots(options))
|
|
60
81
|
.sort()
|
|
61
82
|
.map((root) => `${toml(root)} = "deny"`),
|
|
62
|
-
...
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
? [`${toml(options.pluginReadRoot)} = "read"`]
|
|
67
|
-
: []),
|
|
83
|
+
...readRootLines(options),
|
|
84
|
+
...(options.nativeWriteRoots ?? []).map(
|
|
85
|
+
(root) => `${toml(root)} = "write"`,
|
|
86
|
+
),
|
|
68
87
|
`${toml(options.commandHome)} = "write"`,
|
|
69
88
|
`${toml(options.commandTemp)} = "write"`,
|
|
89
|
+
...optionalRootLines(options.runtimeWriteRoot, "write"),
|
|
90
|
+
// Override Codex's metadata carveout only for this isolated fixture.
|
|
91
|
+
`${toml(join(options.workspace, ".git"))} = "write"`,
|
|
70
92
|
"",
|
|
71
|
-
`[permissions.${name}.filesystem
|
|
93
|
+
`[permissions.${name}.filesystem.":workspace_roots"]`,
|
|
72
94
|
'"." = "write"',
|
|
73
95
|
"",
|
|
74
96
|
`[permissions.${name}.network]`,
|
|
75
|
-
|
|
97
|
+
...networkProfile(options.localHttp),
|
|
76
98
|
"",
|
|
77
99
|
].join("\n");
|
|
78
100
|
}
|
|
101
|
+
|
|
102
|
+
function codexFeatureLines(localHttp: boolean | undefined): string[] {
|
|
103
|
+
return [
|
|
104
|
+
"[features]",
|
|
105
|
+
"hooks = false",
|
|
106
|
+
...(localHttp ? ["network_proxy = true"] : []),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function networkProfile(localHttp: boolean | undefined): string[] {
|
|
111
|
+
if (!localHttp) return ["enabled = false"];
|
|
112
|
+
return [
|
|
113
|
+
"enabled = true",
|
|
114
|
+
"allow_local_binding = true",
|
|
115
|
+
"allow_upstream_proxy = false",
|
|
116
|
+
'domains = { "localhost" = "allow", "127.0.0.1" = "allow", "::1" = "allow" }',
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function profileProtectedRoots(
|
|
121
|
+
options: Parameters<typeof codexPermissionProfile>[0],
|
|
122
|
+
): string[] {
|
|
123
|
+
const readRoots = profileReadRoots(options);
|
|
124
|
+
const trustedRoots = [
|
|
125
|
+
...readRoots,
|
|
126
|
+
...(options.nativeWriteRoots ?? []),
|
|
127
|
+
...(options.runtimeWriteRoot ? [options.runtimeWriteRoot] : []),
|
|
128
|
+
];
|
|
129
|
+
const baselines = [
|
|
130
|
+
...LINUX_MINIMAL_READ_ROOTS,
|
|
131
|
+
...readRoots,
|
|
132
|
+
...(options.nativeWriteRoots ?? []),
|
|
133
|
+
options.workspace,
|
|
134
|
+
options.commandHome,
|
|
135
|
+
options.commandTemp,
|
|
136
|
+
...(options.runtimeWriteRoot ? [options.runtimeWriteRoot] : []),
|
|
137
|
+
];
|
|
138
|
+
return options.protectedRoots.filter(
|
|
139
|
+
(root) =>
|
|
140
|
+
(!implicitLinuxHomeBoundary(root, readRoots, baselines) ||
|
|
141
|
+
process.platform !== "linux") &&
|
|
142
|
+
!implicitNativeBoundary(root, trustedRoots, baselines) &&
|
|
143
|
+
!implicitWorkspaceBoundary(root, options.workspace, baselines),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function implicitWorkspaceBoundary(
|
|
148
|
+
root: string,
|
|
149
|
+
workspace: string,
|
|
150
|
+
baselines: string[],
|
|
151
|
+
): boolean {
|
|
152
|
+
return (
|
|
153
|
+
process.platform === "linux" &&
|
|
154
|
+
workspaceBoundaryException(root, workspace).length > 0 &&
|
|
155
|
+
!hasBroaderBaseline(root, root, baselines)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function implicitNativeBoundary(
|
|
160
|
+
root: string,
|
|
161
|
+
trustedRoots: string[],
|
|
162
|
+
baselines: string[],
|
|
163
|
+
): boolean {
|
|
164
|
+
const canonicalRoot = nativeStatePath();
|
|
165
|
+
if (!supportedNativeNamespace(canonicalRoot)) return false;
|
|
166
|
+
const boundary = nativeBoundaryRoot(root, canonicalRoot);
|
|
167
|
+
if (!boundary) return false;
|
|
168
|
+
const descendant = trustedRoots.some((read) =>
|
|
169
|
+
insideRuntimeRoot(boundary, read),
|
|
170
|
+
);
|
|
171
|
+
return descendant && !hasBroaderBaseline(root, boundary, baselines);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function nativeBoundaryRoot(
|
|
175
|
+
root: string,
|
|
176
|
+
canonicalRoot: string,
|
|
177
|
+
): string | null {
|
|
178
|
+
if (nativeStateProtectedRoots().includes(root)) return canonicalRoot;
|
|
179
|
+
return insideRuntimeRoot(canonicalRoot, root) ? root : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function hasBroaderBaseline(
|
|
183
|
+
root: string,
|
|
184
|
+
boundary: string,
|
|
185
|
+
baselines: string[],
|
|
186
|
+
): boolean {
|
|
187
|
+
return baselines.some(
|
|
188
|
+
(baseline) =>
|
|
189
|
+
insideRuntimeRoot(baseline, boundary) ||
|
|
190
|
+
insideRuntimeRoot(baseline, root),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function supportedNativeNamespace(root: string): boolean {
|
|
195
|
+
if (process.platform === "linux")
|
|
196
|
+
return [
|
|
197
|
+
"/var/tmp/sevro-native-private",
|
|
198
|
+
"/private/var/tmp/sevro-native-private",
|
|
199
|
+
].includes(root);
|
|
200
|
+
return (
|
|
201
|
+
process.platform === "darwin" &&
|
|
202
|
+
/^\/private\/var\/folders\/[^/]+\/[^/]+\/T\/sevro-native-private$/.test(
|
|
203
|
+
root,
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Codex masks deny ancestors after read mounts; the empty-root policy already hides safe HOME ancestors. */
|
|
209
|
+
function implicitLinuxHomeBoundary(
|
|
210
|
+
root: string,
|
|
211
|
+
readRoots: string[],
|
|
212
|
+
baselines: string[],
|
|
213
|
+
): boolean {
|
|
214
|
+
if (!isRuntimeHomeRoot(root)) return false;
|
|
215
|
+
const descendant = readRoots.some((read) => insideRuntimeRoot(root, read));
|
|
216
|
+
return (
|
|
217
|
+
descendant &&
|
|
218
|
+
!baselines.some((baseline) => insideRuntimeRoot(baseline, root))
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function minimalProtectedRoots(roots: string[]): string[] {
|
|
223
|
+
return [...new Set(roots)].filter(
|
|
224
|
+
(root) =>
|
|
225
|
+
!roots.some(
|
|
226
|
+
(other) => other !== root && root.startsWith(`${other}${sep}`),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function commandEnvironmentLines(
|
|
232
|
+
environment: Record<string, string> = {},
|
|
233
|
+
executableReadRoots: string[],
|
|
234
|
+
): string[] {
|
|
235
|
+
const systemPath =
|
|
236
|
+
environment.PATH ??
|
|
237
|
+
"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
238
|
+
const path =
|
|
239
|
+
process.platform === "linux"
|
|
240
|
+
? [...executableReadRoots, systemPath].join(":")
|
|
241
|
+
: systemPath;
|
|
242
|
+
return [
|
|
243
|
+
`PATH = ${toml(path)}`,
|
|
244
|
+
...Object.entries(environment)
|
|
245
|
+
.filter(([name]) => !["PATH", "HOME", "TMPDIR"].includes(name))
|
|
246
|
+
.map(([name, value]) => `${toml(name)} = ${toml(value)}`),
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function optionalRootLines(
|
|
251
|
+
root: string | undefined,
|
|
252
|
+
permission: string,
|
|
253
|
+
): string[] {
|
|
254
|
+
return root ? [`${toml(root)} = ${toml(permission)}`] : [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function readRootLines(
|
|
258
|
+
options: Parameters<typeof codexPermissionProfile>[0],
|
|
259
|
+
): string[] {
|
|
260
|
+
return profileReadRoots(options)
|
|
261
|
+
.sort()
|
|
262
|
+
.map((path) => `${toml(path)} = "read"`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function profileReadRoots(
|
|
266
|
+
options: Parameters<typeof codexPermissionProfile>[0],
|
|
267
|
+
): string[] {
|
|
268
|
+
const roots = [
|
|
269
|
+
...options.executableReadRoots,
|
|
270
|
+
...(options.runtimeReadRoots ?? []),
|
|
271
|
+
...(options.nativeReadRoots ?? []),
|
|
272
|
+
];
|
|
273
|
+
if (options.pluginReadRoot) roots.push(options.pluginReadRoot);
|
|
274
|
+
return [...new Set(roots)];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readRoot(path: string): boolean {
|
|
278
|
+
return isAbsolute(path) && path !== "/";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function optionalReadRoot(path: string | undefined): boolean {
|
|
282
|
+
return path === undefined || readRoot(path);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function validProfilePaths(
|
|
286
|
+
options: Parameters<typeof codexPermissionProfile>[0],
|
|
287
|
+
): boolean {
|
|
288
|
+
const privatePaths = [
|
|
289
|
+
options.workspace,
|
|
290
|
+
options.commandHome,
|
|
291
|
+
options.commandTemp,
|
|
292
|
+
];
|
|
293
|
+
return (
|
|
294
|
+
privatePaths.every(isAbsolute) &&
|
|
295
|
+
validExecutableRoots(options.executableReadRoots) &&
|
|
296
|
+
optionalReadRoot(options.pluginReadRoot) &&
|
|
297
|
+
validProtectedRoots(options.protectedRoots)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function validExecutableRoots(paths: string[]): boolean {
|
|
302
|
+
return paths.length > 0 && paths.every(readRoot);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function validProtectedRoots(paths: string[]): boolean {
|
|
306
|
+
return paths.length > 0 && paths.every(isAbsolute);
|
|
307
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { lstat, readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isMissingFile } from "../fixtures/tools";
|
|
5
|
+
|
|
6
|
+
const MAX_SESSION_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const MAX_SESSION_FILES = 1024;
|
|
8
|
+
const THREAD_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
9
|
+
type SearchStatus = "partial" | "unavailable" | "ambiguous";
|
|
10
|
+
type SessionLocation =
|
|
11
|
+
{ status: SearchStatus } | { status: "found"; path: string };
|
|
12
|
+
|
|
13
|
+
async function sessionsRootStatus(
|
|
14
|
+
root: string,
|
|
15
|
+
): Promise<"valid" | "partial" | "unavailable"> {
|
|
16
|
+
try {
|
|
17
|
+
const entry = await lstat(root);
|
|
18
|
+
return entry.isDirectory() && !entry.isSymbolicLink() ? "valid" : "partial";
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return isMissingFile(error) ? "unavailable" : "partial";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class SessionSearch {
|
|
25
|
+
private pending: string[];
|
|
26
|
+
private matches: string[] = [];
|
|
27
|
+
private visited = 0;
|
|
28
|
+
constructor(
|
|
29
|
+
root: string,
|
|
30
|
+
private suffix: string,
|
|
31
|
+
) {
|
|
32
|
+
this.pending = [root];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async run(): Promise<SessionLocation> {
|
|
36
|
+
while (this.pending.length) {
|
|
37
|
+
const directory = this.pending.pop();
|
|
38
|
+
if (directory === undefined) break;
|
|
39
|
+
try {
|
|
40
|
+
const status = await this.visitDirectory(directory);
|
|
41
|
+
if (status) return { status };
|
|
42
|
+
} catch {
|
|
43
|
+
return { status: "partial" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const [path] = this.matches;
|
|
47
|
+
return this.location(path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private location(path: string | undefined): SessionLocation {
|
|
51
|
+
return this.matches.length === 1 && path !== undefined
|
|
52
|
+
? { status: "found", path }
|
|
53
|
+
: { status: "unavailable" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async visitDirectory(
|
|
57
|
+
directory: string,
|
|
58
|
+
): Promise<SearchStatus | undefined> {
|
|
59
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const status = this.visitEntry(directory, entry);
|
|
62
|
+
if (status) return status;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private visitEntry(
|
|
68
|
+
directory: string,
|
|
69
|
+
entry: Dirent,
|
|
70
|
+
): SearchStatus | undefined {
|
|
71
|
+
this.visited++;
|
|
72
|
+
if (this.visited > MAX_SESSION_FILES || entry.isSymbolicLink())
|
|
73
|
+
return "partial";
|
|
74
|
+
return this.collectCandidate(directory, entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private collectCandidate(
|
|
78
|
+
directory: string,
|
|
79
|
+
entry: Dirent,
|
|
80
|
+
): SearchStatus | undefined {
|
|
81
|
+
const path = join(directory, entry.name);
|
|
82
|
+
if (entry.isDirectory()) this.pending.push(path);
|
|
83
|
+
else if (entry.isFile() && entry.name.endsWith(this.suffix))
|
|
84
|
+
this.matches.push(path);
|
|
85
|
+
return this.matches.length > 1 ? "ambiguous" : undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Locate exactly one original thread while refusing symlinks and bounded tree overflow. */
|
|
90
|
+
export async function locateNativeSession(
|
|
91
|
+
home: string,
|
|
92
|
+
threadId: string,
|
|
93
|
+
): Promise<SessionLocation> {
|
|
94
|
+
if (!THREAD_ID.test(threadId)) return { status: "partial" };
|
|
95
|
+
const root = join(home, "sessions");
|
|
96
|
+
const status = await sessionsRootStatus(root);
|
|
97
|
+
if (status !== "valid") return { status };
|
|
98
|
+
return new SessionSearch(root, `-${threadId}.jsonl`).run();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function readNativeSession(path: string): Promise<string | null> {
|
|
102
|
+
if ((await stat(path)).size > MAX_SESSION_BYTES) return null;
|
|
103
|
+
const bytes = await readFile(path);
|
|
104
|
+
if (bytes.byteLength > MAX_SESSION_BYTES) return null;
|
|
105
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
106
|
+
}
|