Skip to content

Commit 772fea7

Browse files
authored
fix: support --host-tmpdir flag (#379)
1 parent 6c99b5f commit 772fea7

6 files changed

Lines changed: 91 additions & 4 deletions

File tree

docs/opencode-docker.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ Provider access depends on 3 optional sources:
2020
2. passthrough env vars
2121
3. config referenced from `opencode.json`/`opencode.jsonc` placed in this repo root
2222

23+
## Running the runner inside Docker
24+
25+
When the runner itself runs in a container with access to the host Docker socket, solver/judge workspaces and auth isolation dirs must live on a host-visible path. The Docker daemon resolves bind-mount sources on the host, not inside the runner container.
26+
27+
Pass `--host-tmpdir` to a directory that exists on the host and is bind-mounted into the runner container at the **same path**:
28+
29+
```bash
30+
# on the host
31+
mkdir -p /var/tmp/evals-runner
32+
33+
docker run \
34+
-v /var/run/docker.sock:/var/run/docker.sock \
35+
-v /var/tmp/evals-runner:/var/tmp/evals-runner \
36+
... \
37+
bun runner/run.ts \
38+
--model openai/gpt-4.1-mini \
39+
--host-tmpdir /var/tmp/evals-runner \
40+
--output generated/my-generated
41+
```
42+
43+
When omitted, the runner uses the process temp directory (`os.tmpdir()`, typically `/tmp`). That works for local runs where the runner process and Docker daemon share the same filesystem namespace.
44+
45+
The runner creates per-session subdirectories under this root with prefixes `evals-opencode-` (workspaces) and `evals-opencode-home-` (isolated auth copies). They are removed when each session finishes.
46+
2347
## Environment variable passthrough
2448

2549
The runner forwards matching host environment variables into the container with `docker run -e`.
@@ -57,6 +81,10 @@ Extra prefixes are merged with the defaults above.
5781

5882
Both `bun runner/run.ts` and `bun runner/judge.ts` accept:
5983

84+
### `--host-tmpdir`
85+
86+
Optional host-visible temp root for solver/judge workspaces and isolated auth copies. Use when the runner runs inside a container with access to the host Docker socket. See [Running the runner inside Docker](#running-the-runner-inside-docker).
87+
6088
### `--agent-logs`
6189

6290
Streams OpenCode agent activity from the server SSE feed. Log lines are prefixed with `[opencode-docker][...][agent]` and include session lifecycle events, tool calls, and session errors.

runner/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function parseRunCliArgs(argv: string[] = Bun.argv.slice(2)) {
2929
'pattern': { type: 'string', default: 'evals/**/*' },
3030
'timeout': { type: 'string', default: '120000' },
3131
'port': { type: 'string' },
32+
'host-tmpdir': { type: 'string' },
3233
'output': { type: 'string' },
3334
},
3435
strict: true,
@@ -49,6 +50,7 @@ export function parseRunCliArgs(argv: string[] = Bun.argv.slice(2)) {
4950
pattern: values.pattern,
5051
timeout: parsePositiveInteger(values.timeout, '--timeout'),
5152
port: parsePort(values.port),
53+
hostTmpdir: values['host-tmpdir'],
5254
output: values.output,
5355
}
5456
}
@@ -72,6 +74,7 @@ export function parseJudgeCliArgs(argv: string[] = Bun.argv.slice(2)) {
7274
'rerun-requirements-file': { type: 'string' },
7375
'timeout': { type: 'string', default: '120000' },
7476
'port': { type: 'string' },
77+
'host-tmpdir': { type: 'string' },
7578
'input': { type: 'string' },
7679
'output': { type: 'string' },
7780
},
@@ -119,6 +122,7 @@ export function parseJudgeCliArgs(argv: string[] = Bun.argv.slice(2)) {
119122
rerunRequirementsFile: values['rerun-requirements-file'],
120123
timeout: parsePositiveInteger(values.timeout, '--timeout'),
121124
port: parsePort(values.port),
125+
hostTmpdir: values['host-tmpdir'],
122126
input: values.input,
123127
output: values.output,
124128
}

runner/judge.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { partitionEvalRuns } from './utils/eval-runs'
1515
import { loadFiles, sanitizeSegment } from './utils/fs'
1616
import {
1717
configureOpencodeDockerLogging,
18+
configureOpencodeHostTmpdir,
1819
prepareOpencodeDockerRuntime,
1920
runWithOpencodeWorkerContext,
2021
shutdownAllOpencodeDockerContainers,
@@ -366,6 +367,7 @@ export async function runJudgeEntry(argv: string[] = Bun.argv.slice(2)) {
366367
agentLogs: cliOptions.agentLogs,
367368
verbose: cliOptions.verbose,
368369
})
370+
configureOpencodeHostTmpdir({ hostTmpdir: cliOptions.hostTmpdir })
369371
const inputDirectory = path.resolve(process.cwd(), cliOptions.input)
370372
const outputDirectory = cliOptions.output ?? path.dirname(inputDirectory)
371373
const outputDirectories = await createRunOutputDirectories(outputDirectory)

runner/run.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './utils/fs'
2020
import {
2121
configureOpencodeDockerLogging,
22+
configureOpencodeHostTmpdir,
2223
prepareOpencodeDockerRuntime,
2324
runWithOpencodeWorkerContext,
2425
shutdownAllOpencodeDockerContainers,
@@ -71,6 +72,7 @@ export async function runGenerationEntry(argv: string[] = Bun.argv.slice(2)) {
7172
agentLogs: cliOptions.agentLogs,
7273
verbose: cliOptions.verbose,
7374
})
75+
configureOpencodeHostTmpdir({ hostTmpdir: cliOptions.hostTmpdir })
7476
const discoveredEvals = await discoverEvals(cliOptions.pattern)
7577
const runId = new Date().toISOString().replace(/[:.]/g, '-')
7678
const startedAt = new Date().toISOString()

runner/utils/opencode.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import { createServer } from 'node:net'
2-
import { access } from 'node:fs/promises'
2+
import { access, mkdtemp, rm } from 'node:fs/promises'
3+
import os from 'node:os'
4+
import path from 'node:path'
35
import { afterEach, describe, expect, test } from 'bun:test'
46

57
import {
68
allocateHostPort,
79
cleanupOpencodeTempDir,
10+
configureOpencodeHostTmpdir,
811
createOpencodeTempDir,
912
DEFAULT_OPENCODE_PORT,
1013
forceStopEvalsOpencodeContainersSync,
1114
formatContainerLogLine,
15+
getOpencodeTempRoot,
1216
shouldPassthroughOpencodeDockerEnvKey,
1317
} from './opencode'
1418

1519
describe('opencode temp workspace', () => {
1620
test('creates and cleans up a temp directory', async () => {
21+
configureOpencodeHostTmpdir({})
1722
const workspace = await createOpencodeTempDir()
1823

1924
expect(workspace.includes('evals-opencode-')).toBe(true)
@@ -23,6 +28,27 @@ describe('opencode temp workspace', () => {
2328

2429
await expect(access(workspace)).rejects.toThrow()
2530
})
31+
32+
test('uses --host-tmpdir when configured', async () => {
33+
const configuredRoot = await mkdtemp(
34+
path.join(os.tmpdir(), 'runner-opencode-host-tmp-')
35+
)
36+
configureOpencodeHostTmpdir({ hostTmpdir: configuredRoot })
37+
38+
try {
39+
const workspace = await createOpencodeTempDir()
40+
expect(workspace.startsWith(configuredRoot)).toBe(true)
41+
expect(getOpencodeTempRoot()).toBe(configuredRoot)
42+
await cleanupOpencodeTempDir(workspace)
43+
} finally {
44+
configureOpencodeHostTmpdir({})
45+
await rm(configuredRoot, { recursive: true, force: true })
46+
}
47+
})
48+
49+
afterEach(() => {
50+
configureOpencodeHostTmpdir({})
51+
})
2652
})
2753

2854
describe('opencode port allocation', () => {

runner/utils/opencode.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type OpencodeWorkerContext = {
2929

3030
const opencodeWorkerContext = new AsyncLocalStorage<OpencodeWorkerContext>()
3131
let agentActivityLoggingEnabled = false
32+
let configuredHostTmpdir: string | undefined
3233

3334
const OPENCODE_TEMP_PREFIX = 'evals-opencode-'
3435
const OPENCODE_HOME_PREFIX = 'evals-opencode-home-'
@@ -109,6 +110,12 @@ export function logOpencodeAgent(message: string) {
109110
console.log(`${formatOpencodeLogTag('auto')}[agent] ${message}`)
110111
}
111112

113+
export function configureOpencodeHostTmpdir(options: { hostTmpdir?: string }) {
114+
configuredHostTmpdir = options.hostTmpdir?.trim()
115+
? path.resolve(options.hostTmpdir.trim())
116+
: undefined
117+
}
118+
112119
export function configureOpencodeDockerLogging(options: {
113120
agentLogs?: boolean
114121
verbose?: boolean
@@ -467,8 +474,19 @@ function getDockerBuildContext() {
467474
return path.resolve(process.cwd(), 'runner/docker/opencode')
468475
}
469476

477+
export function getOpencodeTempRoot() {
478+
return configuredHostTmpdir ?? os.tmpdir()
479+
}
480+
481+
async function ensureOpencodeTempRoot() {
482+
const tempRoot = getOpencodeTempRoot()
483+
await mkdir(tempRoot, { recursive: true })
484+
return tempRoot
485+
}
486+
470487
export async function createOpencodeTempDir() {
471-
return mkdtemp(path.join(os.tmpdir(), OPENCODE_TEMP_PREFIX))
488+
const tempRoot = await ensureOpencodeTempRoot()
489+
return mkdtemp(path.join(tempRoot, OPENCODE_TEMP_PREFIX))
472490
}
473491

474492
export async function cleanupOpencodeTempDir(directory: string) {
@@ -693,9 +711,15 @@ export async function ensureOpencodeDockerImage(image = DEFAULT_DOCKER_IMAGE) {
693711
return ensurePromise
694712
}
695713

696-
export async function prepareOpencodeDockerRuntime(image = DEFAULT_DOCKER_IMAGE) {
714+
export async function prepareOpencodeDockerRuntime(
715+
image = DEFAULT_DOCKER_IMAGE
716+
) {
697717
ensureOpencodeDockerShutdownHandlers()
698718
logOpencode(`preparing docker runtime (image=${image})`, 'global')
719+
if (configuredHostTmpdir) {
720+
const tempRoot = await ensureOpencodeTempRoot()
721+
logOpencode(`host temp root: ${tempRoot}`, 'global')
722+
}
699723
await ensureOpencodeDockerImage(image)
700724
logOpencode(
701725
`docker runtime ready (serve log level=${agentActivityLoggingEnabled ? 'DEBUG' : (process.env.OPENCODE_SERVER_LOG_LEVEL ?? DEFAULT_OPENCODE_SERVER_LOG_LEVEL)}, stream container logs=${shouldStreamContainerLogs()}, agent logs=${agentActivityLoggingEnabled}, verbose=${isOpencodeVerboseLoggingEnabled()})`,
@@ -735,8 +759,9 @@ async function buildDockerVolumeArgs(hostWorkspace: string) {
735759
const authSource = path.join(homeDirectory, '.local/share/opencode/auth.json')
736760

737761
if (await pathExists(authSource)) {
762+
const tempRoot = await ensureOpencodeTempRoot()
738763
const runtimeDataDirectory = await mkdtemp(
739-
path.join(os.tmpdir(), OPENCODE_HOME_PREFIX)
764+
path.join(tempRoot, OPENCODE_HOME_PREFIX)
740765
)
741766
cleanupPaths.push(runtimeDataDirectory)
742767
await mkdir(runtimeDataDirectory, { recursive: true })

0 commit comments

Comments
 (0)