fix: enforce remote Ask at tool boundary

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 仍在工具权限层仅允许读取操作。
This commit is contained in:
mesalogo
2026-09-03 16:23:14 +08:00
parent 987a08ad78
commit 6586192222
7 changed files with 71 additions and 367 deletions
+5 -5
View File
@@ -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 响应的模型桥回归。
+15 -19
View File
@@ -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;
@@ -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<DirectLinuxStdioSpawn>((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'
}
}
@@ -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,
@@ -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)
}
+5 -145
View File
@@ -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<Record<
(typeof OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES)[number],
string
>> = {
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 (
-1
View File
@@ -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