diff --git a/README.md b/README.md index ca5a021..fc2ae18 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ OpenCode 的 `location.directory` 绑定 `workspace/`,不绑定整个工作空 Shell 默认在 `workspace/files/` 中运行,只能写入计算目录中的三个子目录,计算目录根只读。`chat/` 只由 Agent 服务读写,目录权限为 `0700`、记录文件为 `0600`,模型文件工具及 Shell 不能访问。OpenCode 仅为受管理的计算目录增加文件权限,插件还验证目录属于当前原生会话;其他业务记录、聊天记录和 `logs/` 继续禁止访问。目录隔离配合 Landlock 和 seccomp 限制文件访问、网络与进程行为;目录本身不等于 OS 沙箱。限制覆盖 Shell 及其子进程,OpenCode 服务和类型化业务工具仍由可信宿主进程运行。本地 stdio MCP 也不在这个沙箱中。 -Shell 和文件修改仍经过原生权限审批。宿主确认的后台 `task_execution` 会话对沙箱 Shell 的待审批请求自动回复 `once`,避免无人值守任务等待交互确认;不自动批准原生文件修改或业务写工具,也不覆盖明确拒绝与沙箱限制。聊天选择“请求批准”(`approval_mode=request`)时逐次询问;选择“始终允许”(`approval_mode=always`)后,Agent 自动批准本轮进入审批流程的请求,包括 Shell、文件修改和 MCP 请求。每次仅回复原生 `once`,不把本轮选择保存为后续轮次授权;明确拒绝的操作不会进入自动批准,业务权限、MCP 会话策略和沙箱限制继续执行。只读白名单工具可直接执行,不因选择“请求批准”而增加询问。 +Shell 和文件修改仍经过原生权限审批。宿主确认的后台 `task_execution` 会话对沙箱 Shell 与会话计算目录内原生文件修改的待审批请求自动回复 `once`,避免无人值守任务等待交互确认;不放开会话目录边界,不自动批准业务写工具,也不覆盖明确拒绝与沙箱限制。聊天选择“请求批准”(`approval_mode=request`)时逐次询问;选择“始终允许”(`approval_mode=always`)后,Agent 自动批准本轮进入审批流程的请求,包括 Shell、文件修改和 MCP 请求。每次仅回复原生 `once`,不把本轮选择保存为后续轮次授权;明确拒绝的操作不会进入自动批准,业务权限、MCP 会话策略和沙箱限制继续执行。只读白名单工具可直接执行,不因选择“请求批准”而增加询问。 OpenCode 原生大结果仍保存在其全局工具输出目录。Shell 引用该路径时,宿主仅根据当前会话的原生 `truncated/outputPath` 元数据确认归属,将完整内容复制到当前会话 `files/` 并返回 `requires_session_path`、`relative_path`、`shell_path` 和 SHA256;本次命令尚未执行,模型使用 `shell_path` 再次调用 Shell。全局目录、其他会话结果和符号链接不会因此开放。 diff --git a/src/routes/chatPermissionPolicy.ts b/src/routes/chatPermissionPolicy.ts index 67b3079..72f2193 100644 --- a/src/routes/chatPermissionPolicy.ts +++ b/src/routes/chatPermissionPolicy.ts @@ -1,5 +1,5 @@ // Called only for native "asked" events. Explicit denials and host isolation // still run before this point; the selected round mode answers pending requests. -export function canAutomaticallyApprove(mode: string, action: string, backgroundSandboxShell = false) { - return mode === "always" || (backgroundSandboxShell && action === "shell"); +export function canAutomaticallyApprove(mode: string, action: string, backgroundSandboxPermissions = false) { + return mode === "always" || (backgroundSandboxPermissions && (action === "shell" || action === "edit")); } diff --git a/src/routes/chatStream.ts b/src/routes/chatStream.ts index 5a92da1..3dacc1d 100644 --- a/src/routes/chatStream.ts +++ b/src/routes/chatStream.ts @@ -84,7 +84,7 @@ type StreamPromptOptions = { message: string; model?: SupportedModel; approvalMode?: ApprovalMode; - backgroundSandboxShell?: boolean; + backgroundSandboxPermissions?: boolean; traceId?: string; projectId?: string; signal?: AbortSignal; @@ -149,7 +149,7 @@ export const streamPromptResponse = async ({ message, model, approvalMode = "request", - backgroundSandboxShell = false, + backgroundSandboxPermissions = false, traceId, projectId, signal, @@ -483,7 +483,7 @@ export const streamPromptResponse = async ({ patterns: event.properties.patterns, elapsedMs: Math.max(0, Date.now() - requestStartedAt), }); - const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.permission, backgroundSandboxShell); + const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.permission, backgroundSandboxPermissions); emitProgress({ id: `permission-${event.properties.id}`, phase: "permission", @@ -493,7 +493,7 @@ export const streamPromptResponse = async ({ autoApprove ? approvalMode === "always" ? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。" - : "后台任务的沙箱 Shell 已自动批准;沙箱与明确拒绝规则仍生效。" + : "后台任务的会话内 Shell / 文件修改已自动批准;目录边界与明确拒绝规则仍生效。" : buildPermissionDetail(event), }); if (autoApprove) { @@ -533,7 +533,7 @@ export const streamPromptResponse = async ({ resources: event.properties.resources, elapsedMs: Math.max(0, Date.now() - requestStartedAt), }); - const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.action, backgroundSandboxShell); + const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.action, backgroundSandboxPermissions); emitProgress({ id: `permission-${event.properties.id}`, phase: "permission", @@ -543,7 +543,7 @@ export const streamPromptResponse = async ({ autoApprove ? approvalMode === "always" ? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。" - : "后台任务的沙箱 Shell 已自动批准;沙箱与明确拒绝规则仍生效。" + : "后台任务的会话内 Shell / 文件修改已自动批准;目录边界与明确拒绝规则仍生效。" : buildPermissionV2Detail(event), }); if (autoApprove) { diff --git a/src/routes/chatStreamRoute.ts b/src/routes/chatStreamRoute.ts index be59ca7..546d1b5 100644 --- a/src/routes/chatStreamRoute.ts +++ b/src/routes/chatStreamRoute.ts @@ -997,7 +997,7 @@ export const registerChatStreamRoute = ( message: preparedMessage, model: parsed.data.model, approvalMode: parsed.data.approval_mode, - backgroundSandboxShell: sessionPurpose === "task_execution", + backgroundSandboxPermissions: sessionPurpose === "task_execution", traceId: requestContext.traceId, projectId: requestContext.projectId, signal: abortController.signal, diff --git a/tests/routes/chatStream.test.ts b/tests/routes/chatStream.test.ts index e8bb95d..ea49933 100644 --- a/tests/routes/chatStream.test.ts +++ b/tests/routes/chatStream.test.ts @@ -716,8 +716,9 @@ describe("streamPromptResponse", () => { }); -describe("background sandbox Shell approval", () => { - it.each(["permission.asked", "permission.v2.asked"])("only auto-approves sandbox Shell for %s", async (type) => { +describe("background sandbox permissions", () => { + it.each([["permission.asked", true], ["permission.v2.asked", true], + ["permission.asked", false], ["permission.v2.asked", false]] as const)("limits session Shell/edit approval for %s background=%s", async (type, background) => { const replies: Array> = []; const runtime = { subscribeEvents: async () => createEventStream([ @@ -739,11 +740,14 @@ describe("background sandbox Shell approval", () => { const events: Array<{ event: string; data: Record }> = []; await streamPromptResponse({ runtime, sessionId: "runtime-session-1", clientSessionId: "client-session-1", - message: "后台调查", approvalMode: "request", backgroundSandboxShell: true, + message: "后台调查", approvalMode: "request", backgroundSandboxPermissions: background, write: (event, data) => events.push({ event, data }), }); - expect(replies).toEqual([{ requestId: "shell", sessionId: "runtime-session-1", reply: "once" }]); + expect(replies).toEqual(background ? [ + { requestId: "shell", sessionId: "runtime-session-1", reply: "once" }, + { requestId: "edit", sessionId: "runtime-session-1", reply: "once" }, + ] : []); expect(events.filter(({ event }) => event === "permission_request") - .map(({ data }) => data.permission)).toEqual(["edit", "mcp_fixture_write"]); + .map(({ data }) => data.permission)).toEqual(background ? ["mcp_fixture_write"] : ["shell", "edit", "mcp_fixture_write"]); }); });