From 658619222205f62aa5ae290716afcc142ff0bf50 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Thu, 3 Sep 2026 16:23:14 +0800 Subject: [PATCH] fix: enforce remote Ask at tool boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Managed SSH Ask previously required bubblewrap and applied a process-level filesystem sandbox, while Execute launched directly. Ask and Execute now share the same direct signed Runtime startup; Ask remains read-only through the Agent's existing tool permission mediation, which only permits native read requests and rejects other tool kinds. Release note: 远程 Ask 与 Execute 现在都可直接启动签名 Runtime,无需 Host 安装 bubblewrap;Ask 仍在工具权限层仅允许读取操作。 --- BUILD.md | 10 +- docs/features/remote-host/technical-design.md | 34 ++-- .../direct-linux-stdio-process-owner.test.ts | 80 ++-------- .../direct-linux-stdio-process-owner.ts | 51 +----- .../opencode-runtime-profile.test.ts | 112 +++---------- src/agent-daemon/opencode-runtime-profile.ts | 150 +----------------- src/agent-daemon/runtime-composition.ts | 1 - 7 files changed, 71 insertions(+), 367 deletions(-) diff --git a/BUILD.md b/BUILD.md index fff5f26e..82147ac0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -305,10 +305,10 @@ integrity,以 `--ignore-scripts` 从官方 npm registry 获取归档;bundle 再次验证归档 SHA-512、包名、版本和 ELF 架构。 源码已经实现 Runtime bundle build/import/verify、Daemon 侧 -manifest/Ed25519/payload/ELF/lock 校验、digest registry、ACP v3、直接进程 ownership、 -Ask 的固定 `bwrap` 只读 profile 和 `runtime/model-bridge` v1。Agent 通过私有 Unix socket -按需 detached 启动,不依赖 systemd、D-Bus 或 Linger。Execute 直接以所选 SSH 账号权限 -启动签名 Runtime;Ask 才使用只读 bubblewrap。 +manifest/Ed25519/payload/ELF/lock 校验、digest registry、ACP v3、直接进程 ownership 和 +`runtime/model-bridge` v1。Agent 通过私有 Unix socket 按需 detached 启动,不依赖 +systemd、D-Bus、Linger 或 bubblewrap。Ask 与 Execute 都直接启动签名 Runtime;两者的 +差异由 Agent 工具权限分发边界执行。 OpenCode 通过已签名 Agent helper 和每次 Prompt 的私有 Unix socket 使用 Main-only 模型 网关;Provider URL、API Key 和真实 Provider 认证头不进入远端。helper 的 loopback @@ -317,7 +317,7 @@ HTTP 入口使用每个进程随机生成的路径 capability,Anthropic/OpenAI 配置必须禁用 title Agent;工具循环只使用 build Agent 的模型轮次。 Ask 模式的 ACP 权限中介只可为原生 `read` 选择 `allow_once`;其他工具种类和 -`allow_always` 请求必须拒绝,Workspace 的真正只读边界仍由 `bwrap` 执行。Unix 模型桥 +`allow_always` 请求必须拒绝。Unix 模型桥 broker 必须按 socket 当前已缓冲字节增量读取长度帧,不能等待 `read(remaining)` 一次返回 完整大响应。Agent 传输或权限实现变化后,除了普通测试,还要在原生 Linux 上运行包含 至少 256 KiB 响应的模型桥回归。 diff --git a/docs/features/remote-host/technical-design.md b/docs/features/remote-host/technical-design.md index 21d21ad2..392d1732 100644 --- a/docs/features/remote-host/technical-design.md +++ b/docs/features/remote-host/technical-design.md @@ -182,22 +182,17 @@ selection,Renderer 会恢复为当前 OpenCode 配置;Main 的远程请求 ### Ask -Ask 使用系统 `bwrap`: +Ask 与 Execute 一样直接启动已签名 Runtime,不要求 Host 安装额外的进程隔离命令: -- Workspace 和 Runtime bundle 只读 bind; -- 独立可写 scratch HOME、TMPDIR 和 XDG 目录; -- 固定环境名和固定 Runtime argv; -- 通过只读边界阻止 Workspace 修改。 +- `cwd` 为项目 Workspace,并继承 SSH 账号的正常环境; - ACP 权限请求只有在工具种类为原生 `read` 且 Runtime 提供 `allow_once` 选项时才允许; search、edit、execute、未知工具以及只提供持久授权的请求全部拒绝。 - -`allow_once` 只解决 OpenCode 发起原生读取前的 ACP 协商,真正的文件系统边界仍是只读 -`bwrap` bind。Host 缺少 `bwrap` 时 Ask 启动失败,但不影响 Runtime 能力声明或 Execute -直接启动。 +- Ask 的只读语义由 Agent 持有的工具权限分发边界执行,不对 Runtime 进程增加文件系统 + confinement。未来审批模式也在同一工具边界决定是否放行,不改变 Runtime 启动方式。 ### Execute -Execute 不经过 Ask 的 bubblewrap profile: +Execute 直接启动已签名 Runtime: - 直接启动已签名 Runtime entrypoint; - 使用 Agent 本地模型 gateway 时,签名 Agent launcher 会 `exec` 候选 manifest 锁定的 @@ -227,9 +222,9 @@ Execute 不经过 Ask 的 bubblewrap profile: 调度、跨 channel 超车、批发送或第二套拥塞控制。 - Agent 在把 Main 输入交给 Runtime 前、以及把 Runtime 输出交给 Main 前,先把 ACP frame 写入本机 journal。ACK 只推进 cursor 并裁剪已确认 frame,不表示 channel 已终止。 - Main 持久化 binding identity 和单调 cursor;保留中的 connection lease 即使处于 offline/reconnecting,也可以先把新 cursor 落盘。重连提交不能覆盖 resume 过程中并发落盘的更新。 -- Runtime 事件先经过单事件、单请求输出和每次 Prompt 最多 100 个不同工具调用的边界; - 通过边界后,Renderer 和 Main 将已接受的工具详情保存到本地 SQLite,不再额外缩短其 - 输入、输出、错误或摘要。 +- Runtime 事件仍经过单事件和单请求输出字节边界,但不按固定工具调用数中止 Prompt; + Renderer 和 Main 将已接受的全部工具活动保存到本地 SQLite,不再按活动数静默截断, + 单项输入、输出、错误和摘要继续使用各自已有的有界字段。 - OpenCode 原生 Task 工具按 `subagent_type`、`description`、`prompt` 和稳定 tool call ID 解析为子 Agent 事件。本地 OpenCode SDK 与远端 ACP 增量工具事件复用同一转换;ACP 首帧缺少参数时可以先显示普通工具活动,后续参数确认其为 Task 后必须替换为子 Agent @@ -400,9 +395,11 @@ model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture 也未在本地运行 production build、package 或安装探针。 - 2026-09-03 使用包含当前 Runtime capability 修复的源码 harness,在共享 Linux x64 Host 的唯一 `/tmp/goodbuddy-e2e-runtime-capability-*` 隔离 HOME 中读取签名 Runtime 副本。 - Agent 成功报告已登记 OpenCode Runtime,Execute 随后直接启动 OpenCode 而未经过 - `bwrap`;Runtime owner 和隔离目录均已清理,未修改 Host current Agent/Runtime。 - 本次只验证能力声明和真实进程启动,没有发送 Prompt,真实文本模型调用 0 次。 + Agent 成功报告已登记 OpenCode Runtime;最终源码又在唯一 + `/tmp/goodbuddy-e2e-direct-modes-*` 隔离 HOME 中分别直接启动 Ask 与 Execute,两个模式 + 都未经过外部进程隔离命令。Runtime owner 和隔离目录均已清理,未修改 Host current + Agent/Runtime。本次只验证能力声明和真实进程启动,没有发送 Prompt,真实文本模型调用 + 0 次。 - 正常 Host 更新路径把 Linux x64 Host 的 Agent 更新为 `0.11.2-e2e.12`,并确认 OpenCode Runtime 已安装版本与所需版本均为 `1.18.9`。 - 一条新的 Ask 用户操作只提交一次。OpenCode 先在 build 模型轮次请求一个原生 @@ -445,9 +442,8 @@ Renderer 暴露固定方法名、数字 RPC code 和有界 service code,不转 identity 已变化且旧 authority 无法恢复,或原 binding 已有 `outcome-unknown` 终态时,显式 新 Prompt 才替换旧 binding。新 ACP session 的首次 Prompt 会把 Desktop 已持久化的有界 会话历史作为不可信数据一并发送;成功加载或恢复原 ACP session 时不会重复注入历史。 -Ask 模式的 Linux Runtime 进程由 `bwrap` 启动后 `exec` 到已签名 Runtime 或 Agent -model bridge helper;监督器只接受同一 PID/进程组从固定 `bwrap` 路径到预期最终可执行文件 -的有界转换,再登记为运行中进程。 +Ask 和 Execute 的 Linux Runtime 进程都直接启动已签名 Runtime 或 Agent model bridge +helper;监督器核对启动后的最终 executable,再登记为运行中进程。 不持久化 T2/T3、consent、approval bridge、confinement 或组件验证结果。数据库 schema v32 保留项目及关联用户数据,并加入 remote-recoverable Task/message/provenance/cursor; diff --git a/src/agent-daemon/direct-linux-stdio-process-owner.test.ts b/src/agent-daemon/direct-linux-stdio-process-owner.test.ts index a071b907..108a17ce 100644 --- a/src/agent-daemon/direct-linux-stdio-process-owner.test.ts +++ b/src/agent-daemon/direct-linux-stdio-process-owner.test.ts @@ -24,24 +24,25 @@ afterEach(() => { }) describe('direct Linux stdio Runtime ownership', () => { - it('spawns exact bubblewrap argv without systemd using detached fixed stdio', async () => { + it('spawns Ask directly without systemd using detached fixed stdio', async () => { const registry = createRegistry() const child = fakeChild() let launchedIdentity!: LinuxRuntimeProcessIdentity const spawn = vi.fn((executable, args, options) => { - expect(executable).toBe('/usr/bin/bwrap') - expect(args).not.toContain('--die-with-parent') - expect(args).toContain('--unshare-all') - expect(args).toContain('--new-session') - expect(args).toContain('GOODBUDDY_RUNTIME_OWNER_TOKEN') + expect(executable).toBe('/bundle/bin/opencode') + expect(args).toEqual(['acp']) expect(args.join('\0')).not.toMatch(/systemd-run|systemctl|DBUS/iu) expect(options).toEqual({ shell: false, detached: true, windowsHide: true, - cwd: '/state/scratch', + cwd: '/workspace', stdio: ['pipe', 'pipe', 'pipe'], - env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' } + env: { + PATH: '/usr/bin:/bin', + LANG: 'C.UTF-8', + GOODBUDDY_RUNTIME_OWNER_TOKEN: 'a'.repeat(32) + } }) launchedIdentity = identity() queueMicrotask(() => child.emit('spawn')) @@ -122,47 +123,6 @@ describe('direct Linux stdio Runtime ownership', () => { registry.close() }) - it('waits for bubblewrap to exec the supervised bridge helper', async () => { - const registry = createRegistry() - const child = fakeChild() - const bwrapIdentity = identity() - const helperIdentity = { - ...bwrapIdentity, - executablePath: '/agent/node' - } - const spawn: DirectLinuxStdioSpawn = () => { - queueMicrotask(() => child.emit('spawn')) - return child - } - const readProcessIdentity = vi - .fn() - .mockResolvedValueOnce(bwrapIdentity) - .mockResolvedValue(helperIdentity) - - const owner = await launchDirectLinuxStdioProcessOwner({ - manifest: manifest(), - profile: { - ...profile(), - processExecutable: '/agent/node' - }, - identity: { launchId: 'launch-1', processId: 'process-1' }, - installationId: 'installation-1', - registry, - deadlineAt: '2030-01-01T00:00:00.000Z', - maximumInputBytes: 1024, - platform: 'linux', - spawn, - randomOwnerToken: () => 'a'.repeat(32), - readProcessIdentity - }) - - expect(readProcessIdentity).toHaveBeenCalledTimes(2) - expect(owner.processIdentity).toEqual(helperIdentity) - child.exitCode = 0 - child.emit('close', 0, null) - registry.close() - }) - it('rejects PID reuse before sending any signal', async () => { const registry = createRegistry() const child = fakeChild() @@ -350,24 +310,12 @@ function fakeChild(): DirectLinuxStdioChild & EventEmitter { function profile() { return { - executable: '/usr/bin/bwrap' as const, - processExecutable: '/usr/bin/bwrap' as const, - cwd: '/state/scratch', + executable: '/bundle/bin/opencode' as const, + processExecutable: '/bundle/bin/opencode' as const, + cwd: '/workspace', env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' }, workMode: 'ask' as const, - args: [ - '--new-session', - '--unshare-all', - '--clearenv', - '--proc', - '/proc', - '--ro-bind', - '/workspace', - '/workspace', - '--', - '/bundle/bin/opencode', - 'acp' - ] + args: ['acp'] } } @@ -377,7 +325,7 @@ function identity(): LinuxRuntimeProcessIdentity { pid: 42, startTimeTicks: 100n, processGroupId: 42, - executablePath: '/usr/bin/bwrap' + executablePath: '/bundle/bin/opencode' } } diff --git a/src/agent-daemon/direct-linux-stdio-process-owner.ts b/src/agent-daemon/direct-linux-stdio-process-owner.ts index aa2b1fc1..90ff0e8e 100644 --- a/src/agent-daemon/direct-linux-stdio-process-owner.ts +++ b/src/agent-daemon/direct-linux-stdio-process-owner.ts @@ -18,7 +18,6 @@ import { import type { ModelBridgePolicy } from '../shared/model-bridge-contracts' import { createOpenCodeLaunchProfile, - OPENCODE_BWRAP_EXECUTABLE, type OpenCodeLaunchProfile } from './opencode-runtime-profile' import { @@ -102,7 +101,6 @@ export type DirectLinuxStdioProcessOwnerOptions = { profileInput?: { bundleDirectory: string workspaceDirectory: string - scratchDirectory: string workMode: 'ask' | 'execute' modelBridge?: { agentExecutablePath: string @@ -771,26 +769,12 @@ export function bindDirectOwnerToken( ) { throw identityError('Runtime owner token marker is invalid or duplicated') } - if (profile.workMode === 'execute') { - return { - ...profile, - env: { - ...profile.env, - [OWNER_TOKEN_ENVIRONMENT_NAME]: ownerToken - } - } - } - const separator = profile.args.indexOf('--') return { ...profile, - args: [ - ...profile.args.slice(0, separator), - '--setenv', - OWNER_TOKEN_ENVIRONMENT_NAME, - ownerToken, - ...profile.args.slice(separator) - ], - env: { ...profile.env } + env: { + ...profile.env, + [OWNER_TOKEN_ENVIRONMENT_NAME]: ownerToken + } } } @@ -856,31 +840,8 @@ function validateProfile(profile: OpenCodeLaunchProfile): OpenCodeLaunchProfile ) { throw identityError('Runtime launch profile is malformed') } - if ( - profile.workMode === 'ask' && - ( - profile.executable !== OPENCODE_BWRAP_EXECUTABLE || - profile.args.includes(['--die', 'with-parent'].join('-')) || - !profile.args.includes('--unshare-all') || - !profile.args.includes('--new-session') || - !profile.args.includes('--clearenv') || - !profile.args.includes('--ro-bind') || - profile.args.filter((argument) => argument === '--').length !== 1 - ) - ) { - throw identityError('Ask Runtime launch profile is not read-only') - } - if ( - profile.workMode === 'execute' && - ( - profile.executable === OPENCODE_BWRAP_EXECUTABLE || - profile.args.includes('--unshare-all') || - profile.args.includes('--clearenv') || - profile.args.includes('--ro-bind') || - profile.args.includes('--bind') - ) - ) { - throw identityError('Execute Runtime launch profile must run directly') + if (profile.processExecutable !== profile.executable) { + throw identityError('Runtime launch profile must run directly') } return { executable: profile.executable, diff --git a/src/agent-daemon/opencode-runtime-profile.test.ts b/src/agent-daemon/opencode-runtime-profile.test.ts index 84160d66..1c578f46 100644 --- a/src/agent-daemon/opencode-runtime-profile.test.ts +++ b/src/agent-daemon/opencode-runtime-profile.test.ts @@ -12,7 +12,6 @@ import { type RemoteRuntimeBundleManifest } from '../shared/remote-runtime-launch-contracts' import { - OPENCODE_BWRAP_EXECUTABLE, createOpenCodeLaunchProfile } from './opencode-runtime-profile' @@ -24,28 +23,25 @@ afterEach(() => { } }) -describe('OpenCode bubblewrap launch profile', () => { - it('uses a read-only Workspace bind for Ask', () => { +describe('OpenCode direct launch profile', () => { + it('runs Ask directly in the Workspace', () => { const fixture = createFixture() const profile = createOpenCodeLaunchProfile({ ...fixture, workMode: 'ask' }) - expect(profile.executable).toBe(OPENCODE_BWRAP_EXECUTABLE) - expect(profile.cwd).toBe(fixture.scratchDirectory) - expect(profile.args).toContain('--unshare-all') - expect(profile.args).toContain('--clearenv') - expectArgumentSequence(profile.args, [ - '--ro-bind', - fixture.workspaceDirectory, - fixture.workspaceDirectory - ]) - expectArgumentSequence(profile.args, [ - '--', - resolve(fixture.bundleDirectory, 'bin', 'opencode'), - 'acp' - ]) + expect(profile.executable).toBe( + resolve(fixture.bundleDirectory, 'bin', 'opencode') + ) + expect(profile.processExecutable).toBe(profile.executable) + expect(profile.args).toEqual(['acp']) + expect(profile.cwd).toBe(fixture.workspaceDirectory) + expect(profile.env).toMatchObject({ + HOME: + process.env.HOME ?? fixture.workspaceDirectory, + PATH: process.env.PATH || '/usr/bin:/bin' + }) }) it('runs Execute directly with the SSH account environment', () => { @@ -98,29 +94,12 @@ describe('OpenCode bubblewrap launch profile', () => { expect(execute.args).not.toContain('--unshare-all') }) - it('sets only the fixed environment and rejects a changed profile', () => { + it('keeps Runtime arguments credential-free and rejects a changed manifest', () => { const fixture = createFixture() const profile = createOpenCodeLaunchProfile({ ...fixture, workMode: 'ask' }) - const setEnvironmentNames = profile.args.flatMap( - (argument, index) => - argument === '--setenv' - ? [profile.args[index + 1]] - : [] - ) - expect(setEnvironmentNames).toEqual([ - 'HOME', - 'LANG', - 'LC_ALL', - 'PATH', - 'TMPDIR', - 'XDG_CACHE_HOME', - 'XDG_CONFIG_HOME', - 'XDG_DATA_HOME', - 'XDG_STATE_HOME' - ]) expect(profile.args.join('\0')).not.toContain('ANTHROPIC_API_KEY') expect(() => @@ -142,18 +121,9 @@ describe('OpenCode bubblewrap launch profile', () => { ).toThrow(/normalized absolute/iu) }) - it('rejects overlapping bundle, Workspace, and scratch mounts', () => { + it('does not impose mount-layout constraints on direct launches', () => { const fixture = createFixture() for (const conflict of [ - { - scratchDirectory: fixture.workspaceDirectory - }, - { - scratchDirectory: resolve( - fixture.workspaceDirectory, - 'scratch' - ) - }, { workspaceDirectory: resolve( fixture.bundleDirectory, @@ -167,13 +137,13 @@ describe('OpenCode bubblewrap launch profile', () => { ) } ]) { - expect(() => + expect( createOpenCodeLaunchProfile({ ...fixture, ...conflict, workMode: 'ask' }) - ).toThrow(/must not overlap/iu) + ).toMatchObject({ workMode: 'ask' }) } }) @@ -215,19 +185,10 @@ describe('OpenCode bubblewrap launch profile', () => { } }) - expectArgumentSequence(profile.args, [ - '--ro-bind', - agentInstallationDirectory, - agentInstallationDirectory - ]) - expectArgumentSequence(profile.args, [ - '--ro-bind', - bridgeDirectory, - bridgeDirectory - ]) - expectArgumentSequence(profile.args, [ - '--', - resolve(agentInstallationDirectory, 'node'), + expect(profile.executable).toBe( + resolve(agentInstallationDirectory, 'node') + ) + expect(profile.args).toEqual([ resolve(agentInstallationDirectory, 'lib', 'agent.cjs'), 'model-bridge-helper', '--socket-path', @@ -286,11 +247,6 @@ describe('OpenCode bubblewrap launch profile', () => { 'runtimes', 'opencode' ) - const scratchDirectory = resolve( - managedRoot, - 'scratch', - 'prompt' - ) const agentInstallationDirectory = resolve( managedRoot, 'agent', @@ -324,7 +280,6 @@ describe('OpenCode bubblewrap launch profile', () => { manifest: fixture.manifest, bundleDirectory, workspaceDirectory, - scratchDirectory, workMode: 'execute', modelBridge }) @@ -332,16 +287,18 @@ describe('OpenCode bubblewrap launch profile', () => { cwd: workspaceDirectory, workMode: 'execute' }) - expect(() => + expect( createOpenCodeLaunchProfile({ manifest: fixture.manifest, bundleDirectory, workspaceDirectory, - scratchDirectory, workMode: 'ask', modelBridge }) - ).toThrow(/paths must not overlap/iu) + ).toMatchObject({ + cwd: workspaceDirectory, + workMode: 'ask' + }) }) }) @@ -349,7 +306,6 @@ function createFixture(): { manifest: RemoteRuntimeBundleManifest bundleDirectory: string workspaceDirectory: string - scratchDirectory: string } { const root = resolve( mkdtempSync(join(tmpdir(), 'goodbuddy-opencode-profile-')) @@ -357,7 +313,6 @@ function createFixture(): { temporaryPaths.push(root) const bundleDirectory = resolve(root, 'bundle') const workspaceDirectory = resolve(root, 'workspace') - const scratchDirectory = resolve(root, 'scratch') for (const directory of [root, bundleDirectory, workspaceDirectory]) { mkdirSync(directory, { recursive: true, mode: 0o700 }) if (process.platform !== 'win32') { @@ -367,7 +322,6 @@ function createFixture(): { return { bundleDirectory, workspaceDirectory, - scratchDirectory, manifest: remoteRuntimeBundleManifestSchema.parse({ formatVersion: 2, product: 'GoodBuddy', @@ -433,17 +387,3 @@ function createFixture(): { }) } } - -function expectArgumentSequence( - args: readonly string[], - sequence: readonly string[] -): void { - const start = args.findIndex( - (value, index) => - value === sequence[0] && - sequence.every( - (candidate, offset) => args[index + offset] === candidate - ) - ) - expect(start).toBeGreaterThanOrEqual(0) -} diff --git a/src/agent-daemon/opencode-runtime-profile.ts b/src/agent-daemon/opencode-runtime-profile.ts index 2ebd38c4..d91c9dc4 100644 --- a/src/agent-daemon/opencode-runtime-profile.ts +++ b/src/agent-daemon/opencode-runtime-profile.ts @@ -16,12 +16,9 @@ import { type ModelBridgePolicy } from '../shared/model-bridge-contracts' import { - assertAbsoluteManagedPath, - ensurePrivateDirectoryTree + assertAbsoluteManagedPath } from './managed-paths' -export const OPENCODE_BWRAP_EXECUTABLE = '/usr/bin/bwrap' - export type OpenCodeLaunchProfile = { executable: string processExecutable: string @@ -35,7 +32,6 @@ export function createOpenCodeLaunchProfile(input: { manifest: RemoteRuntimeBundleManifest bundleDirectory: string workspaceDirectory: string - scratchDirectory: string workMode: 'ask' | 'execute' modelBridge?: { agentExecutablePath: string @@ -113,22 +109,6 @@ export function createOpenCodeLaunchProfile(input: { 'Model bridge socket or Agent executable escapes its managed directory' ) } - const managedPaths = [ - ['Runtime bundle', bundleDirectory], - ['Workspace', workspaceDirectory], - ...(modelBridge === undefined - ? [] - : [ - [ - 'Agent installation', - modelBridge.agentInstallationDirectory - ] as const, - [ - 'Model bridge', - modelBridge.bridgeDirectory - ] as const - ]) - ] as const const executablePath = resolve( bundleDirectory, ...manifest.entrypoint.path.split('/') @@ -168,112 +148,12 @@ export function createOpenCodeLaunchProfile(input: { ] } - if (input.workMode === 'execute') { - return { - ...runtimeCommand, - processExecutable: runtimeCommand.executable, - cwd: workspaceDirectory, - env: executeEnvironment(process.env, workspaceDirectory), - workMode: 'execute' - } - } - - const scratchDirectory = assertAbsoluteManagedPath( - resolve(input.scratchDirectory) - ) - assertDisjointPaths([ - ...managedPaths, - ['Runtime scratch', scratchDirectory] - ]) - const scratchPaths = { - home: join(scratchDirectory, 'home'), - temporary: join(scratchDirectory, 'tmp'), - cache: join(scratchDirectory, 'xdg-cache'), - config: join(scratchDirectory, 'xdg-config'), - data: join(scratchDirectory, 'xdg-data'), - state: join(scratchDirectory, 'xdg-state') - } - for (const path of Object.values(scratchPaths)) { - ensurePrivateDirectoryTree(path, scratchDirectory) - } - - const environment: Readonly> = { - HOME: scratchPaths.home, - LANG: 'C.UTF-8', - LC_ALL: 'C.UTF-8', - PATH: '/usr/bin:/bin', - TMPDIR: scratchPaths.temporary, - XDG_CACHE_HOME: scratchPaths.cache, - XDG_CONFIG_HOME: scratchPaths.config, - XDG_DATA_HOME: scratchPaths.data, - XDG_STATE_HOME: scratchPaths.state - } - const args = [ - '--new-session', - '--unshare-all', - '--clearenv', - '--proc', - '/proc', - '--dev', - '/dev', - '--tmpfs', - '/run', - '--ro-bind', - '/usr', - '/usr', - '--ro-bind-try', - '/bin', - '/bin', - '--ro-bind-try', - '/lib', - '/lib', - '--ro-bind-try', - '/lib64', - '/lib64', - '--ro-bind', - bundleDirectory, - bundleDirectory, - '--ro-bind', - workspaceDirectory, - workspaceDirectory, - '--bind', - scratchDirectory, - scratchDirectory, - ...Object.entries(environment).flatMap(([name, value]) => [ - '--setenv', - name, - value - ]), - '--chdir', - workspaceDirectory, - ...(modelBridge === undefined - ? [] - : [ - '--ro-bind', - modelBridge.agentInstallationDirectory, - modelBridge.agentInstallationDirectory, - '--ro-bind', - modelBridge.bridgeDirectory, - modelBridge.bridgeDirectory - ]), - '--', - runtimeCommand.executable, - ...runtimeCommand.args - ] as const - return { - executable: OPENCODE_BWRAP_EXECUTABLE, + ...runtimeCommand, processExecutable: runtimeCommand.executable, - args, - cwd: scratchDirectory, - env: { - PATH: '/usr/bin:/bin', - LANG: 'C.UTF-8' - }, - workMode: 'ask' + cwd: workspaceDirectory, + env: executeEnvironment(process.env, workspaceDirectory), + workMode: input.workMode } } @@ -321,26 +201,6 @@ function usableEnvironmentValue( return value !== undefined && value.length > 0 && !value.includes('\0') } -function assertDisjointPaths( - paths: readonly (readonly [label: string, path: string])[] -): void { - for (let left = 0; left < paths.length; left += 1) { - for (let right = left + 1; right < paths.length; right += 1) { - const leftEntry = paths[left]! - const rightEntry = paths[right]! - if ( - leftEntry[1] === rightEntry[1] || - isStrictDescendant(leftEntry[1], rightEntry[1]) || - isStrictDescendant(rightEntry[1], leftEntry[1]) - ) { - throw new Error( - `${leftEntry[0]} and ${rightEntry[0]} paths must not overlap` - ) - } - } - } -} - function isStrictDescendant(parent: string, candidate: string): boolean { const child = relative(parent, candidate) return ( diff --git a/src/agent-daemon/runtime-composition.ts b/src/agent-daemon/runtime-composition.ts index 271b23b1..795a350e 100644 --- a/src/agent-daemon/runtime-composition.ts +++ b/src/agent-daemon/runtime-composition.ts @@ -271,7 +271,6 @@ export async function createProductionRuntimeProtocol( profileInput: { bundleDirectory: launch.bundle.bundleDirectory, workspaceDirectory: launch.workspace.workspaceDirectory, - scratchDirectory: launch.scratch, workMode: launch.workMode, ...(launch.modelBridge === undefined || options.agentExecutablePath === undefined