headlesscode 1.2.1 → 1.2.2

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,582 @@
1
+ /**
2
+ * OpenShellSessionProvider — experimental CloudProvider backed by NVIDIA
3
+ * OpenShell. Each session uses a disposable, independent Git clone mounted at
4
+ * /workspace. The host worktree and its process-control files are never mounted.
5
+ * After sandbox deletion, a verified Git bundle imports changes to the assigned
6
+ * host branch.
7
+ */
8
+ import { execFileSync, spawnSync } from "node:child_process"
9
+ import { createHash } from "node:crypto"
10
+ import * as fs from "node:fs"
11
+ import * as os from "node:os"
12
+ import * as path from "node:path"
13
+ import { parse, stringify } from "yaml"
14
+
15
+ import { writeTaskFiles } from "../orchestrator/cli.js"
16
+ import { projectStoreRoot, resolveProjectDataDir } from "../project-store.js"
17
+ import type { CloudProvider, CloudSessionRequest, CommandResult, SessionHandle } from "./provider.js"
18
+
19
+ /** The harness is baked into the sandbox image; never mount the host checkout. */
20
+ export const OPENSHELL_HARNESS_ROOT = "/opt/headlesscode"
21
+
22
+ export const OPENSHELL_WORKSPACE_TARGET = "/workspace"
23
+
24
+ interface OpenShellWorkspaceState {
25
+ hostWorkspace: string
26
+ sandboxWorkspace: string
27
+ exportDir: string
28
+ tempRoot: string
29
+ lockPath: string
30
+ memorySnapshot?: string
31
+ memoryHost?: string
32
+ branchRef: string
33
+ baseCommit: string
34
+ importResults: boolean
35
+ }
36
+
37
+ export function writeOpenShellSandboxMarker(workspace: string, name: string): string {
38
+ const marker = path.join(workspace, ".harness.openshell-sandbox")
39
+ try {
40
+ const stat = fs.lstatSync(marker)
41
+ if (!stat.isFile() || stat.isSymbolicLink() || fs.readFileSync(marker, "utf8").trim() !== name) {
42
+ throw new Error(`refusing to overwrite unexpected sandbox marker at ${marker}`)
43
+ }
44
+ } catch (error) {
45
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
46
+ }
47
+ const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK
48
+ const fd = fs.openSync(marker, flags, 0o600)
49
+ try {
50
+ fs.fchmodSync(fd, 0o600)
51
+ fs.writeFileSync(fd, `${name}\n`)
52
+ } finally {
53
+ fs.closeSync(fd)
54
+ }
55
+ return marker
56
+ }
57
+
58
+ export interface OpenShellSessionProviderOptions {
59
+ image?: string
60
+ policy?: string
61
+ providers?: string[]
62
+ cpu?: string
63
+ memory?: string
64
+ namePrefix?: string
65
+ readOnlyMounts?: Array<{ source: string; target: string }>
66
+ importResults?: boolean
67
+ scratchRoot?: string
68
+ run?: (args: string[], timeoutMs?: number) => CommandResult
69
+ listSandboxes?: () => string[]
70
+ }
71
+
72
+ export class OpenShellSessionProvider implements CloudProvider {
73
+ readonly name = "openshell"
74
+ private readonly image: string
75
+ private readonly policy?: string
76
+ private readonly providers: string[]
77
+ private readonly cpu: string
78
+ private readonly memory: string
79
+ private readonly namePrefix: string
80
+ private readonly readOnlyMounts: Array<{ source: string; target: string }>
81
+ private readonly run: (args: string[], timeoutMs?: number) => CommandResult
82
+ private readonly listSandboxes: () => string[]
83
+ private readonly importResults: boolean
84
+ private readonly scratchRoot: string
85
+ private readonly workspaces = new Map<string, OpenShellWorkspaceState>()
86
+
87
+ constructor(options: OpenShellSessionProviderOptions = {}) {
88
+ this.image = options.image ?? process.env.HEADLESSCODE_OPENSHELL_IMAGE ?? "headlesscode-openshell:local"
89
+ this.policy = options.policy ?? process.env.HEADLESSCODE_OPENSHELL_POLICY
90
+ this.providers = options.providers ?? (process.env.HEADLESSCODE_OPENSHELL_PROVIDERS ?? "headlesscode-openrouter").split(",").map((name) => name.trim()).filter(Boolean)
91
+ this.cpu = options.cpu ?? process.env.HEADLESSCODE_OPENSHELL_CPU ?? "2"
92
+ this.memory = options.memory ?? process.env.HEADLESSCODE_OPENSHELL_MEMORY ?? "4Gi"
93
+ this.namePrefix = options.namePrefix ?? process.env.HEADLESSCODE_OPENSHELL_NAME_PREFIX ?? "hcls"
94
+ this.readOnlyMounts = options.readOnlyMounts ?? []
95
+ this.importResults = options.importResults ?? true
96
+ this.scratchRoot = path.resolve(options.scratchRoot ?? process.env.HEADLESSCODE_OPENSHELL_SCRATCH_ROOT ?? path.join(os.homedir(), ".local", "share", "headlesscode", "openshell-sessions"))
97
+ this.run = options.run ?? ((args, timeoutMs) => {
98
+ const result = spawnSync("openshell", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: timeoutMs })
99
+ return { exitCode: result.status ?? 1, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() }
100
+ })
101
+ this.listSandboxes = options.listSandboxes ?? (() => {
102
+ const result = this.run(["sandbox", "list", "--output", "json"])
103
+ if (result.exitCode !== 0) throw new Error(`OpenShell sandbox list failed: ${result.output}`)
104
+ try {
105
+ const parsed = JSON.parse(result.output) as unknown
106
+ const value = Array.isArray(parsed)
107
+ ? parsed
108
+ : typeof parsed === "object" && parsed !== null && "sandboxes" in parsed && Array.isArray((parsed as { sandboxes: unknown }).sandboxes)
109
+ ? (parsed as { sandboxes: unknown[] }).sandboxes
110
+ : undefined
111
+ if (!value || value.some((item) => typeof item !== "object" || item === null || !("name" in item) || typeof (item as { name: unknown }).name !== "string")) {
112
+ throw new Error("expected a sandbox array with string names")
113
+ }
114
+ return value.map((item) => (item as { name: string }).name)
115
+ } catch (error) {
116
+ throw new Error(`OpenShell sandbox list returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`)
117
+ }
118
+ })
119
+ }
120
+
121
+ private sandboxName(handle: SessionHandle): string { return handle.containerId ?? this.sandboxNameForWorktree(handle.id) }
122
+
123
+ /** Stable OpenShell name shared by worker launch and stop/recovery paths. */
124
+ sandboxNameForWorktree(worktreeName: string, repoPath = ""): string {
125
+ const prefix = this.namePrefix.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "") || "hcls"
126
+ const prefixPart = prefix.slice(0, 3).replace(/-+$/g, "") || "h"
127
+ const identity = `${path.resolve(repoPath || process.cwd())}\0${worktreeName}`
128
+ const hash = createHash("sha256").update(identity).digest("hex").slice(0, 19 - prefixPart.length - 1)
129
+ const name = `${prefixPart}-${hash}`
130
+ if (name.length > 19 || !/^[a-z0-9](?:[a-z0-9-]{0,17}[a-z0-9])?$/.test(name)) {
131
+ throw new Error(`OpenShell sandbox name is not a DNS label: ${name}`)
132
+ }
133
+ return name
134
+ }
135
+
136
+ async spawnWorktreeSession(request: CloudSessionRequest): Promise<SessionHandle> {
137
+ const repo = path.resolve(request.repo)
138
+ if (!/^[A-Za-z0-9._-]+$/.test(request.worktreeSpec.name)) {
139
+ throw new Error(`Invalid OpenShell worktree name: ${request.worktreeSpec.name}`)
140
+ }
141
+ const workspace = path.join(repo, ".worktrees", request.worktreeSpec.name)
142
+ const name = this.sandboxNameForWorktree(request.worktreeSpec.name, repo)
143
+ try {
144
+ if (!fs.statSync(path.join(repo, ".git")).isDirectory()) throw new Error("not a repository root")
145
+ } catch {
146
+ throw new Error(`OpenShell provider requires a Git repository root with a .git directory: ${repo}`)
147
+ }
148
+ writeTaskFiles(repo, [request.worktreeSpec], [request.issue])
149
+ const branch = `headlesscode/${request.worktreeSpec.name}-${Date.now()}`
150
+ const wt = spawnSync("git", ["worktree", "add", "-b", branch, workspace, "HEAD"], { cwd: repo, encoding: "utf8" })
151
+ if (wt.status !== 0) throw new Error(`OpenShell worktree creation failed: ${wt.stderr || wt.stdout}`)
152
+ const taskFile = path.join(repo, "plans", "parallel-tasks", request.worktreeSpec.taskFile)
153
+ try {
154
+ fs.copyFileSync(taskFile, path.join(workspace, "ORCHESTRATOR_TASK.md"))
155
+ } catch (error) {
156
+ spawnSync("git", ["worktree", "remove", "--force", workspace], { cwd: repo, encoding: "utf8" })
157
+ spawnSync("git", ["branch", "-D", branch], { cwd: repo, encoding: "utf8" })
158
+ throw error
159
+ }
160
+ return this.spawnExistingWorktreeSession(request, workspace)
161
+ }
162
+
163
+ /** Start a sandbox around a worktree already created by the normal spawner. */
164
+ async spawnExistingWorktreeSession(request: CloudSessionRequest, worktreePath: string): Promise<SessionHandle> {
165
+ const repo = path.resolve(request.repo)
166
+ const workspace = path.resolve(worktreePath)
167
+ const expected = path.join(repo, ".worktrees", request.worktreeSpec.name)
168
+ if (workspace !== expected) throw new Error(`OpenShell worker path must be ${expected}`)
169
+ if (!fs.statSync(workspace).isDirectory()) throw new Error(`OpenShell worker worktree does not exist: ${workspace}`)
170
+ const name = this.sandboxNameForWorktree(request.worktreeSpec.name, repo)
171
+ const isolated = this.prepareIsolatedWorkspace(workspace, name)
172
+ try {
173
+ return await this.createSandbox(request, repo, workspace, isolated, name)
174
+ } catch (error) {
175
+ if (!fs.existsSync(path.join(workspace, ".harness.openshell-sandbox"))) {
176
+ this.workspaces.delete(name)
177
+ fs.rmSync(isolated.tempRoot, { recursive: true, force: true })
178
+ fs.rmSync(isolated.lockPath, { recursive: true, force: true })
179
+ }
180
+ throw error
181
+ }
182
+ }
183
+
184
+ private prepareIsolatedWorkspace(hostWorkspace: string, sandboxName: string): OpenShellWorkspaceState {
185
+ const tempRoot = path.join(this.scratchRoot, `headlesscode-openshell-session-${sandboxName}`)
186
+ const lockPath = `${tempRoot}.lock`
187
+ const sandboxWorkspace = path.join(tempRoot, "workspace")
188
+ const exportDir = path.join(tempRoot, "result")
189
+ let ownsLock = false
190
+ let ownsTempRoot = false
191
+ try {
192
+ fs.mkdirSync(this.scratchRoot, { recursive: true, mode: 0o700 })
193
+ const rootStat = fs.lstatSync(this.scratchRoot)
194
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error(`refusing unexpected OpenShell scratch root ${this.scratchRoot}`)
195
+ fs.mkdirSync(lockPath, { mode: 0o700 })
196
+ ownsLock = true
197
+ if (this.listSandboxes().includes(sandboxName)) throw new Error(`OpenShell sandbox ${sandboxName} is already active`)
198
+ try {
199
+ const existing = fs.lstatSync(tempRoot)
200
+ if (!existing.isDirectory() || existing.isSymbolicLink()) throw new Error(`refusing unexpected OpenShell scratch path ${tempRoot}`)
201
+ fs.rmSync(tempRoot, { recursive: true, force: true })
202
+ } catch (error) {
203
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
204
+ }
205
+ fs.mkdirSync(tempRoot, { mode: 0o700 })
206
+ ownsTempRoot = true
207
+ fs.mkdirSync(exportDir, { mode: 0o700 })
208
+ const branchRef = execFileSync("git", ["symbolic-ref", "HEAD"], { cwd: hostWorkspace, encoding: "utf8" }).trim()
209
+ if (!branchRef.startsWith("refs/heads/")) throw new Error(`OpenShell requires a local worktree branch, got ${branchRef}`)
210
+ const branchName = branchRef.slice("refs/heads/".length)
211
+ const baseCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: hostWorkspace, encoding: "utf8" }).trim()
212
+ const cloned = spawnSync("git", ["clone", "--no-local", "--single-branch", "--branch", branchName, hostWorkspace, sandboxWorkspace], { encoding: "utf8", timeout: 120_000 })
213
+ if (cloned.status !== 0) throw new Error(`OpenShell isolated Git clone failed: ${cloned.stderr || cloned.stdout}`)
214
+ const ident = execFileSync("git", ["var", "GIT_AUTHOR_IDENT"], { cwd: hostWorkspace, encoding: "utf8" }).trim()
215
+ const match = /^(.*) <([^<>]+)> \d+ [+-]\d{4}$/.exec(ident)
216
+ if (match) {
217
+ execFileSync("git", ["-C", sandboxWorkspace, "config", "user.name", match[1]])
218
+ execFileSync("git", ["-C", sandboxWorkspace, "config", "user.email", match[2]])
219
+ }
220
+ fs.appendFileSync(path.join(sandboxWorkspace, ".git", "info", "exclude"), "\n/ORCHESTRATOR_TASK.md\n/ORCHESTRATOR_PLAN.md\n/.qa-task.md\n/.env\n/.env.*\n/.headlesscode/\n/.headlesscode-session/\n/.harness.*\n/harness.log\n/qa.log\n/review.log\n")
221
+ // Stage only known orchestration inputs that wrappers can reference by
222
+ // path. The disposable clone does not include arbitrary host files.
223
+ for (const inputName of ["ORCHESTRATOR_TASK.md", "ORCHESTRATOR_PLAN.md", ".qa-task.md"]) {
224
+ const inputPath = path.join(hostWorkspace, inputName)
225
+ try {
226
+ const inputStat = fs.lstatSync(inputPath)
227
+ if (!inputStat.isFile() || inputStat.isSymbolicLink()) throw new Error(`OpenShell task input is not a regular file: ${inputPath}`)
228
+ fs.copyFileSync(inputPath, path.join(sandboxWorkspace, inputName))
229
+ } catch (error) {
230
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
231
+ }
232
+ }
233
+ // The orchestrator creates this file from port overrides only for
234
+ // OpenShell worktrees. Copy just those known-safe settings.
235
+ const envSource = path.join(hostWorkspace, ".env")
236
+ try {
237
+ if (fs.lstatSync(envSource).isFile()) {
238
+ const safeEnv = fs.readFileSync(envSource, "utf8").split(/\r?\n/).filter((line) => /^(COMPOSE_PROJECT_NAME|POSTGRES_PORT|AIRUNNER_HTTP_PORT|VITE_PORT)=/.test(line))
239
+ fs.writeFileSync(path.join(sandboxWorkspace, ".env"), `${safeEnv.join("\n")}\n`, { mode: 0o600 })
240
+ }
241
+ } catch (error) {
242
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
243
+ }
244
+ return { hostWorkspace, sandboxWorkspace, exportDir, tempRoot, lockPath, branchRef, baseCommit, importResults: this.importResults }
245
+ } catch (error) {
246
+ if (ownsTempRoot) fs.rmSync(tempRoot, { recursive: true, force: true })
247
+ if (ownsLock) fs.rmSync(lockPath, { recursive: true, force: true })
248
+ throw error
249
+ }
250
+ }
251
+
252
+ private async createSandbox(
253
+ request: CloudSessionRequest,
254
+ repo: string,
255
+ hostWorkspace: string,
256
+ isolated: OpenShellWorkspaceState,
257
+ name: string,
258
+ ): Promise<SessionHandle> {
259
+ const { sandboxWorkspace } = isolated
260
+ if (!this.policy || !fs.existsSync(this.policy)) throw new Error("OpenShell requires a base policy file")
261
+ const args = ["sandbox", "create", "--detach", "--auto-providers", "--cpu", this.cpu, "--memory", this.memory, "--name", name, "--from", this.image]
262
+ if (this.policy) args.push("--policy", path.resolve(this.policy))
263
+ for (const provider of this.providers) args.push("--provider", provider)
264
+ // Only the disposable clone is writable inside the sandbox. Host Git
265
+ // metadata and process-control files live in the separate orchestration
266
+ // worktree, which is never mounted into the container.
267
+ const mounts: Array<{ type: string; source: string; target: string; read_only: boolean }> = [
268
+ { type: "bind", source: sandboxWorkspace, target: OPENSHELL_WORKSPACE_TARGET, read_only: false },
269
+ { type: "bind", source: isolated.exportDir, target: "/result", read_only: false },
270
+ ]
271
+ const projectData = resolveProjectDataDir(repo)
272
+ const projectDataTarget = "/opt/headlesscode-data/projects/" + path.basename(projectData)
273
+ const dataSnapshotRoot = path.join(isolated.tempRoot, "data")
274
+ const projectDataSnapshot = path.join(dataSnapshotRoot, "projects", path.basename(projectData))
275
+ fs.mkdirSync(path.dirname(projectDataSnapshot), { recursive: true })
276
+ fs.cpSync(projectData, projectDataSnapshot, { recursive: true, dereference: true })
277
+ mounts.push({ type: "bind", source: projectDataSnapshot, target: projectDataTarget, read_only: true })
278
+ const policy = this.policy ? parse(fs.readFileSync(this.policy, "utf8")) as Record<string, any> : undefined
279
+ const filesystemPolicy = policy?.filesystem_policy
280
+ if (!filesystemPolicy || !Array.isArray(filesystemPolicy.read_only) || !Array.isArray(filesystemPolicy.read_write)) {
281
+ throw new Error("policy must define filesystem_policy.read_only and read_write arrays")
282
+ }
283
+ filesystemPolicy.read_write.push(OPENSHELL_WORKSPACE_TARGET)
284
+ filesystemPolicy.read_write.push("/result")
285
+ filesystemPolicy.read_only.push(projectDataTarget, OPENSHELL_HARNESS_ROOT)
286
+ const sharedData = path.join(projectStoreRoot(), "shared")
287
+ if (fs.existsSync(sharedData)) {
288
+ const sharedDataSnapshot = path.join(dataSnapshotRoot, "shared")
289
+ fs.cpSync(sharedData, sharedDataSnapshot, { recursive: true, dereference: true })
290
+ mounts.push({ type: "bind", source: sharedDataSnapshot, target: "/opt/headlesscode-data/shared", read_only: true })
291
+ filesystemPolicy.read_only.push("/opt/headlesscode-data/shared")
292
+ }
293
+ const checkpointsSnapshot = path.join(dataSnapshotRoot, "checkpoints")
294
+ fs.mkdirSync(checkpointsSnapshot, { recursive: true })
295
+ mounts.push({ type: "bind", source: checkpointsSnapshot, target: "/opt/headlesscode-data/checkpoints", read_only: false })
296
+ filesystemPolicy.read_only.push("/opt/headlesscode-data")
297
+ filesystemPolicy.read_write.push("/opt/headlesscode-data/checkpoints")
298
+ const memoryDir = request.env?.HEADLESSCODE_MEMORY_DIR
299
+ if (memoryDir) {
300
+ const requestedMemoryPath = path.resolve(memoryDir)
301
+ assertNoSymlinkAncestors(requestedMemoryPath)
302
+ const memoryHost = fs.realpathSync(requestedMemoryPath)
303
+ const commonGitDir = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: repo, encoding: "utf8" }).trim()
304
+ for (const protectedPath of [hostWorkspace, commonGitDir, isolated.tempRoot, this.scratchRoot]) {
305
+ if (pathsOverlap(memoryHost, path.resolve(protectedPath))) throw new Error(`OpenShell memory path overlaps protected session data: ${memoryHost}`)
306
+ }
307
+ const memorySnapshot = path.join(isolated.tempRoot, "memory")
308
+ fs.mkdirSync(memorySnapshot, { recursive: true })
309
+ copyMemorySnapshot(memoryHost, memorySnapshot)
310
+ isolated.memoryHost = memoryHost
311
+ isolated.memorySnapshot = memorySnapshot
312
+ mounts.push({ type: "bind", source: memorySnapshot, target: "/opt/headlesscode-memory", read_only: false })
313
+ filesystemPolicy.read_write.push("/opt/headlesscode-memory")
314
+ }
315
+ for (const mount of this.readOnlyMounts) {
316
+ const relativeTarget = path.relative(OPENSHELL_WORKSPACE_TARGET, path.resolve(mount.target))
317
+ if (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget)) throw new Error(`OpenShell read-only mount target must be under ${OPENSHELL_WORKSPACE_TARGET}: ${mount.target}`)
318
+ const target = path.join(sandboxWorkspace, relativeTarget)
319
+ fs.mkdirSync(path.dirname(target), { recursive: true })
320
+ fs.cpSync(path.resolve(mount.source), target, { recursive: true, dereference: true })
321
+ }
322
+ const generatedPolicy = path.join(os.tmpdir(), `headlesscode-openshell-policy-${process.pid}-${Date.now()}.yaml`)
323
+ fs.writeFileSync(generatedPolicy, stringify(policy), { mode: 0o600 })
324
+ args[args.indexOf("--policy") + 1] = generatedPolicy
325
+ args.push("--driver-config-json", JSON.stringify({ docker: { mounts } }))
326
+ for (const [key, value] of Object.entries(request.env ?? {})) {
327
+ if (key === "HEADLESSCODE_MEMORY_DIR" && isolated.memorySnapshot) args.push("--env", "HEADLESSCODE_MEMORY_DIR=/opt/headlesscode-memory")
328
+ else if (key !== "TARGET_REPO") args.push("--env", `${key}=${value}`)
329
+ }
330
+ args.push("--env", `TARGET_REPO=${OPENSHELL_WORKSPACE_TARGET}`, "--env", `HEADLESSCODE_ROOT=${OPENSHELL_HARNESS_ROOT}`, "--env", "HEADLESSCODE_DATA_DIR=/opt/headlesscode-data", "--env", `HEADLESSCODE_PROJECT_IDENTITY_ROOT=${repo}`, "--env", "GIT_CONFIG_NOSYSTEM=1", "--env", "GIT_CONFIG_GLOBAL=/dev/null")
331
+ args.push("--", "sleep", "infinity")
332
+ this.workspaces.set(name, isolated)
333
+ writeOpenShellSandboxMarker(hostWorkspace, name)
334
+ let result: CommandResult
335
+ try {
336
+ result = this.run(args)
337
+ } finally {
338
+ fs.rmSync(generatedPolicy, { force: true })
339
+ }
340
+ if (result.exitCode !== 0) {
341
+ // A non-zero create often means the sandbox was accepted but failed
342
+ // during provisioning. Clean up this request's deterministic name; if
343
+ // deletion is not confirmed, keep the host marker and scratch tree for
344
+ // the stop-worker recovery path.
345
+ try {
346
+ this.run(["sandbox", "delete", name])
347
+ const deadline = Date.now() + 30_000
348
+ while (this.listSandboxes().includes(name)) {
349
+ if (Date.now() >= deadline) break
350
+ await sleep(250)
351
+ }
352
+ if (!this.listSandboxes().includes(name)) {
353
+ this.workspaces.delete(name)
354
+ fs.rmSync(isolated.tempRoot, { recursive: true, force: true })
355
+ fs.rmSync(isolated.lockPath, { recursive: true, force: true })
356
+ fs.rmSync(path.join(hostWorkspace, ".harness.openshell-sandbox"), { force: true })
357
+ }
358
+ } catch {
359
+ // Preserve the marker and scratch data so operators can recover it.
360
+ }
361
+ throw new Error(`OpenShell sandbox create failed (exit ${result.exitCode}): ${result.output}`)
362
+ }
363
+ return { id: request.worktreeSpec.name, provider: this.name, address: hostWorkspace, containerId: name }
364
+ }
365
+
366
+ async waitReady(handle: SessionHandle): Promise<void> {
367
+ const result = this.run(["sandbox", "exec", "--name", this.sandboxName(handle), "--no-login-shell", "--", "true"])
368
+ if (result.exitCode !== 0) throw new Error(`OpenShell sandbox not ready: ${result.output}`)
369
+ }
370
+
371
+ async runHarness(handle: SessionHandle, cmd: string): Promise<CommandResult> {
372
+ const name = this.sandboxName(handle)
373
+ const state = this.workspaces.get(name)
374
+ if (!state) throw new Error(`OpenShell isolated workspace state is unavailable for ${name}`)
375
+ let mappedCommand = cmd.split(state.hostWorkspace).join(OPENSHELL_WORKSPACE_TARGET)
376
+ if (state.memoryHost) mappedCommand = mappedCommand.split(state.memoryHost).join("/opt/headlesscode-memory")
377
+ return this.run(["sandbox", "exec", "--name", name, "--workdir", OPENSHELL_WORKSPACE_TARGET, "--no-login-shell", "--", "/bin/bash", "-c", mappedCommand])
378
+ }
379
+
380
+ async collectResults(handle: SessionHandle): Promise<Record<string, unknown>> {
381
+ const root = handle.address
382
+ if (!root) throw new Error(`OpenShellSessionProvider: no workspace path on ${handle.id}`)
383
+ return {
384
+ exitCode: readInt(path.join(root, ".harness.exit")),
385
+ done: fs.existsSync(path.join(root, ".harness.done")),
386
+ summary: tail(path.join(root, "harness.log"), 40),
387
+ worktree: root,
388
+ }
389
+ }
390
+
391
+ async teardown(handle: SessionHandle): Promise<void> {
392
+ const name = this.sandboxName(handle)
393
+ if (!handle.address) throw new Error(`OpenShellSessionProvider: no host workspace path on ${handle.id}`)
394
+ const state = this.workspaces.get(name)
395
+ let exportResult: CommandResult | undefined
396
+ let exportError: Error | undefined
397
+ try {
398
+ if (state?.importResults) {
399
+ const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`
400
+ const branchRef = quote(state.branchRef)
401
+ const script = [
402
+ "for p in .headlesscode-session .headlesscode .harness.done ORCHESTRATOR_TASK.md ORCHESTRATOR_PLAN.md .qa-task.md harness.log qa.log review.log; do if [ -z \"$(git ls-files -- \"$p\" \"$p/**\")\" ]; then rm -rf -- \"/workspace/$p\"; fi; done",
403
+ "for f in /workspace/.harness.* /workspace/.qa.* /workspace/.review.*; do [ -e \"$f\" ] || continue; rel=\"${f#/workspace/}\"; [ -z \"$(git ls-files -- \"$rel\")\" ] && rm -f -- \"$f\"; done",
404
+ "git ls-files -z -- .env '.env.*' | while IFS= read -r -d '' rel; do git show \"HEAD:$rel\" > \"/workspace/$rel\"; done",
405
+ "for f in /workspace/.env /workspace/.env.*; do [ -e \"$f\" ] || continue; rel=\"${f#/workspace/}\"; git ls-files --error-unmatch -- \"$rel\" >/dev/null 2>&1 || rm -f -- \"$f\"; done",
406
+ "git add -A",
407
+ "if ! git diff --cached --quiet HEAD; then git -c core.hooksPath=/dev/null commit -m 'HeadlessCode sandbox result'; fi",
408
+ `git bundle create /result/result.bundle ${branchRef}`,
409
+ ].join(" && ")
410
+ exportResult = this.run(["sandbox", "exec", "--name", name, "--workdir", OPENSHELL_WORKSPACE_TARGET, "--no-login-shell", "--", "/bin/bash", "-c", script], 120_000)
411
+ if (exportResult.exitCode !== 0) exportError = new Error(`OpenShell Git export failed for ${name}: ${exportResult.output}`)
412
+ }
413
+ } catch (error) { exportError = error instanceof Error ? error : new Error(String(error)) }
414
+ let deleteError: Error | undefined
415
+ try {
416
+ const result = this.run(["sandbox", "delete", name], 30_000)
417
+ if (result.exitCode !== 0 && !/not found|does not exist/i.test(result.output)) deleteError = new Error(`OpenShell sandbox delete failed for ${name}: ${result.output}`)
418
+ } catch (error) { deleteError = error instanceof Error ? error : new Error(String(error)) }
419
+ const deadline = Date.now() + 30_000
420
+ try {
421
+ while (this.listSandboxes().includes(name)) {
422
+ if (Date.now() >= deadline) throw deleteError ?? new Error(`OpenShell sandbox ${name} remains after delete`)
423
+ await sleep(250)
424
+ }
425
+ } catch (error) {
426
+ // Keep the scratch tree and lock if teardown cannot confirm that the
427
+ // gateway released its bind mounts. stop-worker can retry deletion.
428
+ throw error
429
+ }
430
+ try {
431
+ if (deleteError) throw deleteError
432
+ if (state?.importResults) {
433
+ if (exportError) throw exportError
434
+ if (!exportResult || exportResult.exitCode !== 0) throw new Error(`OpenShell Git export failed for ${name}`)
435
+ this.importIsolatedGitResults(state)
436
+ }
437
+ if (state?.memorySnapshot && state.memoryHost) importMemorySnapshot(state.memorySnapshot, state.memoryHost)
438
+ } finally {
439
+ this.workspaces.delete(name)
440
+ if (state) fs.rmSync(state.tempRoot, { recursive: true, force: true })
441
+ if (state) fs.rmSync(state.lockPath, { recursive: true, force: true })
442
+ fs.rmSync(path.join(handle.address, ".harness.openshell-sandbox"), { force: true })
443
+ }
444
+ }
445
+
446
+ private importIsolatedGitResults(state: OpenShellWorkspaceState): void {
447
+ const bundlePath = path.join(state.exportDir, "result.bundle")
448
+ const bundleStat = fs.lstatSync(bundlePath)
449
+ if (!bundleStat.isFile() || bundleStat.isSymbolicLink() || bundleStat.size > 2 * 1024 * 1024 * 1024) {
450
+ throw new Error(`OpenShell Git export is not a regular bundle within the 2 GiB limit: ${bundlePath}`)
451
+ }
452
+ const hostHead = execFileSync("git", ["rev-parse", "HEAD"], { cwd: state.hostWorkspace, encoding: "utf8" }).trim()
453
+ if (hostHead !== state.baseCommit) throw new Error("OpenShell result import refused because the host worktree moved during the session")
454
+ const hostBranch = execFileSync("git", ["symbolic-ref", "HEAD"], { cwd: state.hostWorkspace, encoding: "utf8" }).trim()
455
+ if (hostBranch !== state.branchRef) throw new Error("OpenShell result import refused because the host worktree switched branches during the session")
456
+ const status = execFileSync("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: state.hostWorkspace, encoding: "utf8" }).trim()
457
+ if (status) throw new Error("OpenShell result import refused because the host worktree has tracked changes")
458
+ const heads = execFileSync("git", ["bundle", "list-heads", bundlePath], { cwd: state.hostWorkspace, encoding: "utf8" }).trim().split(/\r?\n/).filter(Boolean)
459
+ if (heads.length !== 1 || !heads[0].endsWith(` ${state.branchRef}`)) throw new Error("OpenShell Git bundle did not contain exactly the expected branch")
460
+ execFileSync("git", ["bundle", "verify", bundlePath], { cwd: state.hostWorkspace, stdio: "pipe", timeout: 30_000 })
461
+ const importRef = `refs/headlesscode/openshell-import/${createHash("sha256").update(bundlePath).digest("hex").slice(0, 24)}`
462
+ const env = { ...process.env }
463
+ for (const key of ["GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE", "GIT_NAMESPACE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_INDEX_FILE"]) delete env[key]
464
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "fetch", "--no-write-fetch-head", "--force", bundlePath, `${state.branchRef}:${importRef}`], { cwd: state.hostWorkspace, env, stdio: "pipe", timeout: 120_000 })
465
+ try {
466
+ execFileSync("git", ["merge-base", "--is-ancestor", state.baseCommit, importRef], { cwd: state.hostWorkspace, env, stdio: "pipe" })
467
+ validateImportedTree(state.hostWorkspace, state.baseCommit, importRef, env)
468
+ execFileSync("git", ["update-ref", state.branchRef, importRef, state.baseCommit], { cwd: state.hostWorkspace, env, stdio: "pipe" })
469
+ const filterConfigResult = spawnSync("git", ["config", "--get-regexp", "^filter\\..*\\.(smudge|process|required)$"], { cwd: state.hostWorkspace, env, encoding: "utf8" })
470
+ if (filterConfigResult.status !== 0 && filterConfigResult.status !== 1) throw new Error(`Could not inspect host Git filters: ${filterConfigResult.stderr}`)
471
+ const filterNames = [...new Set((filterConfigResult.stdout ?? "").split(/\r?\n/).map((line) => /^filter\.(.+)\.(?:smudge|process|required)\s/.exec(line)?.[1]).filter((value): value is string => !!value))]
472
+ const safeCheckoutArgs = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", ...filterNames.flatMap((name) => ["-c", `filter.${name}.smudge=`, "-c", `filter.${name}.process=`, "-c", `filter.${name}.required=false`]), "reset", "--hard", state.branchRef]
473
+ execFileSync("git", safeCheckoutArgs, { cwd: state.hostWorkspace, env: { ...env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }, stdio: "pipe", timeout: 120_000 })
474
+ } finally {
475
+ execFileSync("git", ["update-ref", "-d", importRef], { cwd: state.hostWorkspace, env, stdio: "pipe" })
476
+ }
477
+ }
478
+
479
+ }
480
+
481
+ function validateImportedTree(cwd: string, base: string, ref: string, env: NodeJS.ProcessEnv): void {
482
+ const paths = execFileSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", "-z", base, ref], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).split("\0").filter(Boolean)
483
+ const protectedPath = (entry: string): boolean => {
484
+ const root = entry.split("/", 1)[0]
485
+ return root === "ORCHESTRATOR_TASK.md"
486
+ || root === "harness.log"
487
+ || root === "qa.log"
488
+ || root === "review.log"
489
+ || root === ".headlesscode"
490
+ || root === ".headlesscode-session"
491
+ || root === ".env"
492
+ || root.startsWith(".env.")
493
+ || root.startsWith(".harness.")
494
+ || root.startsWith(".qa.")
495
+ || root.startsWith(".review.")
496
+ }
497
+ const violation = paths.find(protectedPath)
498
+ if (violation) throw new Error(`OpenShell result import refused protected orchestration or secret path: ${violation}`)
499
+ }
500
+
501
+ function readInt(file: string): number | undefined {
502
+ try { const value = Number(fs.readFileSync(file, "utf8").trim()); return Number.isFinite(value) ? value : undefined } catch { return undefined }
503
+ }
504
+
505
+ function tail(file: string, count: number): string {
506
+ try { return fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).slice(-count).join("\n") } catch { return "" }
507
+ }
508
+
509
+ function copyMemorySnapshot(source: string, target: string): void {
510
+ assertNoSymlinkAncestors(source)
511
+ for (const category of ["facts", "sessions"]) {
512
+ const sourceDir = path.join(source, category)
513
+ if (!fs.existsSync(sourceDir)) continue
514
+ const stat = fs.lstatSync(sourceDir)
515
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`OpenShell memory snapshot refused unexpected path ${sourceDir}`)
516
+ fs.mkdirSync(path.join(target, category), { recursive: true })
517
+ for (const name of fs.readdirSync(sourceDir)) {
518
+ if (!/^[A-Za-z0-9._-]+\.jsonl$/.test(name)) continue
519
+ const file = path.join(sourceDir, name)
520
+ const fileStat = fs.lstatSync(file)
521
+ if (!fileStat.isFile() || fileStat.isSymbolicLink() || fileStat.size > 16 * 1024 * 1024) throw new Error(`OpenShell memory snapshot refused unexpected file ${file}`)
522
+ fs.copyFileSync(file, path.join(target, category, name))
523
+ }
524
+ }
525
+ }
526
+
527
+ function importMemorySnapshot(snapshot: string, destination: string): void {
528
+ assertNoSymlinkAncestors(destination)
529
+ for (const category of ["facts", "sessions"]) {
530
+ const sourceDir = path.join(snapshot, category)
531
+ if (!fs.existsSync(sourceDir)) continue
532
+ const directoryStat = fs.lstatSync(sourceDir)
533
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error(`OpenShell memory import refused unexpected path ${sourceDir}`)
534
+ for (const name of fs.readdirSync(sourceDir)) {
535
+ if (!/^[A-Za-z0-9._-]+\.jsonl$/.test(name)) continue
536
+ const source = path.join(sourceDir, name)
537
+ const stat = fs.lstatSync(source)
538
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024 * 1024) throw new Error(`OpenShell memory import refused unexpected file ${source}`)
539
+ const target = path.join(destination, category, name)
540
+ assertNoSymlinkAncestors(path.dirname(target))
541
+ if (fs.existsSync(target)) {
542
+ const targetStat = fs.lstatSync(target)
543
+ if (!targetStat.isFile() || targetStat.isSymbolicLink()) throw new Error(`OpenShell memory import refused unexpected destination ${target}`)
544
+ }
545
+ const existing = new Set(fs.existsSync(target) ? fs.readFileSync(target, "utf8").split(/\r?\n/).filter(Boolean) : [])
546
+ const additions = fs.readFileSync(source, "utf8").split(/\r?\n/).filter((line) => {
547
+ if (!line) return false
548
+ try { const value = JSON.parse(line); return !!value && typeof value === "object" && !Array.isArray(value) && !existing.has(line) } catch { return false }
549
+ })
550
+ if (additions.length) {
551
+ fs.mkdirSync(path.dirname(target), { recursive: true })
552
+ fs.appendFileSync(target, `${additions.join("\n")}\n`, { mode: 0o600 })
553
+ }
554
+ }
555
+ }
556
+ }
557
+
558
+ function pathsOverlap(left: string, right: string): boolean {
559
+ return isSameOrInside(left, right) || isSameOrInside(right, left)
560
+ }
561
+
562
+ function isSameOrInside(parent: string, child: string): boolean {
563
+ const relative = path.relative(path.resolve(parent), path.resolve(child))
564
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))
565
+ }
566
+
567
+ function assertNoSymlinkAncestors(target: string): void {
568
+ const absolute = path.resolve(target)
569
+ let current = path.parse(absolute).root
570
+ for (const component of absolute.slice(current.length).split(path.sep).filter(Boolean)) {
571
+ current = path.join(current, component)
572
+ try {
573
+ const stat = fs.lstatSync(current)
574
+ if (stat.isSymbolicLink()) throw new Error(`OpenShell path refused symbolic link ${current}`)
575
+ } catch (error) {
576
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") break
577
+ throw error
578
+ }
579
+ }
580
+ }
581
+
582
+ function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)) }