diff --git a/plugins/dsh-mail-bridge/src/index.ts b/plugins/dsh-mail-bridge/src/index.ts index 27fea8f..ed58966 100644 --- a/plugins/dsh-mail-bridge/src/index.ts +++ b/plugins/dsh-mail-bridge/src/index.ts @@ -421,6 +421,9 @@ export function apply(ctx: any, config: PluginConfig): void { async function beat(): Promise { const body: Record = {}; + // DSH 有真沙箱(read-only / workspace-write / danger-full-access), + // 档位在这里是被强制执行的,不是 advisory。 + body.mode_enforcement = 'native'; const [entries, models] = await Promise.all([collectSessions(), collectModels()]); if (entries) { body.platform_sessions = snapshotDshSessions(entries, (id) => mailDrivenSessions.has(id)); @@ -649,7 +652,7 @@ export function apply(ctx: any, config: PluginConfig): void { const handle = await ctx.agents.resume({ resumeSessionId: sessionId as any, agentOptions, - setup: undefined, + setup: undefined, // resume 从磁盘恢复,工具已在 }); return { handle, resumed: true }; } @@ -658,14 +661,61 @@ export function apply(ctx: any, config: PluginConfig): void { sessionId, meta: { cwd }, agentOptions, - // setup 留空:DSH 的 base bundle 已经注册了 agent-loop、llm、tools 等服务。 - // 模型路由通过 agentOptions 传入即可 —— 挂载 preset 或 - // installModelSelection 反而会让 turn 崩溃(实测)。 - setup: undefined, + // 注册标准 preset 的工具(bash、fs、fs-search 等)。 + // dsh-a2a 用同一套 API(ctx.get('agentPresets') + presets.mount)并稳定运行。 + // 之前的崩溃来自尝试在 resume 路径上调用,create 路径经 a2a 实测可靠。 + setup: async (agentCtx: any) => { + try { + const presets = ctx.get('agentPresets'); + if (presets) { + const preset = await presets.resolve('standard'); + if (preset?.id) { + await presets.mount(agentCtx, preset.id); + } + } + } catch (e: any) { + console.error(`[dsh-mail-bridge] presets.mount 失败(工具注册降级): ${e?.message || e}`); + } + }, }); return { handle, resumed: false }; } + // ─── 权限档位 ─── + // + // DSH 的权限由两个独立旋钮控制: + // sandbox/mode → 控制文件系统和命令执行的边界 + // approval/policy → 控制是否需要人类审批 + // + // 三档映射(见 lib/permission-mode.js dshSandboxMode / dshApprovalPolicy): + // plan → read-only + ask(只读,遇到权限询问转邮件) + // workspace → workspace-write + ask(目录内可写,越界转邮件) + // full → danger-full-access + never(完全放开,不问人) + // + // 直接往 session 上 append 事件(与 permissionPresets.set 同一底层)。 + // 不走 permissionPresets 服务:它要求 preset 名在配置表里, + // 而我们的三档映射需要完全自主控制。 + function applyPermissionMode(session: any, mode: string): void { + if (!session?.append) return; + const m = (mode || 'workspace').trim(); + // sandbox mode + const sandboxMap: Record = { + plan: 'read-only', + workspace: 'workspace-write', + full: 'danger-full-access', + }; + const sandbox = sandboxMap[m] ?? 'workspace-write'; + session.append('sandbox/mode', { mode: sandbox }); + // approval policy + const approvalMap: Record = { + plan: 'ask', + workspace: 'ask', + full: 'never', + }; + const approval = approvalMap[m] ?? 'ask'; + session.append('approval/policy', { policy: approval }); + } + // ─── 投递邮件到 DSH 会话 ─── /** @@ -757,6 +807,7 @@ export function apply(ctx: any, config: PluginConfig): void { return { sessionID: adoptedID, reused: true }; } const { handle } = await startAgent(adoptedID, onDisk, attemptOrder()[0]); + applyPermissionMode(handle.agent?.session, data.permission_mode); bindAdopted(mailSessionID, adoptedID, onDisk, data); handle.agent.followup(userMessage(promptText)); await waitForTurnEnd(handle.agent); @@ -880,6 +931,9 @@ export function apply(ctx: any, config: PluginConfig): void { try { const started = await startAgent(attemptSessionId, cwd, route); handle = started.handle; + // 设定权限档位:sandbox/mode 决定文件与命令边界, + // approval/policy 决定越界时是否转邮件问人。 + applyPermissionMode(handle.agent?.session, data.permission_mode); } catch (e: any) { // create/resume 本身很少失败(create 不校验模型), // 但 cwd 不符、日志 replay 不过之类仍会抛