refactor(permission-system): simplify policy and autoreview flow

This commit is contained in:
云服务部-叶林立
2026-09-03 02:01:06 +08:00
parent 180da95ab7
commit 6e7effa792
112 changed files with 911 additions and 10346 deletions
@@ -70,9 +70,9 @@ interface PermissionsService {
/**
* Register a custom access-intent extractor for a specific tool name.
* Declares the filesystem path a tool accesses so the `path` and
* `external_directory` gates can see it. Returns a disposer; throws if an
* extractor is already registered for that tool name.
* Declares the local filesystem path a tool accesses so the `path` gate can
* see it, or suppresses local-path inference for a remote tool. Returns a
* disposer; throws if an extractor is already registered for that tool name.
*/
registerToolAccessExtractor(
toolName: string,
@@ -85,13 +85,13 @@ interface PermissionsService {
| Parameter | Required | Description |
| ----------- | -------- | ---------------------------------------------------------------------------------------- |
| `surface` | Yes | Permission surface: `"bash"`, `"read"`, `"mcp"`, `"skill"`, `"external_directory"`, etc. |
| `surface` | Yes | Permission surface: `"bash"`, `"read"`, `"mcp"`, `"skill"`, `"path"`, etc. |
| `value` | No | Value to evaluate (command, name, path); defaults to `""` |
| `agentName` | No | Agent name for per-agent policy resolution |
Returns `PermissionCheckResult` with fields `state`, `matchedPattern`, `source`, `origin`, etc.
For a path-shaped surface (`path`, `external_directory`, or a path-bearing tool — `read`/`write`/`edit`/`grep`/`find`/`ls`), the supplied `value` is matched against both the path as given and its canonical (symlink-resolved) form, at parity with the gates — so a query for a symlinked path matches a rule on its real target.
For a path-shaped surface (`path` or a path-bearing tool — `read`/`write`/`edit`/`grep`/`find`/`ls`), the supplied `value` is matched against both the path as given and its canonical (symlink-resolved) form, at parity with the gates — so a query for a symlinked path matches a rule on its real target.
For the `bash` surface, a `value` containing a chained or nested command (joined by `&&`, `||`, `;`, `|`, `&`, or newlines, or nested in a command substitution/subshell) is decomposed into its command-pattern units and resolved most-restrictive (`deny` > `ask` > `allow`), at parity with the enforcement gate — so `cd /repo && npm install x` returns the decision of the `npm install x` unit, not the leading `cd`.
A previously chained command that returned `allow` (riding an allowed leading command) may therefore now return `deny`/`ask`.
@@ -235,7 +235,7 @@ This is the reference implementation for the seam — see `src/builtin-tool-inpu
#### `registerToolAccessExtractor`
Declare the filesystem path a tool will access so the cross-cutting `path` and `external_directory` gates can evaluate it.
Declare the local filesystem path a tool will access so the cross-cutting `path` gate can evaluate it; an extractor may also suppress local-path inference for remote paths.
```typescript
registerToolAccessExtractor(
@@ -360,7 +360,7 @@ Nested rather than flattened so the event and the prompt payload share one shape
| Field | Type | Description |
| ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------ |
| `requester` | `PromptRequester` | Who is asking, and whether the ask arrived from a subagent |
| `surface` | `string` | The **gate** surface the rule fired on — `"external_directory"`, `"path"`, `"bash"`, a tool name |
| `surface` | `string` | The **gate** surface the rule fired on — `"path"`, `"bash"`, or a tool name |
| `toolName` | `string \| null` | The gated tool name; `null` when the ask is not tool-shaped |
| `invokedToolName` | `string \| null` | The invoked name when a shell alias re-exposes bash under another name |
| `value` | `string` | The decision-relevant value: the command, path, MCP target, or skill name |
@@ -372,7 +372,7 @@ Nested rather than flattened so the event and the prompt payload share one shape
The top-level `surface` and `request.surface` are two different facts and both belong on the event.
The top-level one is the **display** projection — the child's tool name, what a notification shows.
`request.surface` is the **gate** surface the rule fired on: a `read` of a path outside the working directory displays as `"read"` and gates on `"external_directory"`.
`request.surface` is the **gate** surface the rule fired on. Paths outside the working directory have no special surface; they use the same `path` and tool surfaces as other paths.
#### `ForwardedPromptContext`
@@ -410,7 +410,7 @@ pi.events.on("permissions:decision", (raw) => {
| Field | Type | Description |
| ---------------- | ------------------- | ----------------------------------------------------------------------------------------- |
| `requestId` | `string` | Id of the permission request this decision resolves |
| `surface` | `string` | Permission surface (`"bash"`, `"read"`, `"mcp"`, `"skill"`, `"external_directory"`, etc.) |
| `surface` | `string` | Permission surface (`"bash"`, `"read"`, `"mcp"`, `"skill"`, `"path"`, etc.) |
| `value` | `string` | Value evaluated (command, tool name, skill name, path) |
| `result` | `"allow" \| "deny"` | Final outcome |
| `resolution` | `string` | How the outcome was reached (see table below) |