dsh: 权限档位接线 — presets.mount 注册工具 + 三档 sandbox/approval 映射 + 心跳报 native

L3 dsh 适配:
- startAgent 新建会话时用 agentPresets.mount('standard') 注册 bash/fs/fs-search 等工具
  (与 dsh-a2a 同一套 API,经 a2a server 实测可靠)
- 新增 applyPermissionMode(session, mode):
  plan     → read-only + ask(只读,越界转邮件)
  workspace → workspace-write + ask(目录内可写)
  full     → danger-full-access + never(完全放开)
  直接 session.append sandbox/mode + approval/policy 事件(与 permissionPresets.set 同底层)
- deliverMail 两条投递路径(新建 + 接管)加 applyPermissionMode 调用
- 已有 session 路径不动:档位在首次投递时已设,后续邮件不改
- 心跳上报 mode_enforcement: 'native'(dsh 有真沙箱,不是 advisory)
- 323 测试全过
This commit is contained in:
2026-09-06 19:10:35 +08:00
parent 16df8f44c9
commit af61a37d9a

View File

@ -421,6 +421,9 @@ export function apply(ctx: any, config: PluginConfig): void {
async function beat(): Promise<void> {
const body: Record<string, unknown> = {};
// 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 用同一套 APIctx.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<string, string> = {
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<string, string> = {
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 不过之类仍会抛