# 策略语言参考

## 概述

pi-ast-guard 使用 YAML 格式的策略文件来定义安全规则。策略文件可以放在以下位置（按优先级从高到低）：

1. `.pi/ast-guard.yml`（项目级，覆盖层）
2. `~/.pi/agent/ast-guard.yml`（全局，基础层）
3. 内置默认策略（`config/default-policy.yaml`，基础层兜底）

## 顶层结构

```yaml
settings:
  language: auto
  parseFailure: ask
  showStatus: true
  extraDirs: []
  dna:
    readInside: allow
    readOutside: allow
    writeInside: allow
    writeOutside: block
rules: []
```

### version

`version` 字段已不区分、可省略（历史版本字段被静默忽略）。

### settings

| 字段 | 默认值 | 说明 |
|------|--------|------|
| `language` | `auto` | UI 提示语言（通知/拦截对话框/默认策略文案）。可选值：`zh`（中文）、`en`（英文）、`auto`（跟随系统区域设置） |
| `parseFailure` | `ask` | Bash 解析失败时的处理方式。可选值：`allow`（放行）、`ask`（请求确认）、`block`（直接阻止） |
| `showStatus` | `true` | 是否在状态栏显示安全防护图标 |
| `extraDirs` | `[]` | 额外工作区目录列表。与 cwd 共同构成「完整的工作区」；`outsideWorkdir: true` 的语义变为「在工作区目录列表之外」 |
| `dna.readInside` | `allow` | DNA 模式下工作区内读的默认动作（`allow`/`block`） |
| `dna.readOutside` | `allow` | DNA 模式下工作区外读的默认动作（`allow`/`block`） |
| `dna.writeInside` | `allow` | DNA 模式下工作区内写/删/移动的默认动作（`allow`/`block`） |
| `dna.writeOutside` | `block` | DNA 模式下工作区外写/删/移动的默认动作（`allow`/`block`） |
| `dna.maxViolations` | `3` | DNA 模式下本次会话累计自动拒绝的总上限（所有规则含工具黑白名单），达到后强制中断会话并清零计数（对话结束也清零） |
| `dna.allowTools` | `[]` | 工具白名单（非空时启用白名单模式，仅允许名单中的工具调用） |
| `dna.blockTools` | `[]` | 工具黑名单（白名单为空时启用黑名单模式，禁止名单中的工具调用） |
| `dna.cancelledNote` | 内置默认 | DNA 模式下提问交互被自动取消时，追加给 AI 的结果说明文本（引导 AI 勿提问、按最推荐方案自行执行）；不配置时使用内置默认提示 |

### rules

规则数组，每个规则可以是**命令规则**或**路径规则**。

## 命令规则（type: command）

### 字段

| 字段 | 必填 | 说明 |
|------|------|------|
| `id` | 是 | 规则唯一标识符（用于审批对话框、`/ag:status` 面板展示及拦截原因展示） |
| `type` | 是 | 必须为 `"command"` |
| `label` | 否 | 规则的可读标签 |
| `match` | 是 | 匹配条件（见下文） |
| `action` | 否 | 动作，未指定时默认为 `ask` |
| `priority` | 否 | 优先级，值越大越优先（默认 `0`）。命中多条规则时只保留最高优先级的一组再按动作强度判定；来源偏移：项目 `0` / home `-0.3` / 内置默认 `-0.6` |
| `reason` | 是 | 规则被触发时的说明文字 |

### 匹配条件（match）

#### 命令名称匹配

必须指定 `command` 或 `commandAny` 之一：

| 字段 | 说明 |
|------|------|
| `command` | 精确命令名称，如 `"git"` |
| `commandAny` | 多个命令名称之一，如 `["npm", "pnpm", "yarn"]` |

#### 子命令匹配

可选：

| 字段 | 说明 |
|------|------|
| `subcommand` | 精确子命令，如 `"reset"` |
| `subcommandAny` | 多个子命令之一，如 `["install", "add", "remove"]` |

#### 参数匹配

可选：

| 字段 | 说明 |
|------|------|
| `argsAny` | 参数列表中任意一个匹配即可 |
| `argsAll` | 参数列表中全部必须匹配 |
| `argsNone` | 参数列表中不得出现任何匹配项 |
| `argsContainAny` | 参数列表中任意一个包含指定子串 |
| `argsContainAll` | 参数列表中全部包含指定子串 |

#### 标志匹配（flags）

可选：

```yaml
flags:
  any: [--hard]     # 任意一个匹配即可
  all: [-f, -r]     # 全部必须匹配
  none: [-n]        # 不得出现
```

#### 子命令前选项（optionsBeforeSubcommand）

用于匹配子命令之前的全局选项，如 `git -C repo status`：

```yaml
optionsBeforeSubcommand:
  value: [-C, -c, --git-dir]   # 带值的选项
  boolean: [--no-pager]        # 布尔选项
```

#### 其他匹配字段

| 字段 | 说明 |
|------|------|
| `dynamic` | 当命令参数包含动态内容（变量、通配符等）时匹配。可选值：`true`、`false` |
| `operandCount` | 操作数数量限制：`{ min: 1 }`、`{ max: 3 }`、`{ min: 1, max: 3 }` |
| `visibleTextAny` | 在命令的可见文本中任意一个匹配（适用于 SQL 语句） |
| `visibleTextAll` | 在命令的可见文本中全部匹配 |
| `visibleTextNone` | 在命令的可见文本中不得出现 |

## 路径规则

路径规则通过 `type` 字段区分三种路径保护级别：

| type | 保护级别 | 禁止的操作 |
|------|----------|-----------|
| `path:zeroAccess` | 零访问 | 读、写、删除、移动 |
| `path:readOnly` | 只读 | 写、删除、移动 |
| `path:noDelete` | 禁止删除 | 删除、移动 |

### 字段

| 字段 | 必填 | 说明 |
|------|------|------|
| `id` | 是 | 规则唯一标识符 |
| `type` | 是 | 必须为 `path:zeroAccess`、`path:readOnly` 或 `path:noDelete` 之一 |
| `label` | 否 | 规则的可读标签 |
| `match.path` | 是 | 路径匹配模式 |
| `action` | 否 | 动作，未指定时默认为 `ask` |
| `priority` | 否 | 优先级，值越大越优先（默认 `0`）。命中多条规则时只保留最高优先级的一组再按动作强度判定；来源偏移：项目 `0` / home `-0.3` / 内置默认 `-0.6` |
| `reason` | 是 | 规则被触发时的说明文字 |

### 路径匹配模式

`match.path` 支持以下格式：

#### any（精确/通配符匹配）

```yaml
match:
  path:
    any: .env*          # 字符串
    # 或数组
    any: [.env*, "*.env"]
    except: [.env.example]  # 排除项
```

支持的模式类型：

| 模式类型 | 示例 | 说明 |
|----------|------|------|
| 文件名 | `README.md` | 仅匹配文件名为 README.md 的文件 |
| 目录 | `.git/` | 匹配该目录下的所有文件 |
| 通配符 | `*.pem` | 匹配所有 .pem 文件 |
| 前缀 | `docker-compose.*.yml` | 匹配前缀模式 |
| 精确路径 | `/etc/hosts` | 匹配绝对路径 |

#### outsideWorkdir（工作区外匹配）

```yaml
match:
  path:
    outsideWorkdir: true  # 匹配工作区目录列表之外的所有路径
    except: [/tmp/, /dev/null]
```

工作区目录列表 = cwd + `settings.extraDirs`。

## 运行时控制

```bash
/ag:status        # 重新加载策略并显示状态面板（含带序号的最近决策）；再次执行刷新
/ag:forget 1,3    # 按序号清除会话决策（逗号分隔多个）；清除后序号重排，新决策到达也会使序号位移
/ag:dna           # 开启/关闭 Do Not Ask 模式
```

### 会话级决策（决策层）

「本会话允许/本会话拒绝」记录为会话决策层条目（规则, 作用域路径），在规则匹配之上叠加精确路径匹配：deny 优先于 allow（无论新旧）；同一 (规则, 作用域) 的新决策覆盖旧决策，不同作用域累积生效。

粗粒度规则（匹配域超出工作区锚定的局部区域）选中后会**额外弹输入框**确定作用域：空 = 目标文件本身；`.` = 文件所在目录；`../..` = 上两级目录起（相对输入相对目标文件目录解析）；或直接输入绝对路径。校验：不含通配符；作用域必须与规则匹配域相交（`outsideWorkdir` 规则不接受工作区内的绝对路径）；目录必须真实存在；不通过则重新输入，取消即 fail-closed 中止当前轮。

粗粒度判定（任一命中即弹输入）：`outsideWorkdir: true`；根锚定绝对路径且解析后字面深度 ≤ 2（`/etc/**`、`/var/log/`；深度 ≥ 3 如 `/home/user/data/**` 视为细粒度）；首段通配或覆盖工作区及以上（`**/…`、`*/…`、`.`、`./**`、`..`、`../**`）。无斜杠模式（`ast-guard.yml`、`*.log`）不弹输入，自动限定到本次触发的目标文件。命令面规则与细粒度路径规则为规则级决策（覆盖该规则所有路径）。

首次初始化（扩展加载时）自动检测 home 目录 `~/.pi/agent/`，缺失配置文件或 JSON Schema 时自动创建（yml 已含 schema 头，已存在不覆盖）；yml 首行 `# yaml-language-server: $schema=./ast-guard.schema.json` 为编辑器（YAML Language Server）提供补全与校验。

## 策略加载与合并

策略分层加载，各层按以下方式合并：

1. **基础层**：全局策略 `~/.pi/agent/ast-guard.yml`（不存在时用内置默认策略 `config/default-policy.yaml`）
2. **项目层**：项目策略 `.pi/ast-guard.yml` 存在时在基础层之上合并：
   - `settings`：字段级覆盖（项目写了哪个字段就用哪个，未写的继承基础层）
   - `rules`：按 `id` 覆盖（同 id 只保留项目版本），不同 id 的规则全部保留

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

示例：home 配置提供通用规则，项目配置只写差异部分（覆盖某规则的动作或新增规则）即可。

策略文件只在加载时读取一次，修改后执行 `/ag:status`（已合并 reload）重新加载。