Skip to content

improve sanbox、tool approval and hook module #19

@phantom5099

Description

@phantom5099

状态: Draft
日期: 2026-05-19
组件: Sandbox、Approval、Hook、ToolExecutor、Layer


1. 摘要

当前项目的沙箱模块仅有字符串黑名单检查(string.includes()),审批模块不存在,钩子系统 8 个点只接入了 3 个且纯观察性无决策能力。ToolExecutor 是普通 class + 构造函数注入,脱离 Effect 体系,导致 HookService 不得不暴露 emitSync 这种"后门"方法。

本 RFC 实施三个模块的改造:沙箱接入 @vscode/sandbox-runtime 实现 OS 级隔离,新建六层审批流水线(规则引擎→只读白名单→权限模式→Hook PreToolUse→用户确认→审计日志),钩子系统补全接入点并增加决策返回值,ToolExecutor 改造为 Effect Service 消除手动依赖注入


2. 用户场景

2.1 高危命令被拦截

用户输入:"帮我清理一下临时文件"。模型生成 rm -rf /tmp/some-project/*。某个 prompt injection 让它变成了 rm -rf /

  • 当前:sandbox/index.ts 检查 command.includes('rm -rf /') → 换个写法 rm -rf /home/./user/../ 就绕过了
  • 改造后:规则引擎(Layer 1)内置 deny-rm-rf-root 规则匹配 / * → 直接拒绝,不弹确认。即使规则被绕过,bubblewrap 的 mount namespace 只暴露了项目目录,/ 根本不可见

2.2 只读分析任务不需要确认

用户输入:"帮我分析这个函数的性能瓶颈"。模型使用 Read、Grep、Glob 读取代码后输出分析报告。

  • 当前:所有工具走同一逻辑,没有"只读自动通过"的路径
  • 改造后:只读白名单(Layer 2)自动通过 Read/Glob/Grep/LSP,零次弹窗

2.3 钩子拦截并修改工具参数

用户配置了 PreToolUse hook:所有 npm install 命令自动追加 --registry https://internal-mirror.com

  • 当前:hook handler 返回 void,无法修改参数
  • 改造后:PreToolUse handler 返回 { modifiedInput: { command: 'npm install --registry ...' } },流水线使用修改后的参数继续执行

3. 背景与问题

3.1 当前安全架构

模型输出 tool_call → ToolExecutor.execute() → sandbox.allowTool() → 执行
                                                        │
                                                返回 boolean,纯应用层字符串匹配

整个安全链路只有一个点。

3.2 沙箱的致命缺陷

当前 sandbox/index.ts 的实现:

allowCommand(command: string): boolean {
  for (const pattern of this.denyCommands) {
    if (command.includes(pattern)) return false;  // 一行就能绕过
  }
  return true;
}

攻击面:路径规范化绕过、参数顺序变换、管道拆分、alias 替换。

3.3 审批模块不存在

缺失的:

  • 没有 deny/allow/ask 三级决策
  • 没有规则引擎(glob/regex 模式匹配)
  • 没有权限模式(只读/默认/跳过)
  • 没有用户确认弹窗
  • 没有"Always/Never"学习机制

3.4 钩子系统的三个问题

问题 详情
8 个点只接入了 3 个 tool.execute.* 三个点接入;llm.request.*session.save.* 有定义无调用
纯观察性,不能决策 HookHandler = (payload) => void,无法返回 deny/allow/modifiedInput
无 PreToolUse 钩子 当前只有 tool.execute.before(通知性质),没有审批决策钩子

3.5 ToolExecutor 脱离 Effect 体系

当前 tools/executor.ts 是普通 class + 构造函数注入:

export class ToolExecutor {
  constructor(
    private registry: ToolService,   // Effect Service
    private hooks: HookService,      // Effect Service
    private sandbox: Sandbox,        // 普通 interface → new DefaultSandbox()
  ) {}
}

连锁问题:

  1. HookService 被迫开"后门"hooks/registry.ts:32-37emitSync 注释写明 "for callers outside Effect context (ToolExecutor)"。这是被 ToolExecutor 的 class 形态逼出来的妥协。

  2. cli.ts:86-87 手动 new 对象new DefaultSandbox()new ToolExecutor(tools, hooks, sandbox) 完全绕过 Layer 体系。每加一个 Service 依赖都需改 cli.ts 和构造函数签名。

  3. server/index.ts:6-8 全部 any 类型ServerDepsllmexecutorhooks 都是 any,通过 Hono context 传递,零类型安全。

根本原因:Sandbox 是普通 interface → ToolExecutor 无法成为 Effect Service → 依赖链污染。解法:Sandbox 和 ToolExecutor 都改为 Effect Service。


4. 设计目标

  1. 沙箱接入 @vscode/sandbox-runtime:替换字符串黑名单,实现 OS 级文件系统和网络隔离
  2. 新建审批模块:六层流水线(规则引擎→只读白名单→权限模式→Hook PreToolUse→用户确认→审计日志),权限模式(default/acceptEdits/dontAsk/plan/bypass)
  3. 补全钩子系统:接入 5 个缺失的钩子点,增加 PreToolUse/PostToolUse 决策钩子,handler 可返回决策,删除 emitSync
  4. ToolExecutor 改造为 Effect Service:消除手动依赖注入和 emitSync

5. 非目标

  1. 不实现 ML 分类器 —— 需要额外 LLM 调用成本
  2. 不实现子 Agent 委派工具 —— 审批模块就绪后再做
  3. 不保留旧 sandbox 的 allowCommand/allowPath API 兼容 —— 直接替换
  4. 不保留 ToolExecutor 的 class 形式 —— 直接改为 Effect Service
  5. 不保留 HookService.emitSync —— 删除

6. 核心设计

6.1 Approval Pipeline(六层)

ToolCallRequest
      │
      ▼
┌─────────────────────────────┐
│ Layer 1: Rule Engine        │  ← 系统硬规则优先(deny rm -rf /、sudo 等),不可绕过
└─────────────────────────────┘
      │ (无匹配规则)
      ▼
┌─────────────────────────────┐
│ Layer 2: Read-only Whitelist│  ← Read/Glob/Grep/LSP 自动 allow,零开销
└─────────────────────────────┘
      │ (写操作工具)
      ▼
┌─────────────────────────────┐
│ Layer 3: Permission Mode    │  ← plan=仅读, bypass=全过, acceptEdits=编辑自动过
└─────────────────────────────┘   (全局短路策略,影响后续所有层)
      │ (default 模式)
      ▼
┌─────────────────────────────┐
│ Layer 4: Hook PreToolUse    │  ← 用户自定义策略,可 deny/allow/ask/modifyInput
└─────────────────────────────┘   系统已处理完明确的 deny/allow,钩子处理剩余
      │ (无决策 或 ask)
      ▼
┌─────────────────────────────┐
│ Layer 5: User Confirmation  │  ← Y/N/A/V 交互弹窗 + 超时 60s → deny
└─────────────────────────────┘
      │
      ▼
┌─────────────────────────────┐
│ Layer 6: Audit / Log        │  ← 无论前面哪层做的决策,最终都经过此层记录审计日志
└─────────────────────────────┘

为什么 Hook 是 Layer 4 而不是 Layer 1?

系统硬规则(deny rm -rf /)和安全白名单(read-only 工具)是全局安全底线,不应被用户钩子覆盖。如果 Hook 放在第一层,钩子返回 allow 后又走系统校验——语义矛盾。系统先过滤掉明确的 deny 和 allow,Hook 仅处理剩余的不确定情况,职责更清晰。

为什么 Layer 6 是审计日志而不是 Deny Fallback?

"超时/未确认→拒绝"是 User Confirmation 层的超时行为,不构成独立层。审计日志层确保无论决策来自哪一层(规则引擎 deny、白名单 allow、用户确认允许),最终都经过统一审计点记录,便于追溯。

6.2 各层决策返回值行为

type ApprovalResult =
  | { type: 'deny'; reason: string; source: string }   // 拒绝,不执行
  | { type: 'allow'; source: string }                    // 允许,继续执行
  | { type: 'ask'; source: string }                      // 需要用户确认
  | { type: 'modified'; input: Record<string, unknown>; source: string }  // 修改参数后继续
  | { type: 'continue' }                                 // 不决策,流入下一层
可返回 短路能力
Rule Engine deny, allow, continue deny/allow 均短路
Read-only Whitelist allow, continue allow 短路
Permission Mode deny, allow, continue deny/allow 均短路
Hook PreToolUse deny, allow, ask, modified, continue deny 短路;allow 跳过 Layer 5 直接到 Audit
User Confirmation deny, allow
Audit / Log continue(只记录,不决策) 永不短路

Hook 层(Layer 4)的返回值语义

interface HookDecision {
  decision?: 'allow' | 'deny' | 'ask';
  reason?: string;
  modifiedInput?: Record<string, unknown>;
}
handler 返回 流水线行为
null / undefined 钩子无意见,继续进入 Layer 5(User Confirmation)
{ decision: 'deny', reason: '...' } 短路,跳过 Layer 5,直接到 Layer 6(Audit),工具不执行
{ decision: 'allow' } 短路,跳过 Layer 5,直接到 Layer 6(Audit),工具执行
{ decision: 'ask' } 继续进入 Layer 5(User Confirmation)弹窗
{ modifiedInput: {...} } 修改后的参数替换原参数,继续流转(可配合 decision 或单独使用)

6.3 新建 approval/ 模块

packages/codingcode/src/approval/
├── types.ts           # ApprovalResult, ToolCallRequest, PermissionRule, PermissionMode
├── rule-engine.ts     # RuleEngine: addRule/removeRule/evaluate, minimatch 模式匹配
├── pipeline.ts        # ApprovalPipeline: 六层流水线串联
├── confirmation.ts    # UserConfirmation: TUI Y/N/A/V + Always/Never 规则写入 + 超时 60s
├── presets.ts         # DEFAULT_DENY_RULES, READONLY_TOOLS
└── index.ts           # ApprovalService (Effect)

6.4 替换沙箱模块

当前 sandbox/index.ts 约 60 行普通 class + interface,全部替换为 Effect Service:

// sandbox/index.ts
export class SandboxService extends Effect.Service<SandboxService>()('Sandbox', {
  effect: Effect.gen(function* () {
    return {
      initialize: (cfg: SandboxConfig) =>
        Effect.promise(() => SandboxManager.initialize({
          network: {
            allowedDomains: cfg.allowedDomains ?? [],
            deniedDomains: cfg.deniedDomains ?? [],
            allowLocalBinding: false,  // 必须 false,否则可通过 IP 绕过域名白名单
            allowUnixSockets: cfg.allowUnixSockets ?? [],
          },
          filesystem: {
            denyRead: cfg.denyReadPaths ?? [],
            allowRead: cfg.allowReadPaths ?? [],
            allowWrite: cfg.allowWritePaths ?? [],
            denyWrite: cfg.denyWritePaths ?? [],
          },
        })),

      wrapCommand: (command: string) =>
        Effect.promise(() => SandboxManager.wrapWithSandbox(command)),

      execute: (opts: ExecuteOptions): Effect.Effect<ExecResult> =>
        Effect.gen(function* () {
          if (!SandboxManager.isAvailable()) {
            // Windows / 不支持平台:降级为纯应用级审批 + Node.js timeout
            yield* Effect.logWarning(
              'Sandbox runtime not available on this platform, falling back to application-level restrictions',
            );
            return yield* executeWithTimeoutOnly(opts);
          }
          return yield* Effect.tryPromise(() =>
            SandboxManager.executeWrapped(opts.command, {
              timeout: opts.timeoutMs ?? 60000,
            }),
          );
        }),

      isAvailable: () => Effect.sync(() => SandboxManager.isAvailable()),

      cleanup: () => Effect.promise(() => SandboxManager.cleanup()),
    };
  }),
}) {}

沙箱风险验证结论(实际调研 @vscode/sandbox-runtime):

问题 结论 影响 应对
域名白名单代理 ✅ 原生支持 HTTP/SOCKS proxy 必须设置 allowLocalBinding: false 防止 IP 绕过
cgroup 资源限制 ❌ SRT 不支持 Timeout 用 spawn({ timeout }),cgroups 后续按需添加
Windows 降级 ❌ 不支持(含 WSL2) isAvailable() 返回 false → 降级为纯应用级审批
npm 包维护 ⚠️ 微软 fork 相对活跃 使用 @vscode/sandbox-runtime,监控上游 issue

关键约束:SRT 只负责隔离(文件系统 + 网络),不负责资源限制(memory/CPU/PID)。


6.5 ToolExecutor 改造为 Effect Service

当前 executor.ts 是普通 class,改为 Effect Service。所有依赖通过 yield* 解析,不再手动注入

// tools/executor.ts
export class ToolExecutorService extends Effect.Service<ToolExecutorService>()('ToolExecutor', {
  effect: Effect.gen(function* () {
    return {
      execute: (
        name: string,
        args: unknown,
        opts?: { signal?: AbortSignal },
      ): Effect.Effect<string, AgentError> =>
        Effect.gen(function* () {
          const registry = yield* ToolService;
          const hooks = yield* HookService;
          const approval = yield* ApprovalService;
          const sandbox = yield* SandboxService;

          const toolResult = registry.get(name);
          if (!toolResult.ok) return yield* Effect.fail(toolResult.error);
          const tool = toolResult.value;

          // 1. Approval pipeline
          const decision = yield* approval.evaluate({
            tool: name,
            input: args as Record<string, unknown>,
          });
          if (decision.type === 'deny') {
            yield* hooks.emit('tool.execute.denied', {
              toolName: name,
              args: args as Record<string, unknown>,
              reason: decision.reason,
              source: decision.source,
            });
            return yield* Effect.fail(
              AgentError.toolNotAllowed(name, decision.reason),
            );
          }

          // 2. Use modified input from approval pipeline (or hook)
          let finalArgs =
            (decision as any).modifiedInput ?? (args as Record<string, unknown>);

          // 3. Hook PreToolUse decision
          const hookDecision = yield* hooks.emitDecision('tool.approval.pre', {
            toolName: name,
            args: finalArgs,
          });
          if (hookDecision?.decision === 'deny') {
            return yield* Effect.fail(
              AgentError.toolNotAllowed(name, hookDecision.reason ?? 'denied by hook'),
            );
          }
          if (hookDecision?.modifiedInput) {
            finalArgs = hookDecision.modifiedInput;
          }

          // 4. Notification hook (观察型)
          yield* hooks.emit('tool.execute.before', { toolName: name, args: finalArgs });

          const parsedArgs = yield* Effect.sync(() => tool.parameters.parse(finalArgs));
          const start = Date.now();
          const result = yield* Effect.tryPromise({
            try: () => tool.execute(parsedArgs, opts?.signal),
            catch: (e) =>
              e instanceof AgentError
                ? e
                : AgentError.toolExecutionFailed(name, e),
          });

          yield* hooks.emit('tool.execute.after', {
            toolName: name, args: finalArgs, result,
            durationMs: Date.now() - start,
          });

          return result;
        }).pipe(
          Effect.tapError((error) =>
            hooks.emit('tool.execute.error', {
              toolName: name,
              args: args as Record<string, unknown>,
              error,
            }),
          ),
        ),
    };
  }),
}) {}

关键变化

  • emitSync 删除,全部走 yield* hooks.emit()
  • 审批调用 approval.evaluate() 在工具执行前完成
  • 参数修改链路:原始 args → approval pipeline → Hook PreToolUse → 最终执行参数

6.6 钩子系统改造

6.6.1 删除 emitSync

emitSync 是专门为 class-based ToolExecutor 开的"后门"。ToolExecutor 改为 Effect Service 后,没有外部调用者需要它。

6.6.2 接入 5 个缺失的点

钩子点 接入位置
llm.request.before llm/client.tscompleteStream() 调用前
llm.response.after llm/client.tsresponse 完成后
llm.response.error llm/client.tsresponse 出错时
session.save.before session/store.ts → 写入前
session.save.after session/store.ts → 写入后

6.6.3 增加决策钩子点

type HookPoint =
  | 'tool.execute.before' | 'tool.execute.after' | 'tool.execute.error'
  | 'tool.execute.denied'          // 新增:审批拒绝时触发(观察型)
  | 'tool.approval.pre'            // 新增:审批流水线中,可返回决策
  | 'tool.approval.post'           // 新增:审批后,可修改输出
  | 'llm.request.before' | 'llm.response.after' | 'llm.response.error'
  | 'session.save.before' | 'session.save.after';

6.6.4 扩展 handler 类型

type HookHandler =
  | ObserverHandler    // (payload) => void | Promise<void>
  | DecisionHandler;   // (payload) => HookDecision | Promise<HookDecision>

interface HookDecision {
  decision?: 'allow' | 'deny' | 'ask';
  reason?: string;
  modifiedInput?: Record<string, unknown>;
  modifiedOutput?: unknown;
}

6.6.5 emitDecision 执行策略

问题:"遇到第一个返回非 null 决策的就停止"——"第一个"按什么排序?注册顺序?系统内置 vs 用户注册?

方案:新增 prioritysource 字段,数字小的先执行。系统内置 hooks 用负优先级:

interface HookRegistration {
  id: string;
  event: HookPoint;
  handler: DecisionHandler;
  priority: number;        // 数字小的先执行
  source: 'system' | 'user';  // 来源标记
}

// 系统内置 hooks(审计、安全策略)
// priority: -100 ~ -1

// 用户注册的 hooks
// priority: 0 ~ 100(默认 0,数字小的先执行)

emitDecision 方法:

emitDecision(
  point: HookPoint,
  payload: Record<string, unknown>,
): Effect.Effect<HookDecision | null> {
  return Effect.gen(function* () {
    const handlers = getSortedByPriority(point);  // 按 priority 升序
    for (const h of handlers) {
      const result = yield* Effect.tryPromise(() => h.handler(payload));
      if (result != null) return result;  // 第一个非 null 决策即生效
    }
    return null;
  });
}

执行顺序

  1. 所有 handler 按 priority 升序排列(系统优先,因为有负值)
  2. 逐个调用,遇到第一个返回非 null 的 HookDecision 就停止
  3. 同 priority 的 handler 按注册先后顺序执行

这样系统内置的审计/安全钩子(priority -100)一定先于用户钩子(priority 0+)执行。


6.7 并发工具调用处理

Claude Code 支持一次请求生成多个 tool call(parallel function calling)。审批流水线需要处理并发场景:

LLM 返回 3 个 tool_call:
  tc1: Read(fileA)    — 只读
  tc2: Bash(rm fileB) — 破坏性写
  tc3: Edit(fileC)    — 写

策略

  1. 所有 tool call 的审批各自独立并行 — 每个 tool call 走完整的六层流水线,互不阻塞。因为每个 tool call 的审批决策只取决于自己的工具名和参数。

  2. 一个被 deny 不取消 sibling — Read 被 deny 不影响 Bash 继续执行。每个工具独立决策。但如果需要"事务性"语义(all-or-nothing),由上层调用者决定。

  3. 破坏性工具(Bash)必须串行执行 — 不是串行审批,而是串行执行。审批可以并行,但 Bash 的执行必须串行化,避免竞态条件。当前 agent.ts:97-109Effect.forEach + concurrency: 'unbounded' 并行执行所有工具。改造后:

// agent.ts: runReActLoop 内
const safeTools = toolCalls.filter(tc => tc.name !== 'Bash');
const bashTools = toolCalls.filter(tc => tc.name === 'Bash');

// 安全工具并行执行
const safeResults = await Effect.runPromise(
  Effect.forEach(safeTools, tc =>
    executor.execute(tc.name, tc.arguments, { signal: controllers[i].signal }),
  { concurrency: 'unbounded' },
  ),
);

// Bash 工具串行执行(避免竞态)
const bashResults: any[] = [];
for (const tc of bashTools) {
  const r = await Effect.runPromise(
    executor.execute(tc.name, tc.arguments, { signal: controllers[i].signal }),
  );
  bashResults.push(r);
}

const results = [...safeResults, ...bashResults];

6.8 runReActLoop 的 AsyncGenerator 边界

agent/agent.ts:47runReActLoopasync function*(AsyncGenerator),不是 Effect.gen。这意味着:

  • 不能使用 yield* 调用 Effect Service 方法
  • 必须在边界处使用 Effect.runPromise() 将 Effect 转为 Promise

当前代码已经这样做了(agent.ts:98-109 用 Effect.runPromise(Effect.forEach(...)))。改造后继续保持这个模式,不引入新的不一致性:

// AgentService 内部用 yield* 获取 executor(在 Effect.gen 上下文内)
export class AgentService extends Effect.Service<AgentService>()('Agent', {
  effect: Effect.gen(function* () {
    const executor = yield* ToolExecutorService;  // ← 这里是在 Effect.gen 内,可以用 yield*
    let config: ResolvedConfig = resolveConfig({});

    return {
      runStream: (messages: Message[], llm: LLMStreamAdapter) =>
        runReActLoop(messages, config, llm, executor),
        // ↑ AsyncGenerator,在内部用 Effect.runPromise 调用 executor.execute()
    };
  }),
}) {}

6.9 CLI 简化和 server 类型安全

改造前 — 手动 new,脱离 Layer:

const sandbox = new DefaultSandbox();
const executor = new ToolExecutor(tools, hooks, sandbox);
const app = createServer({ llm: llmResult.value, executor, hooks });

改造后 — 全部走 Layer 解析:

// cli.ts: SandboxService、ToolExecutorService、ApprovalService 由 AppLayer 提供
const app = createServer({ llm: llmResult.value });

server/index.ts ServerDeps 缩减为:

type ServerDeps = {
  llm: LLMClient;  // 不再是 any
};

路由 handler 中的 sendMessage 不再需要 executorhooks 参数——它们通过 yield* 在 Effect 上下文中解析。


7. 与现有模块的关系

packages/codingcode/src/approval/ — 新建

6 个文件,ApprovalService 作为 Effect Service 注册到 layer.ts

packages/codingcode/src/sandbox/index.ts — 重写

删除 Sandbox interface + DefaultSandbox class,改为 Effect Service。

packages/codingcode/src/tools/executor.ts — 重写

删除 ToolExecutor class,改为 ToolExecutorService (Effect Service)。

packages/codingcode/src/hooks/registry.ts — 改造

  • 删除 emitSync
  • 增加 tool.approval.pretool.approval.posttool.execute.denied 钩子点
  • 扩展 HookHandlerObserverHandler | DecisionHandler
  • 新增 emitDecision(),按 priority 排序执行 handler
  • HookRegistration 增加 prioritysource 字段

packages/codingcode/src/agent/agent.ts — 改造

  • AgentService 内部 yield* ToolExecutorService
  • runStream() 签名去掉 executor 参数
  • 并发工具调用:安全工具并行,Bash 串行

packages/codingcode/src/orchestrate.ts — 改造

  • sendMessage 去掉 executorhooks 参数

packages/codingcode/src/llm/client.ts — 改造

  • 接入 llm.request.before/after/error 三个钩子

packages/codingcode/src/session/store.ts — 改造

  • 接入 session.save.before/after 两个钩子

packages/codingcode/src/cli.ts — 改造

  • 删除 new DefaultSandbox()new ToolExecutor(...)

packages/codingcode/src/server/index.ts — 改造

  • ServerDeps 缩减为只含 llm

packages/codingcode/src/layer.ts — 改造

const InfraLayer = Layer.mergeAll(
  ToolLayer,       // ToolService
  HookLayer,       // HookService
  SandboxLayer,    // SandboxService
  ApprovalLayer,   // ApprovalService
);
const ToolExecutorLayer = ToolExecutorService.Default.pipe(
  Layer.provide(InfraLayer),
);
export const AgentLayer = AgentService.Default.pipe(
  Layer.provide(ToolExecutorLayer),
);

测试文件

  • 新建 test/approval/rule-engine.test.ts — 规则匹配(glob、regex、优先级、参数匹配)
  • 新建 test/approval/pipeline.test.ts — 六层流转(每层的 allow/deny/ask/continue 路径)
  • 新建 test/approval/presets.test.ts — 默认规则集验证
  • 新建 test/hooks/decision.test.ts — emitDecision 排序策略、系统/用户优先级
  • 新建 test/tools/executor-approval.test.ts — ToolExecutorService 批准/拒绝/修改参数路径
  • 新建 test/sandbox/wrap-command.test.ts — 命令包装、降级行为
  • 修改 test/hooks/registry.test.ts — 删除 emitSync 测试,补充新钩子点测试
  • 修改 test/agent/agent.test.ts — 并发工具调用的串行/并行行为

8. 测试场景

  1. 高危命令 rm -rf / → Layer 1 规则引擎直接 deny,不弹确认
  2. 只读工具 Read → Layer 2 白名单自动 allow
  3. 写工具 Edit → Layer 2 无匹配,Layer 3 default 模式,进入 Layer 4 Hook(无决策),进入 Layer 5 用户确认
  4. acceptEdits 模式 + Edit → Layer 3 短路 allow,不弹确认
  5. plan 模式 + Write → Layer 3 直接 deny
  6. bypass 模式 + Bash → Layer 3 allow,沙箱仍然限制文件系统和网络
  7. PreToolUse hook(Layer 4)返回 { modifiedInput } → 修改后的参数被后续层使用
  8. PreToolUse hook(Layer 4)返回 { decision: 'deny' } → 跳过 Layer 5,直接到 Layer 6 审计
  9. PreToolUse hook(Layer 4)返回 { decision: 'allow' } → 跳过 Layer 5,直接到 Layer 6 审计
  10. 无规则匹配 + 非只读 + default 模式 + 无 hook → 进入 Layer 5 用户确认弹窗
  11. 用户确认超时 60s → Layer 5 返回 deny
  12. 用户选 Always → 规则引擎新增 allow 规则,下次同类工具自动通过
  13. 用户选 Never → 规则引擎新增 deny 规则,下次同类工具自动拒绝
  14. 系统 hook(priority -100)先于用户 hook(priority 0)执行
  15. 3 个并发 tool call(Read + Bash + Edit)→ Read allow,Edit ask,Bash deny → 各自独立
  16. Bash 类工具在并发中串行执行
  17. Windows 平台 → isAvailable() 返回 false → 降级为纯应用级审批
  18. Layer 6 审计日志记录每一层的决策来源和结果

9. 假设与默认决策

  1. @vscode/sandbox-runtime 在 Linux 上用 bubblewrap,macOS 上用 sandbox-exec,Windows 上不可用 → isAvailable() 返回 false,降级为应用层审批
  2. 用户确认弹窗在 TUI 中用 Ink/readline 实现,非 TUI 模式(HTTP server)默认拒绝
  3. User Confirmation 超时 60 秒无用户响应 → deny(这是 Layer 5 的超时行为,不是独立层)
  4. 规则引擎用 minimatch 做 glob 匹配,用 RegExp 做正则匹配
  5. 旧 sandbox 的 denyCommandsdenyPaths 配置直接废弃,迁移到规则引擎的 deny 规则
  6. runReActLoop 作为 AsyncGenerator 在边界处使用 Effect.runPromise,与当前 aget.ts 模式一致
  7. SRT 配置中 allowLocalBinding 必须为 false,防止 IP 地址绕过域名白名单
  8. 沙箱不负责资源限制(memory/CPU/PID),timeout 由 Node.js spawn({ timeout }) 处理

10. 实施顺序

  1. 新建 approval/types.ts — 所有类型定义
  2. 新建 approval/rule-engine.ts — 规则引擎
  3. 新建 approval/presets.ts — 默认规则集 + 只读工具白名单
  4. 新建 approval/confirmation.ts — 用户确认弹窗 + 超时逻辑
  5. 新建 approval/pipeline.ts — 六层流水线串联
  6. 新建 approval/index.ts — ApprovalService (Effect)
  7. 改造 hooks/registry.ts — 新增 HookPoint、HookDecision 类型、priority+source、emitDecision、删除 emitSync
  8. 改造 sandbox/index.ts — 改为 SandboxService (Effect),接入 @vscode/sandbox-runtime
  9. 改造 tools/executor.ts — 改为 ToolExecutorService (Effect),插入审批流水线
  10. 改造 agent/agent.ts — AgentService yield* ToolExecutorService,并发 Bash 串行化
  11. 改造 orchestrate.ts — sendMessage 去掉 executor/hooks 参数
  12. 改造 llm/client.ts — 接入 LLM 钩子
  13. 改造 session/store.ts — 接入 session 钩子
  14. 改造 server/index.ts — ServerDeps 缩减
  15. 改造 cli.ts — 删除手动 new
  16. 改造 layer.ts — 注册新 Service,调整依赖图
  17. 编写全部测试文件

11. 一句话结论

新建六层审批流水线(规则引擎→只读白名单→权限模式→Hook PreToolUse→用户确认→审计日志),沙箱接入 @vscode/sandbox-runtime 实现 OS 级隔离,钩子补全接入点并增加优先级决策能力。核心改进:将 ToolExecutor 改造为 Effect Service,消除 emitSync 后门和手动依赖注入,统一整个依赖链为 Effect Layer 体系。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions