headlesscode 1.2.2 → 1.3.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.
@@ -0,0 +1,639 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { randomUUID } from "node:crypto"
3
+ import * as fs from "node:fs"
4
+ import * as os from "node:os"
5
+ import * as path from "node:path"
6
+ import { setTimeout as delay } from "node:timers/promises"
7
+ import { fileURLToPath } from "node:url"
8
+ import { DEFAULT_PROTECTED_FILES, findMatchingPattern } from "../permissions/protected-files.js"
9
+ import { OpenShellSessionProvider, OPENSHELL_HARNESS_ROOT } from "../cloud/openshell-provider.js"
10
+ import type { OpenShellSessionProviderOptions } from "../cloud/openshell-provider.js"
11
+ import type { CloudSessionRequest, CommandResult, SessionHandle } from "../cloud/provider.js"
12
+ import type { CandidateRecord, CommandRunner, RsiConfig, TrialResult } from "./types.js"
13
+ import { ADVERSARIAL_RUNNER_PATH, ADVERSARIAL_TESTS_PATH } from "./adversarial.js"
14
+
15
+ const POLICY_PATH = fileURLToPath(new URL("../../shared/openshell/headlesscode-policy.yaml", import.meta.url))
16
+ const MAX_SNAPSHOT_BYTES = 512 * 1024 * 1024
17
+ const MAX_BUNDLE_BYTES = 2 * 1024 * 1024 * 1024
18
+ const IMAGE_DEPENDENCIES_LINK = "node_modules"
19
+ const RSI_OPENSHELL_IMAGE = "headlesscode-openshell-rsi:local"
20
+ const RSI_TRAINING_OPENSHELL_IMAGE = "headlesscode-openshell-rsi-training:local"
21
+ export const RSI_CPU_MODEL_EVALUATION_MAX_CHECKPOINT_BYTES = 2 * 1024 * 1024 * 1024
22
+ const OPENSHELL_INITIAL_CONFIG_SETTLE_MS = 15_000
23
+
24
+ export interface RsiOpenShellProvider {
25
+ spawnExistingWorktreeSession(request: CloudSessionRequest, worktreePath: string): Promise<SessionHandle>
26
+ waitReady(handle: SessionHandle): Promise<void>
27
+ runHarness(handle: SessionHandle, command: string): Promise<CommandResult>
28
+ teardown(handle: SessionHandle): Promise<void>
29
+ }
30
+
31
+ export type RsiOpenShellProviderFactory = (options: OpenShellSessionProviderOptions) => RsiOpenShellProvider
32
+
33
+ export interface IsolatedRunOptions {
34
+ config: RsiConfig
35
+ sourceRoot: string
36
+ commit: string
37
+ command: string
38
+ timeoutMs: number
39
+ mode: "mutation" | "evaluation" | "training" | "model-evaluation"
40
+ sessionName?: string
41
+ env?: Record<string, string>
42
+ providerFactory?: RsiOpenShellProviderFactory
43
+ }
44
+
45
+ function execFileBuffer(command: string, args: string[], options: { cwd?: string; input?: Buffer | string } = {}): Buffer {
46
+ return execFileSync(command, args, {
47
+ cwd: options.cwd,
48
+ input: options.input,
49
+ encoding: "buffer",
50
+ maxBuffer: MAX_BUNDLE_BYTES,
51
+ stdio: ["pipe", "pipe", "pipe"],
52
+ }) as Buffer
53
+ }
54
+
55
+ function pathIsSafe(relativePath: string): boolean {
56
+ return relativePath.length > 0 && !relativePath.includes("\0") && !path.posix.isAbsolute(relativePath) && !relativePath.split("/").some((part) => part === ".." || part === "")
57
+ }
58
+
59
+ function alwaysOmit(relativePath: string): boolean {
60
+ const normalized = relativePath.replaceAll("\\", "/")
61
+ return normalized.startsWith("__headlesscode_rsi_adversarial__/") || findMatchingPattern(normalized, [...DEFAULT_PROTECTED_FILES, ".headlesscode/", ".worktrees/", "node_modules/", "src/rsi/", "scripts/eval-suite/"]) !== null
62
+ }
63
+
64
+ function shouldInclude(relativePath: string, mode: IsolatedRunOptions["mode"], config: RsiConfig): boolean {
65
+ if (alwaysOmit(relativePath)) return false
66
+ if (mode === "mutation") {
67
+ // These are supervisor-controlled RSI policy/evaluation inputs. Enforce
68
+ // their exclusion even when a caller supplies an incomplete protectedPaths list.
69
+ const segments = relativePath.split("/")
70
+ if (segments.includes("__tests__") || /(?:^|\/)(?:test|spec)\.[^/]+$/.test(relativePath) || /\.(?:test|spec)\.[^/]+$/.test(relativePath)) return false
71
+ if (findMatchingPattern(relativePath, ["src/rsi/", "tests/", "test/", "scripts/", "fixtures/rsi-curriculum/"]) !== null) return false
72
+ if (findMatchingPattern(relativePath, config.protectedPaths) !== null) return false
73
+ }
74
+ return true
75
+ }
76
+
77
+ interface TreeFile {
78
+ path: string
79
+ mode: "100644" | "100755"
80
+ objectId: string
81
+ }
82
+
83
+ function treeFiles(sourceRoot: string, commit: string, mode: IsolatedRunOptions["mode"], config: RsiConfig): TreeFile[] {
84
+ const raw = execFileBuffer("git", ["ls-tree", "-r", "-z", "--full-tree", commit], { cwd: sourceRoot })
85
+ const files: TreeFile[] = []
86
+ for (const entry of raw.toString("utf8").split("\0")) {
87
+ if (!entry) continue
88
+ const tab = entry.indexOf("\t")
89
+ if (tab < 0) throw new Error("OpenShell snapshot rejected malformed Git tree entry")
90
+ const [fileMode, objectType, objectId] = entry.slice(0, tab).split(" ")
91
+ const relativePath = entry.slice(tab + 1)
92
+ if (!pathIsSafe(relativePath)) throw new Error(`OpenShell snapshot rejected unsafe repository path: ${relativePath}`)
93
+ if (fileMode === "160000") throw new Error(`OpenShell snapshot does not permit submodules: ${relativePath}`)
94
+ if (fileMode === "120000") throw new Error(`OpenShell snapshot does not permit symlink source files: ${relativePath}`)
95
+ if (objectType !== "blob" || (fileMode !== "100644" && fileMode !== "100755")) continue
96
+ if (shouldInclude(relativePath, mode, config)) files.push({ path: relativePath, mode: fileMode, objectId })
97
+ }
98
+ return files
99
+ }
100
+
101
+ function readBlobBatch(sourceRoot: string, files: TreeFile[]): Map<string, Buffer> {
102
+ const input = Buffer.from(files.map((file) => file.objectId).join("\n") + "\n")
103
+ const output = execFileBuffer("git", ["cat-file", "--batch"], { cwd: sourceRoot, input })
104
+ const blobs = new Map<string, Buffer>()
105
+ let offset = 0
106
+ let total = 0
107
+ for (const file of files) {
108
+ const headerEnd = output.indexOf(0x0a, offset)
109
+ if (headerEnd < 0) throw new Error("OpenShell snapshot rejected truncated Git blob response")
110
+ const header = output.subarray(offset, headerEnd).toString("ascii").split(" ")
111
+ const size = Number(header[2])
112
+ if (header[1] !== "blob" || !Number.isSafeInteger(size) || size < 0) throw new Error(`OpenShell snapshot rejected invalid blob metadata for ${file.path}`)
113
+ total += size
114
+ if (total > MAX_SNAPSHOT_BYTES || offset + (headerEnd - offset + 1) + size >= output.length) {
115
+ if (total > MAX_SNAPSHOT_BYTES) throw new Error("OpenShell snapshot exceeds the 512 MiB experiment limit")
116
+ if (offset + (headerEnd - offset + 1) + size > output.length) throw new Error(`OpenShell snapshot rejected truncated blob ${file.path}`)
117
+ }
118
+ const start = headerEnd + 1
119
+ const end = start + size
120
+ if (output[end] !== 0x0a) throw new Error(`OpenShell snapshot rejected malformed blob terminator for ${file.path}`)
121
+ blobs.set(file.path, output.subarray(start, end))
122
+ offset = end + 1
123
+ }
124
+ return blobs
125
+ }
126
+
127
+ function initializeSnapshot(sourceRoot: string, commit: string, destination: string, mode: IsolatedRunOptions["mode"], config: RsiConfig, includeHarnessDependencies = true): string {
128
+ const files = treeFiles(sourceRoot, commit, mode, config)
129
+ const blobs = readBlobBatch(sourceRoot, files)
130
+ fs.mkdirSync(destination, { recursive: true, mode: 0o700 })
131
+ for (const file of files) {
132
+ const target = path.join(destination, file.path)
133
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 })
134
+ fs.writeFileSync(target, blobs.get(file.path)!, { mode: file.mode === "100755" ? 0o755 : 0o644, flag: "wx" })
135
+ }
136
+ if (mode === "evaluation" && includeHarnessDependencies) fs.symlinkSync(`${OPENSHELL_HARNESS_ROOT}/node_modules`, path.join(destination, IMAGE_DEPENDENCIES_LINK))
137
+ execFileSync("git", ["init", "--quiet", "--initial-branch=main"], { cwd: destination })
138
+ execFileSync("git", ["config", "user.name", "HeadlessCode RSI Snapshot"], { cwd: destination })
139
+ execFileSync("git", ["config", "user.email", "rsi-snapshot@invalid.example"], { cwd: destination })
140
+ execFileSync("git", ["add", "--all"], { cwd: destination })
141
+ if (mode === "evaluation" && includeHarnessDependencies) execFileSync("git", ["add", "--force", "--", IMAGE_DEPENDENCIES_LINK], { cwd: destination })
142
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "commit", "--quiet", "-m", `RSI ${mode} snapshot ${commit}`], { cwd: destination, env: snapshotCommitEnvironment() })
143
+ return execFileSync("git", ["rev-parse", "HEAD"], { cwd: destination, encoding: "utf8" }).trim()
144
+ }
145
+
146
+ function snapshotCommitEnvironment(): NodeJS.ProcessEnv {
147
+ const date = "2000-01-01T00:00:00Z"
148
+ return { ...process.env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date }
149
+ }
150
+
151
+ /** Make an evaluator snapshot containing only fixed, supervisor-generated model-job inputs. */
152
+ export function createModelJobSnapshotBundle(sourceRoot: string, commit: string, config: RsiConfig, inputs: { dataset: Buffer; manifest: Buffer; cells: Buffer; adapterArchive?: Buffer }): { content: Buffer; snapshotCommit: string } {
153
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "headlesscode-rsi-model-snapshot-"))
154
+ const snapshot = path.join(tempRoot, "repo")
155
+ const bundlePath = path.join(tempRoot, "snapshot.bundle")
156
+ try {
157
+ initializeSnapshot(sourceRoot, commit, snapshot, "model-evaluation", config, false)
158
+ const modelInput = path.join(snapshot, ".headlesscode-rsi-model")
159
+ fs.mkdirSync(modelInput, { recursive: true, mode: 0o700 })
160
+ for (const [name, content] of [["dataset.jsonl", inputs.dataset], ["manifest.json", inputs.manifest], ["evaluation-cells.jsonl", inputs.cells]] as const) {
161
+ fs.writeFileSync(path.join(modelInput, name), content, { mode: 0o600, flag: "wx" })
162
+ }
163
+ if (inputs.adapterArchive) fs.writeFileSync(path.join(modelInput, "adapter.tar"), inputs.adapterArchive, { mode: 0o600, flag: "wx" })
164
+ execFileSync("git", ["add", "--all"], { cwd: snapshot })
165
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "commit", "--amend", "--quiet", "--no-edit"], { cwd: snapshot, env: snapshotCommitEnvironment() })
166
+ const snapshotCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: snapshot, encoding: "utf8" }).trim()
167
+ if (execFileSync("git", ["rev-list", "--count", "--all"], { cwd: snapshot, encoding: "utf8" }).trim() !== "1") throw new Error("model job snapshot must contain exactly one commit")
168
+ execFileSync("git", ["bundle", "create", bundlePath, "--all"], { cwd: snapshot, stdio: "pipe" })
169
+ execFileSync("git", ["bundle", "verify", bundlePath], { cwd: snapshot, stdio: "pipe" })
170
+ const content = fs.readFileSync(bundlePath)
171
+ if (content.byteLength > MAX_SNAPSHOT_BYTES) throw new Error("model job snapshot exceeds the 512 MiB limit")
172
+ return { content, snapshotCommit }
173
+ } finally {
174
+ fs.rmSync(tempRoot, { recursive: true, force: true })
175
+ }
176
+ }
177
+
178
+ /** Build a one-commit mutation-only bundle for transfer to a remote worker. */
179
+ export function createMutationSnapshotBundle(sourceRoot: string, commit: string, config: RsiConfig): { content: Buffer; snapshotCommit: string } {
180
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "headlesscode-rsi-fleet-snapshot-"))
181
+ const snapshot = path.join(tempRoot, "repo")
182
+ const bundlePath = path.join(tempRoot, "snapshot.bundle")
183
+ try {
184
+ const snapshotCommit = initializeSnapshot(sourceRoot, commit, snapshot, "mutation", config)
185
+ const commitCount = execFileSync("git", ["rev-list", "--count", "--all"], { cwd: snapshot, encoding: "utf8" }).trim()
186
+ if (commitCount !== "1") throw new Error("fleet mutation snapshot must contain exactly one sanitized commit")
187
+ execFileSync("git", ["bundle", "create", bundlePath, "--all"], { cwd: snapshot, stdio: "pipe" })
188
+ execFileSync("git", ["bundle", "verify", bundlePath], { cwd: snapshot, stdio: "pipe" })
189
+ const content = fs.readFileSync(bundlePath)
190
+ if (content.byteLength > MAX_SNAPSHOT_BYTES) throw new Error("fleet mutation bundle exceeds the 512 MiB snapshot limit")
191
+ return { content, snapshotCommit }
192
+ } finally {
193
+ fs.rmSync(tempRoot, { recursive: true, force: true })
194
+ }
195
+ }
196
+
197
+ /** Build a one-commit evaluation snapshot without supervisor scoring inputs. */
198
+ export function createEvaluationSnapshotBundle(sourceRoot: string, commit: string, config: RsiConfig): { content: Buffer; snapshotCommit: string } {
199
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "headlesscode-rsi-fleet-eval-"))
200
+ const snapshot = path.join(tempRoot, "repo")
201
+ const bundlePath = path.join(tempRoot, "snapshot.bundle")
202
+ try {
203
+ const snapshotCommit = initializeSnapshot(sourceRoot, commit, snapshot, "evaluation", config, false)
204
+ if (execFileSync("git", ["rev-list", "--count", "--all"], { cwd: snapshot, encoding: "utf8" }).trim() !== "1") throw new Error("fleet evaluation snapshot must contain exactly one sanitized commit")
205
+ execFileSync("git", ["bundle", "create", bundlePath, "--all"], { cwd: snapshot, stdio: "pipe" })
206
+ execFileSync("git", ["bundle", "verify", bundlePath], { cwd: snapshot, stdio: "pipe" })
207
+ const content = fs.readFileSync(bundlePath)
208
+ if (content.byteLength > MAX_SNAPSHOT_BYTES) throw new Error("fleet evaluation bundle exceeds the 512 MiB snapshot limit")
209
+ return { content, snapshotCommit }
210
+ } finally {
211
+ fs.rmSync(tempRoot, { recursive: true, force: true })
212
+ }
213
+ }
214
+
215
+ const ADVERSARIAL_RUNNER_SOURCE = `import assert from "node:assert/strict"
216
+ import { readFile } from "node:fs/promises"
217
+ import path from "node:path"
218
+ import { pathToFileURL } from "node:url"
219
+
220
+ const root = process.cwd()
221
+ const spec = JSON.parse(await readFile(path.join(root, "${ADVERSARIAL_TESTS_PATH}"), "utf8"))
222
+ if (!Array.isArray(spec.tests) || spec.tests.length < 1 || spec.tests.length > 8) throw new Error("invalid adversarial test manifest")
223
+ for (const test of spec.tests) {
224
+ if (typeof test.modulePath !== "string" || !/^(?:src|lib|app)\\//.test(test.modulePath) || test.modulePath.includes("..") || test.modulePath.includes("\\\\")) throw new Error("unsafe test module path")
225
+ const target = path.resolve(root, test.modulePath)
226
+ if (!target.startsWith(root + path.sep)) throw new Error("test module escaped the candidate snapshot")
227
+ if (typeof test.exportName !== "string" || !/^[A-Za-z_$][A-Za-z0-9_$]{0,79}$/.test(test.exportName) || !Array.isArray(test.args) || test.args.length > 16) throw new Error("invalid adversarial test invocation")
228
+ const module = await import(pathToFileURL(target).href)
229
+ if (typeof module[test.exportName] !== "function") throw new Error("missing candidate export " + test.exportName)
230
+ const actual = await module[test.exportName](...test.args)
231
+ assert.deepStrictEqual(actual, test.expected, "adversarial test " + test.id + " failed: " + test.reason)
232
+ process.stdout.write("PASS " + test.id + "\\n")
233
+ }
234
+ `
235
+
236
+ /** Bundle a candidate commit with a bounded declarative test manifest and fixed runner. */
237
+ export function createAdversarialEvaluationSnapshotBundle(sourceRoot: string, commit: string, config: RsiConfig, tests: Buffer): { content: Buffer; snapshotCommit: string } {
238
+ if (tests.byteLength < 2 || tests.byteLength > 64 * 1024) throw new Error("adversarial test artifact must be between 2 bytes and 64 KiB")
239
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "headlesscode-rsi-adversarial-snapshot-"))
240
+ const snapshot = path.join(tempRoot, "repo")
241
+ const bundlePath = path.join(tempRoot, "snapshot.bundle")
242
+ try {
243
+ initializeSnapshot(sourceRoot, commit, snapshot, "evaluation", config, false)
244
+ const reserved = path.join(snapshot, "__headlesscode_rsi_adversarial__")
245
+ fs.mkdirSync(reserved, { recursive: true, mode: 0o700 })
246
+ fs.writeFileSync(path.join(snapshot, ADVERSARIAL_TESTS_PATH), tests, { mode: 0o600, flag: "wx" })
247
+ fs.writeFileSync(path.join(snapshot, ADVERSARIAL_RUNNER_PATH), ADVERSARIAL_RUNNER_SOURCE, { mode: 0o600, flag: "wx" })
248
+ execFileSync("git", ["add", "--all"], { cwd: snapshot })
249
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "commit", "--amend", "--quiet", "--no-edit"], { cwd: snapshot, env: snapshotCommitEnvironment() })
250
+ const snapshotCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: snapshot, encoding: "utf8" }).trim()
251
+ if (execFileSync("git", ["rev-list", "--count", "--all"], { cwd: snapshot, encoding: "utf8" }).trim() !== "1") throw new Error("adversarial evaluation snapshot must contain exactly one commit")
252
+ execFileSync("git", ["bundle", "create", bundlePath, "--all"], { cwd: snapshot, stdio: "pipe" })
253
+ execFileSync("git", ["bundle", "verify", bundlePath], { cwd: snapshot, stdio: "pipe" })
254
+ const content = fs.readFileSync(bundlePath)
255
+ if (content.byteLength > MAX_SNAPSHOT_BYTES) throw new Error("adversarial evaluation bundle exceeds the 512 MiB snapshot limit")
256
+ return { content, snapshotCommit }
257
+ } finally {
258
+ fs.rmSync(tempRoot, { recursive: true, force: true })
259
+ }
260
+ }
261
+
262
+ function shellQuote(value: string): string {
263
+ return `'${value.replaceAll("'", "'\\''")}'`
264
+ }
265
+
266
+ function workerEnvironment(config: RsiConfig, candidate: CandidateRecord): Record<string, string> {
267
+ const worker = config.roles?.worker
268
+ if (worker && worker.provider !== "ollama") throw new Error("RSI OpenShell currently permits only the local Ollama worker; other providers require an explicit reviewed credential policy")
269
+ const host = process.env.HEADLESSCODE_RSI_OLLAMA_HOST?.trim() || "host.openshell.internal"
270
+ if (host !== "host.openshell.internal") throw new Error("RSI OpenShell permits Ollama access only through host.openshell.internal:11434")
271
+ return {
272
+ HEADLESSCODE_CODE_MODE_BACKEND: "ollama",
273
+ HEADLESSCODE_LOCAL_BACKEND_MODES: "code",
274
+ HEADLESSCODE_CODE_MODE_MODEL: worker?.model ?? candidate.model,
275
+ HEADLESSCODE_OLLAMA_URL: `http://${host}:11434`,
276
+ HEADLESSCODE_OLLAMA_THINK: "0",
277
+ HEADLESSCODE_CAPTURE_TRANSCRIPT_DIR: path.join("/workspace", ".headlesscode", "rsi-transcripts"),
278
+ HEADLESSCODE_MAX_ITERATIONS: String(config.maxIterations),
279
+ HEADLESSCODE_MAX_DURATION_MS: String(config.commandTimeoutMs),
280
+ }
281
+ }
282
+
283
+ export function networkRules(mode: IsolatedRunOptions["mode"]): Record<string, unknown> {
284
+ if (mode !== "mutation") return {}
285
+ const host = process.env.HEADLESSCODE_RSI_OLLAMA_HOST?.trim() || "host.openshell.internal"
286
+ if (host !== "host.openshell.internal") throw new Error("RSI OpenShell permits Ollama access only through host.openshell.internal:11434")
287
+ return {
288
+ rsi_local_ollama: {
289
+ name: "rsi-local-ollama-only",
290
+ endpoints: [{ host, port: 11434, protocol: "tcp" }],
291
+ binaries: [{ path: "/usr/local/bin/node" }],
292
+ },
293
+ }
294
+ }
295
+
296
+ export function inspectLocalRsiCheckpoint(configuredBase: string, mode: IsolatedRunOptions["mode"]): { path: string; files: number; bytes: number } {
297
+ const stat = fs.lstatSync(configuredBase)
298
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("configured RSI base model must be a real directory, not a symlink")
299
+ const source = fs.realpathSync(configuredBase)
300
+ if (!fs.existsSync(path.join(source, "config.json"))) throw new Error("configured RSI base model is not a local Hugging Face checkpoint")
301
+ const pending = [source]
302
+ let files = 0
303
+ let bytes = 0
304
+ while (pending.length) {
305
+ const directory = pending.pop()!
306
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
307
+ const target = path.join(directory, entry.name)
308
+ const entryStat = fs.lstatSync(target)
309
+ if (entryStat.isSymbolicLink() || (!entryStat.isDirectory() && !entryStat.isFile())) throw new Error("RSI base checkpoint contains a symlink or special file")
310
+ if (entryStat.isDirectory()) pending.push(target)
311
+ else {
312
+ files += 1
313
+ bytes += entryStat.size
314
+ if (files > 100_000 || bytes > 64 * 1024 * 1024 * 1024) throw new Error("RSI base checkpoint exceeds the 100,000-file or 64 GiB mount bound")
315
+ }
316
+ }
317
+ }
318
+ if (mode === "model-evaluation" && bytes > RSI_CPU_MODEL_EVALUATION_MAX_CHECKPOINT_BYTES) throw new Error("CPU-only paired model evaluation requires a checkpoint no larger than 2 GiB; GPU-backed evaluation is not implemented")
319
+ return { path: source, files, bytes }
320
+ }
321
+
322
+ function providerOptions(config: RsiConfig, mode: IsolatedRunOptions["mode"], timeoutMs: number): OpenShellSessionProviderOptions {
323
+ const policy = process.env.HEADLESSCODE_OPENSHELL_POLICY ?? POLICY_PATH
324
+ const configuredImage = process.env.HEADLESSCODE_OPENSHELL_IMAGE?.trim()
325
+ const trainingMode = mode === "training" || mode === "model-evaluation"
326
+ const image = trainingMode ? RSI_TRAINING_OPENSHELL_IMAGE : RSI_OPENSHELL_IMAGE
327
+ if (configuredImage && configuredImage !== image) {
328
+ throw new Error(`RSI requires the verified OpenShell image ${image} for ${mode}; custom images are not verified`)
329
+ }
330
+ let readOnlyMounts: Array<{ source: string; target: string }> = []
331
+ if (trainingMode) {
332
+ const configuredBase = process.env.HEADLESSCODE_RSI_BASE_MODEL_PATH?.trim()
333
+ if (!configuredBase) throw new Error("model training/evaluation requires HEADLESSCODE_RSI_BASE_MODEL_PATH on the worker host")
334
+ const checkpoint = inspectLocalRsiCheckpoint(configuredBase, mode)
335
+ readOnlyMounts = [{ source: checkpoint.path, target: "/workspace/.rsi-base-model" }]
336
+ }
337
+ return {
338
+ image,
339
+ policy,
340
+ providers: [],
341
+ autoProviders: false,
342
+ importResults: true,
343
+ includeProjectData: false,
344
+ projectIdentityRoot: "/workspace",
345
+ commandTimeoutMs: timeoutMs,
346
+ networkPolicy: networkRules(mode),
347
+ ...(trainingMode ? { readOnlyMounts } : {}),
348
+ ...(mode === "training" ? { gpu: 1 } : {}),
349
+ }
350
+ }
351
+
352
+ function makeRequest(repoRoot: string, name: string, env: Record<string, string>): CloudSessionRequest {
353
+ return {
354
+ repo: repoRoot,
355
+ issue: { number: 0, title: `RSI isolated ${name}` },
356
+ worktreeSpec: { name, issues: [], taskFile: "RSI_TASK.md" },
357
+ env,
358
+ }
359
+ }
360
+
361
+ function snapshotChanges(snapshot: string, baseCommit: string): Array<{ path: string; mode: string; status: string }> {
362
+ const raw = execFileBuffer("git", ["diff", "--raw", "-z", "--no-renames", `${baseCommit}...HEAD`], { cwd: snapshot })
363
+ const records = raw.toString("utf8").split("\0")
364
+ const changes: Array<{ path: string; mode: string; status: string }> = []
365
+ for (let index = 0; index < records.length - 1; index += 2) {
366
+ const metadata = /^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])$/.exec(records[index])
367
+ const file = records[index + 1]
368
+ if (!metadata || !file) throw new Error("OpenShell mutation artifact contains malformed Git diff metadata")
369
+ changes.push({ path: file, mode: metadata[2], status: metadata[3] })
370
+ }
371
+ return changes
372
+ }
373
+
374
+ function applyCandidatePatch(snapshot: string, snapshotBase: string, candidate: CandidateRecord, config: RsiConfig): string[] {
375
+ const changes = snapshotChanges(snapshot, snapshotBase)
376
+ const files = changes.map((change) => change.path)
377
+ if (files.some((file) => !pathIsSafe(file))) throw new Error("OpenShell mutation artifact contains an unsafe path")
378
+ const invalidModes = changes.filter((change) => change.status !== "D" && change.mode !== "100644" && change.mode !== "100755")
379
+ if (invalidModes.length > 0) throw new Error(`OpenShell mutation artifact contains symlinks, submodules, or special files: ${invalidModes.map((change) => change.path).join(", ")}`)
380
+ const forbiddenMetadata = new Set([".gitattributes", ".gitmodules", ".gitignore"])
381
+ const violations = files.filter((file) => findMatchingPattern(file, config.protectedPaths) !== null || alwaysOmit(file) || file === "node_modules" || forbiddenMetadata.has(file))
382
+ if (violations.length > 0) throw new Error(`OpenShell mutation artifact changed protected paths: ${violations.join(", ")}`)
383
+ const commits = execFileSync("git", ["rev-list", "--count", `${snapshotBase}..HEAD`], { cwd: snapshot, encoding: "utf8" }).trim()
384
+ if (Number(commits) === 0 || files.length === 0) throw new Error("OpenShell mutation produced no committed source changes")
385
+ const patch = execFileBuffer("git", ["diff", "--binary", "--no-ext-diff", "--no-renames", `${snapshotBase}...HEAD`], { cwd: snapshot })
386
+ const tempPatch = path.join(path.dirname(candidate.worktree), `.rsi-${candidate.id}-${randomUUID()}.patch`)
387
+ try {
388
+ fs.writeFileSync(tempPatch, patch, { mode: 0o600, flag: "wx" })
389
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "apply", "--check", "--index", tempPatch], { cwd: candidate.worktree, stdio: "pipe" })
390
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "apply", "--index", tempPatch], { cwd: candidate.worktree, stdio: "pipe" })
391
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "commit", "--quiet", "-m", `RSI candidate ${candidate.id}`], { cwd: candidate.worktree, stdio: "pipe" })
392
+ } finally {
393
+ fs.rmSync(tempPatch, { force: true })
394
+ }
395
+ return files
396
+ }
397
+
398
+ /** Validate and import an untrusted worker patch into the supervisor candidate worktree. */
399
+ export function applyFleetMutationPatch(patch: Buffer, candidate: CandidateRecord, config: RsiConfig): string[] {
400
+ if (patch.byteLength === 0 || patch.byteLength > MAX_SNAPSHOT_BYTES) throw new Error("fleet worker returned an empty or oversized patch")
401
+ const tempPatch = path.join(path.dirname(candidate.worktree), `.rsi-fleet-${candidate.id}-${randomUUID()}.patch`)
402
+ try {
403
+ fs.writeFileSync(tempPatch, patch, { mode: 0o600, flag: "wx" })
404
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "apply", "--check", "--index", tempPatch], { cwd: candidate.worktree, stdio: "pipe" })
405
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "apply", "--index", tempPatch], { cwd: candidate.worktree, stdio: "pipe" })
406
+ // The patch is applied with --index, before the supervisor creates its
407
+ // commit. Compare the staged index directly; a base...HEAD diff is empty
408
+ // at this point and would reject every valid worker patch.
409
+ const changes = stagedSnapshotChanges(candidate.worktree)
410
+ const files = changes.map((change) => change.path)
411
+ if (files.length === 0 || files.some((file) => !pathIsSafe(file))) throw new Error("fleet worker patch produced no files or an unsafe path")
412
+ const invalidModes = changes.filter((change) => change.status !== "D" && change.mode !== "100644" && change.mode !== "100755")
413
+ if (invalidModes.length > 0) throw new Error(`fleet worker patch contains symlinks or special files: ${invalidModes.map((change) => change.path).join(", ")}`)
414
+ const forbiddenMetadata = new Set([".gitattributes", ".gitmodules", ".gitignore"])
415
+ const violations = files.filter((file) => findMatchingPattern(file, config.protectedPaths) !== null || alwaysOmit(file) || file === "node_modules" || forbiddenMetadata.has(file))
416
+ if (violations.length > 0) throw new Error(`fleet worker patch changed protected paths: ${violations.join(", ")}`)
417
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "commit", "--quiet", "-m", `RSI candidate ${candidate.id}`], { cwd: candidate.worktree, stdio: "pipe" })
418
+ return files
419
+ } catch (error) {
420
+ execFileSync("git", ["reset", "--hard", "HEAD"], { cwd: candidate.worktree, stdio: "ignore" })
421
+ throw error
422
+ } finally {
423
+ fs.rmSync(tempPatch, { force: true })
424
+ }
425
+ }
426
+
427
+ function stagedSnapshotChanges(snapshot: string): Array<{ path: string; mode: string; status: string }> {
428
+ const raw = execFileBuffer("git", ["diff", "--cached", "--raw", "-z", "--no-renames", "HEAD"], { cwd: snapshot })
429
+ const records = raw.toString("utf8").split("\0")
430
+ const changes: Array<{ path: string; mode: string; status: string }> = []
431
+ for (let index = 0; index < records.length - 1; index += 2) {
432
+ const metadata = /^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])$/.exec(records[index])
433
+ const file = records[index + 1]
434
+ if (!metadata || !file) throw new Error("fleet worker patch produced malformed staged Git diff metadata")
435
+ changes.push({ path: file, mode: metadata[2], status: metadata[3] })
436
+ }
437
+ return changes
438
+ }
439
+
440
+ async function runSnapshot(options: IsolatedRunOptions & { preserveSnapshot?: boolean }): Promise<{ result: TrialResult; snapshot: string; snapshotBase: string }> {
441
+ if (options.config.hiddenEvalCommands.length > 0) {
442
+ throw new Error("RSI hidden evaluations are disabled until an evaluator service can keep test inputs outside candidate execution")
443
+ }
444
+ if (options.mode === "evaluation" && /(?:scripts\/eval-suite|\.headlesscode)/i.test(options.command)) {
445
+ throw new Error("RSI evaluation command refers to supervisor-only evaluation material")
446
+ }
447
+ const name = options.sessionName ?? `rsi-${options.mode}-${randomUUID().slice(0, 12)}`
448
+ const snapshot = path.join(options.config.repoRoot, ".worktrees", name)
449
+ fs.mkdirSync(path.dirname(snapshot), { recursive: true })
450
+ const snapshotBase = initializeSnapshot(options.sourceRoot, options.commit, snapshot, options.mode, options.config)
451
+ const factory = options.providerFactory ?? ((providerConfig) => new OpenShellSessionProvider(providerConfig))
452
+ const provider = factory(providerOptions(options.config, options.mode, options.timeoutMs))
453
+ const request = makeRequest(options.config.repoRoot, name, options.env ?? {})
454
+ let handle: SessionHandle | undefined
455
+ const started = Date.now()
456
+ try {
457
+ const startedHandle = await provider.spawnExistingWorktreeSession(request, snapshot)
458
+ handle = startedHandle
459
+ await provider.waitReady(startedHandle)
460
+ if (options.mode === "mutation") {
461
+ // OpenShell 0.1.2 polls provider environment revisions about every
462
+ // 10 seconds. Its first empty-environment sync advances the policy
463
+ // generation and closes older in-flight tunnels, even with no attached
464
+ // providers. Let that initial sync settle before local inference starts.
465
+ const remaining = OPENSHELL_INITIAL_CONFIG_SETTLE_MS - (Date.now() - started)
466
+ if (remaining > 0) await delay(remaining)
467
+ }
468
+ const command = `cd /workspace && ${options.command}`
469
+ const result = await provider.runHarness(startedHandle, command)
470
+ return {
471
+ result: {
472
+ ok: result.exitCode === 0,
473
+ command: options.command,
474
+ exitCode: result.exitCode,
475
+ durationMs: Date.now() - started,
476
+ stdout: result.output,
477
+ stderr: "",
478
+ },
479
+ snapshot,
480
+ snapshotBase,
481
+ }
482
+ } finally {
483
+ try {
484
+ if (handle) await provider.teardown(handle)
485
+ } finally {
486
+ if (!options.preserveSnapshot && options.mode !== "mutation" && !fs.existsSync(path.join(snapshot, ".harness.openshell-sandbox"))) {
487
+ fs.rmSync(snapshot, { recursive: true, force: true })
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ /** Run the fixed QLoRA or matched-model evaluator command inside OpenShell. */
494
+ export async function runOpenShellModelJob(candidate: CandidateRecord, config: RsiConfig, task: NonNullable<import("./postgres-queue.js").FleetJobPayload["modelTask"]>, providerFactory?: RsiOpenShellProviderFactory): Promise<{ result: Record<string, unknown>; adapterArchive?: Buffer }> {
495
+ if (task.seed !== 1337 || task.baseModelPath !== "/workspace/.rsi-base-model" || !/^[A-Za-z0-9._:@/-]{1,180}$/.test(task.baseModelId)) throw new Error("model job does not match the fixed dataset seed, base-model mount, or model identity")
496
+ const training = task.kind === "qlora-train"
497
+ if (training && (task.maxSteps !== 8 || task.adapterPath || task.modelKind)) throw new Error("training job differs from the bounded QLoRA policy")
498
+ if (!training && (!task.modelKind || !task.cellIds.length || (task.modelKind === "adapter" && !task.adapterPath) || (task.modelKind === "base" && task.adapterPath))) throw new Error("model evaluation job is missing or mismatches its fixed cell/model identity")
499
+ const output = "/workspace/.headlesscode-rsi-model-output"
500
+ const command = training
501
+ ? `python3 /opt/headlesscode-rsi-training/train_qlora.py --base-model /workspace/.rsi-base-model --base-model-id '${task.baseModelId}' --dataset /workspace/.headlesscode-rsi-model/dataset.jsonl --manifest /workspace/.headlesscode-rsi-model/manifest.json --output /workspace/.headlesscode-rsi-model-output`
502
+ : `python3 /opt/headlesscode-rsi-training/evaluate_adapter.py --base-model /workspace/.rsi-base-model --base-model-id '${task.baseModelId}' --cells /workspace/.headlesscode-rsi-model/evaluation-cells.jsonl --output ${output}/evaluation-result.json${task.modelKind === "adapter" ? " --adapter-archive /workspace/.headlesscode-rsi-model/adapter.tar" : ""}`
503
+ const isolated = await runSnapshot({
504
+ config,
505
+ sourceRoot: candidate.worktree,
506
+ commit: execFileSync("git", ["rev-parse", "HEAD"], { cwd: candidate.worktree, encoding: "utf8" }).trim(),
507
+ command,
508
+ timeoutMs: config.commandTimeoutMs,
509
+ mode: training ? "training" : "model-evaluation",
510
+ sessionName: `rsi-model-${candidate.id}-${randomUUID().slice(0, 8)}`,
511
+ providerFactory,
512
+ preserveSnapshot: true,
513
+ })
514
+ try {
515
+ const resultName = training ? "training-result.json" : "evaluation-result.json"
516
+ const resultPath = path.join(isolated.snapshot, ".headlesscode-rsi-model-output", resultName)
517
+ if (!fs.existsSync(resultPath)) throw new Error(`OpenShell model job omitted ${resultName}; exit=${isolated.result.exitCode}`)
518
+ const result = JSON.parse(fs.readFileSync(resultPath, "utf8")) as Record<string, unknown>
519
+ if (result.schemaVersion !== 1 || !["completed", "failed", "partial"].includes(String(result.status))) throw new Error("OpenShell model job returned an invalid result record")
520
+ if (training && result.status === "completed") {
521
+ const archive = execFileBuffer("git", ["archive", "--format=tar", "HEAD", ".headlesscode-rsi-model-output/adapter"], { cwd: isolated.snapshot })
522
+ if (archive.byteLength < 1) throw new Error("completed QLoRA job produced no adapter archive")
523
+ return { result, adapterArchive: archive }
524
+ }
525
+ return { result }
526
+ } finally {
527
+ if (!fs.existsSync(path.join(isolated.snapshot, ".harness.openshell-sandbox"))) fs.rmSync(isolated.snapshot, { recursive: true, force: true })
528
+ }
529
+ }
530
+
531
+ export async function runOpenShellMutation(candidate: CandidateRecord, config: RsiConfig, providerFactory?: RsiOpenShellProviderFactory): Promise<{ ok: boolean; result?: TrialResult; error?: string }> {
532
+ try {
533
+ const workerCommand = [
534
+ "/opt/headlesscode/node_modules/.bin/tsx",
535
+ "/opt/headlesscode/src/cli.ts",
536
+ "--mode",
537
+ "code",
538
+ "--max-iterations",
539
+ String(config.maxIterations),
540
+ "--no-checkpoints",
541
+ "--task",
542
+ mutationTask(candidate, config),
543
+ ].map(shellQuote).join(" ")
544
+ const boundaryGuard = [
545
+ 'test "$(git -C /workspace rev-list --count HEAD)" = 1',
546
+ "test ! -e /workspace/src/rsi",
547
+ "test ! -e /workspace/scripts",
548
+ "test ! -e /workspace/tests",
549
+ "test ! -e /workspace/test",
550
+ "test ! -e /workspace/.env",
551
+ "test ! -e /workspace/.headlesscode",
552
+ "test ! -e /opt/headlesscode/src/rsi",
553
+ "test ! -e /opt/headlesscode/scripts/eval-suite",
554
+ "test -z \"$(find /workspace /opt/headlesscode/src -type f \\( -path '*/__tests__/*' -o -name '*.test.*' -o -name '*.spec.*' \\) -print -quit)\"",
555
+ 'test -z "${HEADLESSCODE_OPENROUTER_API_KEY:-}${OPENROUTER_API_KEY:-}${OPENAI_API_KEY:-}${ANTHROPIC_API_KEY:-}"',
556
+ `exec ${workerCommand}`,
557
+ ].join(" && ")
558
+ const env = workerEnvironment(config, candidate)
559
+ const isolation = await runSnapshot({
560
+ config,
561
+ sourceRoot: candidate.worktree,
562
+ commit: candidate.baseCommit,
563
+ command: boundaryGuard,
564
+ timeoutMs: config.commandTimeoutMs,
565
+ mode: "mutation",
566
+ sessionName: `rsi-mutation-${candidate.id}-${randomUUID().slice(0, 6)}`,
567
+ env,
568
+ providerFactory,
569
+ })
570
+ if (!isolation.result.ok) {
571
+ const detail = isolation.result.stderr || isolation.result.stdout
572
+ return { ok: false, result: isolation.result, error: `OpenShell mutation worker failed (exit ${isolation.result.exitCode ?? "unknown"})${detail ? `: ${detail.slice(0, 3000)}` : ""}` }
573
+ }
574
+ const changedFiles = applyCandidatePatch(isolation.snapshot, isolation.snapshotBase, candidate, config)
575
+ if (isolation.result.ok && changedFiles.length === 0) return { ok: false, result: isolation.result, error: "OpenShell worker returned success without a candidate change" }
576
+ return { ok: true, result: isolation.result }
577
+ } catch (error) {
578
+ return {
579
+ ok: false,
580
+ result: { ok: false, command: "openshell-rsi-mutation", exitCode: null, durationMs: 0, stdout: "", stderr: error instanceof Error ? error.message : String(error) },
581
+ error: error instanceof Error ? error.message : String(error),
582
+ }
583
+ } finally {
584
+ const snapshotPrefix = `rsi-mutation-${candidate.id}`
585
+ try {
586
+ for (const name of fs.readdirSync(path.join(config.repoRoot, ".worktrees"))) {
587
+ if (name.startsWith(snapshotPrefix)) {
588
+ const snapshot = path.join(config.repoRoot, ".worktrees", name)
589
+ if (!fs.existsSync(path.join(snapshot, ".harness.openshell-sandbox"))) fs.rmSync(snapshot, { recursive: true, force: true })
590
+ }
591
+ }
592
+ } catch {
593
+ // Keep a failed cleanup visible for operator recovery.
594
+ }
595
+ }
596
+ }
597
+
598
+ function mutationTask(candidate: CandidateRecord, config: RsiConfig): string {
599
+ const hypothesis = candidate.hypothesis
600
+ return [
601
+ "You are a bounded RSI worker inside an OpenShell sandbox.",
602
+ `Improve the isolated candidate snapshot for generation ${candidate.generation}.`,
603
+ `Mutation class: ${candidate.mutationKind ?? "corrective"}. Compute policy: ${config.computePolicy ?? "single"}.`,
604
+ `Objective: ${config.mutationTask}`,
605
+ hypothesis ? `Hypothesis: ${hypothesis.statement}\nExpected effect: ${hypothesis.expectedEffect}\nPotential downside: ${hypothesis.potentialDownside}${hypothesis.evidence ? `\nEvidence: ${hypothesis.evidence}` : ""}` : "",
606
+ "The workspace contains only candidate source files. Supervisor evaluation scripts, scoring state, project history, and operator data are not mounted.",
607
+ "Make a focused source change and commit it. Do not run tests; the supervisor will evaluate the sealed artifact in a separate OpenShell session.",
608
+ ].filter(Boolean).join("\n\n")
609
+ }
610
+
611
+ export function createOpenShellCommandRunner(config: RsiConfig): CommandRunner {
612
+ return async (command, cwd, timeoutMs, env) => {
613
+ const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim()
614
+ const safeEnv: Record<string, string> = {}
615
+ if (env?.HEADLESSCODE_RSI_CANDIDATE_ROOT) safeEnv.HEADLESSCODE_RSI_CANDIDATE_ROOT = "/workspace"
616
+ try {
617
+ const isolated = await runSnapshot({ config, sourceRoot: cwd, commit, command, timeoutMs, mode: "evaluation", env: safeEnv })
618
+ return isolated.result
619
+ } catch (error) {
620
+ return { ok: false, command, exitCode: null, durationMs: 0, stdout: "", stderr: error instanceof Error ? error.message : String(error) }
621
+ }
622
+ }
623
+ }
624
+
625
+ /** Execute only visible candidate checks inside a fresh OpenShell guest. */
626
+ export async function runOpenShellEvaluation(candidate: CandidateRecord, config: RsiConfig): Promise<{ regression: TrialResult; visible: TrialResult[] }> {
627
+ if (config.hiddenEvalCommands.length > 0) throw new Error("hidden RSI evaluation is supervisor-only and cannot be submitted as a worker job")
628
+ const commands = [config.regressionCommand, ...config.evalCommands]
629
+ const results: TrialResult[] = []
630
+ for (const command of commands) {
631
+ const isolated = await runSnapshot({ config, sourceRoot: candidate.worktree, commit: execFileSync("git", ["rev-parse", "HEAD"], { cwd: candidate.worktree, encoding: "utf8" }).trim(), command, timeoutMs: config.commandTimeoutMs, mode: "evaluation" })
632
+ results.push(isolated.result)
633
+ }
634
+ return { regression: results[0], visible: results.slice(1) }
635
+ }
636
+
637
+ export function openShellMutationRunner(): typeof runOpenShellMutation {
638
+ return runOpenShellMutation
639
+ }