fix(agent): auto-approve scoped investigation edits
This commit is contained in:
@@ -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。全局目录、其他会话结果和符号链接不会因此开放。
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user