1 Commits
Author SHA1 Message Date
jiang e476569ebc fix(agent): auto-approve scoped investigation edits
Generic Container CI/CD / test-build-publish (push) Successful in 1m19s
Agent CI/CD / build-test-publish-and-deploy (push) Successful in 1m19s
2026-11-08 02:02:58 +08:00
5 changed files with 19 additions and 15 deletions
+1 -1
View File
@@ -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。全局目录、其他会话结果和符号链接不会因此开放。
+2 -2
View File
@@ -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"));
}
+6 -6
View File
@@ -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) {
+1 -1
View File
@@ -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,
+9 -5
View File
@@ -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<Record<string, unknown>> = [];
const runtime = {
subscribeEvents: async () => createEventStream([
@@ -739,11 +740,14 @@ describe("background sandbox Shell approval", () => {
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
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"]);
});
});