# pi-ast-guard

[![npm](https://img.shields.io/npm/v/pi-ast-guard)](https://www.npmjs.com/package/pi-ast-guard)

**Languages:** [简体中文](README.md) | [English](README.en.md)

## 简介

`pi-ast-guard`（原名 `pi-damage-control`）是一款基于 AST 的代理安全防护扩展，专为 [Pi](https://pi.dev) 编码代理设计。
它能够在破坏性 shell 命令或文件操作执行之前进行拦截，同时避免因普通文本、markdown、heredoc 和任务描述中的内容而产生误报。

> **继承说明**：本项目继承自 [`pi-damage-control`](https://github.com/ghoseb/pi-damage-control)（原作者 Baishampayan Ghose，原仓库已删除），
> 在保留原有 AST 解析方案与策略引擎的基础上继续维护与改进。

## 安装

```bash
pi install npm:pi-ast-guard
# 或者
# pi install git:github.com/rainmanhhh/pi-ast-guard
```

## DNA 模式（Do Not Ask）

不想每次操作都弹窗确认？`/ag:dna` 一键进入 DNA 模式：遇到 `ask` 不再询问，而是**按策略自动审批或拒绝**；`block` 规则照常拦截，安全底线不丢。

自动回答由 4 个参数决定（工作区内/外 × 读/写），外加工具黑白名单与累计违反上限，可在 `settings.dna` 中配置：

| 参数 | 默认 | 含义 |
|------|------|------|
| `dna.readInside` | `allow` | 工作区内读取 |
| `dna.readOutside` | `allow` | 工作区外读取（仅读取，风险低） |
| `dna.writeInside` | `allow` | 工作区内写/删/移动 |
| `dna.writeOutside` | `block` | 工作区外写/删/移动（高风险，默认拒绝） |
| `dna.maxViolations` | `3` | 本次 DNA 模式下累计自动拒绝的总上限（所有规则含工具黑白名单），达到后强制中断会话并清零（对话结束也清零） |
| `dna.allowTools` | `[]` | 工具白名单（非空时启用白名单模式，仅允许名单中的工具调用） |
| `dna.blockTools` | `[]` | 工具黑名单（白名单为空时启用黑名单模式，禁止名单中的工具调用） |

> 工作区 = cwd + `settings.extraDirs`。命令类 `ask`（如发布命令）在 DNA 模式下默认放行；自动拒绝会拦截操作并引导 AI 评估替代方案（勿绕过规则），本次模式内累计违反达到上限（`dna.maxViolations`，默认 3）才强制中断。

## 工作原理

- 使用 `just-bash` 解析 Bash 命令 AST，**无需正则表达式回退**
- 根据 `config/default-policy.yaml` 中的语义化命令规则进行评估
- 从 Bash 命令和 Pi 文件工具中提取文件操作意图
- 对零访问（zero-access）、只读（read-only）、禁止删除（no-delete）和工作区外写入路径应用路径策略
- 当策略动作为 `ask` 时弹出四选对话框（同意一次 / 本会话允许 / 本会话拒绝 / 拒绝一次）；当 UI 不可用时自动拒绝（fail-closed）

## 配置方式

创建项目级策略文件：

```text
.pi/ast-guard.yml
```

或全局策略文件：

```text
~/.pi/agent/ast-guard.yml
```

策略分层加载：全局策略（`~/.pi/agent/ast-guard.yml`）为基础层（缺失时用内置默认策略），项目策略（`.pi/ast-guard.yml`）存在时在其上合并——`settings` 字段级覆盖，`rules` 按 `id` 覆盖（同 id 项目优先），不同 id 的规则全部保留。

内置默认策略文件：[`config/default-policy.yaml`](./config/default-policy.yaml)

### 策略语言

顶层策略结构：

```yaml
settings:
  language: auto
  parseFailure: ask
  showStatus: true
  # 额外工作区目录：与 cwd 共同构成「完整的工作区」，outsideWorkdir: true 的语义变为「在工作区目录列表之外」
  extraDirs: []
  # DNA 模式（Do Not Ask）下 ask 的自动回答
  dna:
    readInside: allow
    readOutside: allow
    writeInside: allow
    writeOutside: block
    maxViolations: 3
rules: []
```

`settings.language` 控制扩展 UI 提示的语言（`zh` 中文 / `en` 英文 / `auto` 跟随系统，默认 `auto`），包括通知、拦截/确认对话框、命令描述与默认策略规则文案；`auto` 通过系统区域设置探测（Windows 取系统区域，Unix-like 取 `LANG`/`LC_ALL`，无则英文）。跟随项目策略，`/ag:status`（已合并 reload）后生效。

#### 路径规则

路径规则与命令规则同属顶层的 `rules` 列表。其 `type` 字段编码了路径策略的子类型：

- `path:zeroAccess` — 禁止读、写、删除和移动操作
- `path:readOnly` — 仅禁止写、删除和移动操作，读取允许
- `path:noDelete` — 仅禁止删除和移动操作

每条路径规则示例：

```yaml
- id: path-secrets-env
  type: path:zeroAccess
  action: block
  reason: 环境文件可能包含密钥信息
  match:
    path:
      any: .env*
      except: [.env.example]
```

`match.path.any` 可以是字符串或字符串列表：

```yaml
match:
  path:
    any: [LICENSE, LICENSE.*, COPYING, COPYING.*]
```

支持的路径模式（glob 语义，`*` 按段匹配不跨 `/`，`**` 匹配任意深度）：

- 精确匹配/文件名：`README.md`、`.env`（无斜杠模式匹配**任意位置**的同名文件，如 `ast-guard.yml` 可命中 `~/.pi/ast-guard.yml`）
- 目录：`.git/`、`node_modules/`（含目录本身及其下所有内容）
- 通配符：`*.pem`、`docker-compose.*.yml`、`dist/**`、`**/secrets/**`（`**` 段感知，`**/secrets/**` 命中任意深度含 `secrets` 段的路径）
- 前缀：`build-*`（`/` 段结尾）
- 当前工作目录宏：`$CWD`、`$CWD/…`（工作区外用 `outsideWorkdir: true`）
- 相对模式解析到工作区根（cwd），`~` 解析到 HOME

工作区外写入确认示例：

```yaml
- id: path-outside-project-write
  type: path:readOnly
  reason: 工作区外写入需要确认
  match:
    path:
      outsideWorkdir: true
      except: [/tmp/, /dev/null]
```

#### 命令规则

命令规则使用 `type: command`：

```yaml
- id: git-reset-hard
  type: command
  action: block
  reason: git reset --hard 会丢弃工作区变更
  match:
    command: git
    subcommand: reset
    flags:
      any: [--hard]
```

常用匹配字段：

- `command`：精确的命令名称
- `commandAny`：多个命令名称之一
- `subcommand`：第一个非选项命令操作数
- `subcommandAny`：多个子命令之一
- `argsAny`：参数列表中**任意一个**匹配即可
- `argsAll`：参数列表中**全部**必须匹配
- `argsNone`：参数列表中**不得出现**任何匹配项
- `argsContainAny` / `argsContainAll`：参数子串匹配
- `flags.any` / `flags.all` / `flags.none`：语义化标志匹配
- `optionsBeforeSubcommand.value`：子命令检测前的全局选项值，适用于 `git -C repo ...`
- `visibleTextAny` / `visibleTextAll` / `visibleTextNone`：匹配可见静态文本，适用于 SQL 执行器

规则默认启用。审批对话框中的「本会话允许」会在当前会话内临时解除被触发规则的检查。

规则可配 `priority`（默认 `0`，值越大越优先）：命中多条规则时只保留最高优先级的一组再按动作强度判定（`block > ask > allow`），因此高优先级 `allow` 规则可豁免低优先级 `ask`/`block`。来源偏移：项目 `0` / home `-0.3` / 内置默认 `-0.6`。

动作选项：

- `allow` — 放行
- `ask` — 请求确认
- `block` — 直接阻止

`action` 字段可选，省略时默认为 `ask`。

```yaml
- id: git-commit
  type: command
  action: ask
  reason: git commit 需要确认
  match:
    command: git
    subcommand: commit
```

完整示例：

```yaml
settings:
  parseFailure: ask
  showStatus: true
rules:
  - id: path-secrets-env
    type: path:zeroAccess
    action: block
    reason: 环境文件可能包含密钥信息
    match:
      path:
        any: .env*
        except: [.env.example]
  - id: path-outside-project-write
    type: path:readOnly
    reason: 工作区外写入需要确认
    match:
      path:
        outsideWorkdir: true
        except: [/tmp/, /dev/null]
  - id: git-commit
    type: command
    action: ask
    reason: git commit 需要确认
    match:
      command: git
      subcommand: commit
```

## 审批对话框

当规则动作评估为 `ask` 时，弹出四选对话框（30 秒超时，超时即拒绝）：

- **同意一次** — 只放行当前这一次工具调用（单次生效，不影响下次）
- **本会话允许** — 记录为会话级决策（决策层），当前会话内该规则在作用域内不再询问
- **本会话拒绝** — 记录为会话级决策（决策层），当前会话内该规则在作用域内直接拦截、不再弹窗
- **拒绝一次** — 拦截本次调用（单次生效，不影响下次）

**会话级决策 = 规则匹配之上叠加的一层精确路径匹配**：每条决策是 `(规则, 作用域路径)`，查询时 deny 优先于 allow（无论新旧）；同一 (规则, 作用域) 的新决策覆盖旧决策，不同作用域累积生效。

**拒绝（含本会话拒绝、拒绝一次）、对话框取消/超时未应答，以及无 UI 自动拒绝，都会在拦截的同时中止当前轮**（agent 停止执行，回到等待用户输入的状态）；拦截消息仍作为工具结果发给 AI。

对话框被取消或超时均视为拒绝并拦截。无 UI 环境（print/JSON 模式）下，`ask` 决策自动拦截（fail-closed）。

### 作用域输入（粗粒度规则）

规则匹配域超出「工作区锚定的局部区域」（粗粒度）时，选中「本会话允许/拒绝」后**额外弹一次输入框**确定作用域：

| 输入 | 作用域 |
|------|--------|
| （空） | 仅本次触发的目标文件（父目录存在即可，容忍尚未创建的文件） |
| `.` | 目标文件所在目录 |
| `../..` | 从目标文件的上两级目录起（相对输入均相对目标文件目录解析） |
| 绝对路径 | 直接以该路径为作用域 |

输入会校验：不得含通配符；**作用域必须与规则的匹配域相交**（如 `outsideWorkdir` 规则不接受工作区内的绝对路径，`/etc/**` 规则不接受 `/var`）；目录作用域必须真实存在；不通过则提示并重新输入。取消输入等同取消对话框（fail-closed，中止当前轮）。

粗粒度判定（任一命中即弹输入）：

- `outsideWorkdir: true`
- 根锚定绝对路径且解析后字面深度 ≤ 2（如 `/etc/**`、`/var/log/`；`/home/user/data/**` 深度 ≥ 3 视为细粒度）
- 首段为通配符或覆盖工作区及以上（`**/…`、`*/…`、`.`、`./**`、`..`、`../**`）

无斜杠模式（`ast-guard.yml`、`*.log`）**不弹输入**：选中后自动把作用域限定为本次触发的目标文件（同名其他位置下次仍会询问）。命令面规则（无路径可锚定）与细粒度路径规则保持规则级决策（覆盖该规则所有路径）。

### 最近决策与按序号清除

**会话级决策记录到 `/ag:status` 面板的「最近决策」**（保留最近 10 条），每行带序号（最新在前 = 1）：

```
最近决策
1. 14:32 本会话允许 → outside-ask → C:/tmp/x.log (touch C:/tmp/x.log)
2. 14:31 本会话拒绝 → git-commit (git commit)
```

单次生效的同意/拒绝不影响下次 ask，不在面板展示。执行 `/ag:forget 1,3` 按序号清除单条决策（逗号分隔多个）；清除后序号重排，新决策到达也会使序号位移，忘记前可先执行 `/ag:status` 查看最新序号。

## 可用命令

| 命令 | 说明 |
|------|------|
| `/ag:forget <序号...>` | 按序号清除会话决策（逗号分隔多个，如 `1,3`），清除后序号重排 |
| `/ag:dna` | 开启/关闭 Do Not Ask 模式 |
| `/ag:status` | 重新加载策略并显示状态面板（含带序号的「最近决策」）；再次执行刷新 |

## 开发

使用 [Bun](https://bun.sh) 作为包管理器：

```bash
bun install
bun run test        # 运行单元测试（Vitest）
bun run test:watch  # 监听模式
bun run typecheck   # TypeScript 类型检查
bun run check       # 完整检查（lint + 测试 + 类型）
bun run bench       # 性能基准（mitata）
```

测试位于 `tests/` 目录，按模块组织（`bash/`、`rules/`、`policy/`、`engine/`、`extension/`、`intents/`）。
已知问题与待优化项见 [docs/known-issues.md](docs/known-issues.md)。

### 本地开发与验证

在 `~/.pi/agent/settings.json` 中直接指向源码（改代码即时生效）：

```jsonc
{
  "extensions": ["E:/workspace/pi-ast-guard/src/index.ts"]
}
```

然后进入任意测试项目运行 `pi`，状态栏出现 🛡 图标即加载成功。仓库内 `bun run check` 通过后提交（pre-commit hook 会自动运行 `bun run lint`）。
也可在 `ast-guard-demo` 测试沙箱（含 `.env`、`dist/`、自定义策略与完整验证清单）中手动验证。

## 鸣谢

本项目继承自 [pi-damage-control](https://github.com/ghoseb/pi-damage-control)（原作者 Baishampayan Ghose，原仓库已删除）。
灵感来源于 [claude-code-damage-control](https://github.com/disler/claude-code-damage-control)。

## 许可证

MIT © rainmanhhh