From 666d986c41c48dad75ed4e723998d1f97890638f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 17 Sep 2026 23:12:14 -0700 Subject: [PATCH] fix(drive): reject ambient fixture config --- .changeset/isolated-config-root.md | 5 +++++ packages/drive/src/instance/instance.ts | 23 +++++++++++++++++++-- packages/drive/test/cli/integration.test.ts | 11 ++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 .changeset/isolated-config-root.md diff --git a/.changeset/isolated-config-root.md b/.changeset/isolated-config-root.md new file mode 100644 index 0000000..7826418 --- /dev/null +++ b/.changeset/isolated-config-root.md @@ -0,0 +1,5 @@ +--- +"opencode-drive": patch +--- + +Reject artifact roots beneath ambient OpenCode configuration instead of silently contaminating isolated fixtures. diff --git a/packages/drive/src/instance/instance.ts b/packages/drive/src/instance/instance.ts index 4ee49ee..1d4503e 100644 --- a/packages/drive/src/instance/instance.ts +++ b/packages/drive/src/instance/instance.ts @@ -1,6 +1,6 @@ -import { mkdir } from "node:fs/promises" +import { mkdir, stat } from "node:fs/promises" import { tmpdir } from "node:os" -import { join, resolve } from "node:path" +import { dirname, join, resolve } from "node:path" import * as Effect from "effect/Effect" import { createScriptFileSystem } from "../script/filesystem.js" import { @@ -20,6 +20,11 @@ export function artifactDirectory() { } export async function initializeInstance(name?: string) { + const ambient = await ambientConfigSource(artifactDirectory()) + if (ambient) + throw new Error( + `Drive's isolated project would inherit ambient OpenCode configuration from ${ambient}; use a neutral TMPDIR`, + ) const artifacts = resolve( join(artifactDirectory(), `run-${crypto.randomUUID()}`), ) @@ -53,6 +58,20 @@ export async function initializeInstance(name?: string) { return artifacts } +async function ambientConfigSource(root: string) { + const names = [".opencode", ".claude", ".agents", "opencode.json", "opencode.jsonc"] + let directory = dirname(root) + while (true) { + for (const name of names) { + const source = join(directory, name) + if (await stat(source).then(() => true, () => false)) return source + } + const parent = dirname(directory) + if (parent === directory) return undefined + directory = parent + } +} + export const prepareInstanceProject = Effect.fn("OpenCodeInstance.prepareProject")(function* (options: { readonly artifacts: string readonly project?: Project diff --git a/packages/drive/test/cli/integration.test.ts b/packages/drive/test/cli/integration.test.ts index d31324a..3e11de0 100644 --- a/packages/drive/test/cli/integration.test.ts +++ b/packages/drive/test/cli/integration.test.ts @@ -154,6 +154,17 @@ describe("opencode-drive", () => { }) }) + test("rejects an artifact root beneath ambient OpenCode configuration", async () => { + const root = await temporary() + await mkdir(join(root, ".opencode", "skills", "ambient"), { recursive: true }) + const child = spawn(["init", "--name", "ambient-config"], root) + const [status, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]) + + expect(status).toBe(1) + expect(stderr).toContain(`would inherit ambient OpenCode configuration from ${join(root, ".opencode")}`) + expect(stderr).toContain("use a neutral TMPDIR") + }) + test("starts, drives, prints dir, restarts, and stops a named detached instance", async () => { const root = await temporary() const name = "detached-test"