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 默认在 `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。全局目录、其他会话结果和符号链接不会因此开放。
|
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
|
// Called only for native "asked" events. Explicit denials and host isolation
|
||||||
// still run before this point; the selected round mode answers pending requests.
|
// still run before this point; the selected round mode answers pending requests.
|
||||||
export function canAutomaticallyApprove(mode: string, action: string, backgroundSandboxShell = false) {
|
export function canAutomaticallyApprove(mode: string, action: string, backgroundSandboxPermissions = false) {
|
||||||
return mode === "always" || (backgroundSandboxShell && action === "shell");
|
return mode === "always" || (backgroundSandboxPermissions && (action === "shell" || action === "edit"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ type StreamPromptOptions = {
|
|||||||
message: string;
|
message: string;
|
||||||
model?: SupportedModel;
|
model?: SupportedModel;
|
||||||
approvalMode?: ApprovalMode;
|
approvalMode?: ApprovalMode;
|
||||||
backgroundSandboxShell?: boolean;
|
backgroundSandboxPermissions?: boolean;
|
||||||
traceId?: string;
|
traceId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
@@ -149,7 +149,7 @@ export const streamPromptResponse = async ({
|
|||||||
message,
|
message,
|
||||||
model,
|
model,
|
||||||
approvalMode = "request",
|
approvalMode = "request",
|
||||||
backgroundSandboxShell = false,
|
backgroundSandboxPermissions = false,
|
||||||
traceId,
|
traceId,
|
||||||
projectId,
|
projectId,
|
||||||
signal,
|
signal,
|
||||||
@@ -483,7 +483,7 @@ export const streamPromptResponse = async ({
|
|||||||
patterns: event.properties.patterns,
|
patterns: event.properties.patterns,
|
||||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
});
|
});
|
||||||
const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.permission, backgroundSandboxShell);
|
const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.permission, backgroundSandboxPermissions);
|
||||||
emitProgress({
|
emitProgress({
|
||||||
id: `permission-${event.properties.id}`,
|
id: `permission-${event.properties.id}`,
|
||||||
phase: "permission",
|
phase: "permission",
|
||||||
@@ -493,7 +493,7 @@ export const streamPromptResponse = async ({
|
|||||||
autoApprove
|
autoApprove
|
||||||
? approvalMode === "always"
|
? approvalMode === "always"
|
||||||
? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。"
|
? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。"
|
||||||
: "后台任务的沙箱 Shell 已自动批准;沙箱与明确拒绝规则仍生效。"
|
: "后台任务的会话内 Shell / 文件修改已自动批准;目录边界与明确拒绝规则仍生效。"
|
||||||
: buildPermissionDetail(event),
|
: buildPermissionDetail(event),
|
||||||
});
|
});
|
||||||
if (autoApprove) {
|
if (autoApprove) {
|
||||||
@@ -533,7 +533,7 @@ export const streamPromptResponse = async ({
|
|||||||
resources: event.properties.resources,
|
resources: event.properties.resources,
|
||||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
});
|
});
|
||||||
const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.action, backgroundSandboxShell);
|
const autoApprove = canAutomaticallyApprove(approvalMode, event.properties.action, backgroundSandboxPermissions);
|
||||||
emitProgress({
|
emitProgress({
|
||||||
id: `permission-${event.properties.id}`,
|
id: `permission-${event.properties.id}`,
|
||||||
phase: "permission",
|
phase: "permission",
|
||||||
@@ -543,7 +543,7 @@ export const streamPromptResponse = async ({
|
|||||||
autoApprove
|
autoApprove
|
||||||
? approvalMode === "always"
|
? approvalMode === "always"
|
||||||
? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。"
|
? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。"
|
||||||
: "后台任务的沙箱 Shell 已自动批准;沙箱与明确拒绝规则仍生效。"
|
: "后台任务的会话内 Shell / 文件修改已自动批准;目录边界与明确拒绝规则仍生效。"
|
||||||
: buildPermissionV2Detail(event),
|
: buildPermissionV2Detail(event),
|
||||||
});
|
});
|
||||||
if (autoApprove) {
|
if (autoApprove) {
|
||||||
|
|||||||
@@ -997,7 +997,7 @@ export const registerChatStreamRoute = (
|
|||||||
message: preparedMessage,
|
message: preparedMessage,
|
||||||
model: parsed.data.model,
|
model: parsed.data.model,
|
||||||
approvalMode: parsed.data.approval_mode,
|
approvalMode: parsed.data.approval_mode,
|
||||||
backgroundSandboxShell: sessionPurpose === "task_execution",
|
backgroundSandboxPermissions: sessionPurpose === "task_execution",
|
||||||
traceId: requestContext.traceId,
|
traceId: requestContext.traceId,
|
||||||
projectId: requestContext.projectId,
|
projectId: requestContext.projectId,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
|
|||||||
@@ -716,8 +716,9 @@ describe("streamPromptResponse", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("background sandbox Shell approval", () => {
|
describe("background sandbox permissions", () => {
|
||||||
it.each(["permission.asked", "permission.v2.asked"])("only auto-approves sandbox Shell for %s", async (type) => {
|
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 replies: Array<Record<string, unknown>> = [];
|
||||||
const runtime = {
|
const runtime = {
|
||||||
subscribeEvents: async () => createEventStream([
|
subscribeEvents: async () => createEventStream([
|
||||||
@@ -739,11 +740,14 @@ describe("background sandbox Shell approval", () => {
|
|||||||
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||||
await streamPromptResponse({
|
await streamPromptResponse({
|
||||||
runtime, sessionId: "runtime-session-1", clientSessionId: "client-session-1",
|
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 }),
|
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")
|
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