mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 07:23:06 +00:00
feat: delegate external read-only tools to auto-review
This commit is contained in:
@@ -64,7 +64,7 @@
|
|||||||
- Codex fast mode 只为符合条件的 `openai-codex-responses` 请求设置 priority service tier,由 `/fast-mode` 在会话内控制。
|
- Codex fast mode 只为符合条件的 `openai-codex-responses` 请求设置 priority service tier,由 `/fast-mode` 在会话内控制。
|
||||||
- 权限策略默认允许常规工具,允许 FFF 工具;拒绝 Bash 直搜和敏感凭据路径;Git 非只读操作、包管理、外部目录、文件/系统/网络高风险操作与普通 MCP 调用先由 `pi-permission-system` 判为 `ask`。
|
- 权限策略默认允许常规工具,允许 FFF 工具;拒绝 Bash 直搜和敏感凭据路径;Git 非只读操作、包管理、外部目录、文件/系统/网络高风险操作与普通 MCP 调用先由 `pi-permission-system` 判为 `ask`。
|
||||||
- `pi-permission-auto-review` 不是独立 `tool_call` gate,而是 `pi-permission-system` authorizer chain 中名为 `auto-review` 的链路;只复核权限基线产生的 `ask`,不会重复处理已 `allow` 或已 `deny` 的请求。
|
- `pi-permission-auto-review` 不是独立 `tool_call` gate,而是 `pi-permission-system` authorizer chain 中名为 `auto-review` 的链路;只复核权限基线产生的 `ask`,不会重复处理已 `allow` 或已 `deny` 的请求。
|
||||||
- reviewer 返回 `allow` 时自动批准、返回 `deny` 时直接拒绝,配置、模型、认证、超时或响应异常时必须 `defer` 到正常人工提示。`pi-permission-system` 的 delegation envelope 继续禁止 authorizer 自动批准全部 `path` 请求;`external_directory` 只对内置 `read` 接受 reviewer 的 `allow`,写入、编辑、Bash、未知工具和其他外部访问仍转人工。
|
- reviewer 返回 `allow` 时自动批准、返回 `deny` 时直接拒绝,配置、模型、认证、超时或响应异常时必须 `defer` 到正常人工提示。`pi-permission-system` 的 delegation envelope 继续禁止 authorizer 自动批准全部 `path` 请求;`external_directory` 对内置只读路径工具(`read`、`find`、`grep`、`ls`)接受 reviewer 的 `allow`,写入、编辑、Bash、未知工具和其他外部访问仍转人工。
|
||||||
- 默认 reviewer 为 `openai-codex/codex-auto-review`、low reasoning、90 秒总重试预算和内置 Codex Guardian 风格策略;只把 active branch 中的直接用户消息与已识别结构化问答作为授权证据,assistant/tool/compaction 内容不能自行授权。
|
- 默认 reviewer 为 `openai-codex/codex-auto-review`、low reasoning、90 秒总重试预算和内置 Codex Guardian 风格策略;只把 active branch 中的直接用户消息与已识别结构化问答作为授权证据,assistant/tool/compaction 内容不能自行授权。
|
||||||
- `config/pi-permission-system.json` 必须显式配置 `authorizerChain: ["auto-review"]`,并把需要自动复核的 Git 非只读操作、包管理及其他类别声明为 `ask`;硬 `deny` 不得改成可由模型覆盖的 `ask`。
|
- `config/pi-permission-system.json` 必须显式配置 `authorizerChain: ["auto-review"]`,并把需要自动复核的 Git 非只读操作、包管理及其他类别声明为 `ask`;硬 `deny` 不得改成可由模型覆盖的 `ask`。
|
||||||
- `pi-lsp` 的 TypeScript/JavaScript 后端由根包固定依赖提供,`extensions/lsp.ts` 使用当前 Node 可执行文件直接启动包内 `typescript-language-server` CLI,不得依赖或调用 VS Code GUI;Kotlin/JDT LS 仍使用组合包部署的 `kotlin-lsp --stdio` 与 `jdtls` 配置,根包不安装这两个系统可执行文件,便捷脚本可在用户逐项明确确认后通过 Homebrew 安装 Kotlin LSP、Java 21 和 JDT LS。
|
- `pi-lsp` 的 TypeScript/JavaScript 后端由根包固定依赖提供,`extensions/lsp.ts` 使用当前 Node 可执行文件直接启动包内 `typescript-language-server` CLI,不得依赖或调用 VS Code GUI;Kotlin/JDT LS 仍使用组合包部署的 `kotlin-lsp --stdio` 与 `jdtls` 配置,根包不安装这两个系统可执行文件,便捷脚本可在用户逐项明确确认后通过 Homebrew 安装 Kotlin LSP、Java 21 和 JDT LS。
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
|
|
||||||
- 扩展入口是 `pi-permission-system/src/index.ts`,公共跨扩展服务入口是 `pi-permission-system/src/service.ts`,测试保留在该目录的 `test/`。
|
- 扩展入口是 `pi-permission-system/src/index.ts`,公共跨扩展服务入口是 `pi-permission-system/src/service.ts`,测试保留在该目录的 `test/`。
|
||||||
- 同步上游必须从明确 tag/commit 移植并记录快照;保留上游 `LICENSE`、作者和来源,不导入 `dist/`、嵌套 `.git` 或上游包目录的 `.pi` 本地状态。
|
- 同步上游必须从明确 tag/commit 移植并记录快照;保留上游 `LICENSE`、作者和来源,不导入 `dist/`、嵌套 `.git` 或上游包目录的 `.pi` 本地状态。
|
||||||
- 本地 delegation envelope 只允许内置 `read` 对 `external_directory` 接受 authorizer 的 `allow`;`write`、`edit`、Bash、未知工具、未确定 surface 以及全部 `path` ask 必须继续 `defer` 到终端人工 authority。
|
- 本地 delegation envelope 允许内置只读路径工具(`read`、`find`、`grep`、`ls`)对 `external_directory` 接受 authorizer 的 `allow`;`write`、`edit`、Bash、未知工具、未确定 surface 以及全部 `path` ask 必须继续 `defer` 到终端人工 authority。
|
||||||
- 修改 gate、authorizer chain、delegation envelope、子代理转发或公共 service 类型时,必须同步更新包内 README/架构文档和对应测试。
|
- 修改 gate、authorizer chain、delegation envelope、子代理转发或公共 service 类型时,必须同步更新包内 README/架构文档和对应测试。
|
||||||
- 根包必须通过 `file:./pi-permission-system` 提供运行时依赖,包装入口必须直接加载仓库源码;不得同时加载 npm 预编译入口或第二个 permission-system 实例。
|
- 根包必须通过 `file:./pi-permission-system` 提供运行时依赖,包装入口必须直接加载仓库源码;不得同时加载 npm 预编译入口或第二个 permission-system 实例。
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ codegraph status
|
|||||||
|
|
||||||
- `allow`:权限基线已明确允许的常规操作不会调用 reviewer。
|
- `allow`:权限基线已明确允许的常规操作不会调用 reviewer。
|
||||||
- `deny`:Bash 直搜、敏感凭据路径等硬拒绝不会交给 reviewer,也不能被其绕过。
|
- `deny`:Bash 直搜、敏感凭据路径等硬拒绝不会交给 reviewer,也不能被其绕过。
|
||||||
- `ask`:Git 非只读操作、包管理、文件/系统/网络/环境操作和普通 MCP 调用交给 reviewer;`path` 仍全部受 delegation envelope 保护。`external_directory` 仅允许内置 `read` 接受 reviewer 的 `allow`,`write`、`edit`、Bash、未知工具及其他外部目录访问仍降级为人工确认。
|
- `ask`:Git 非只读操作、包管理、文件/系统/网络/环境操作和普通 MCP 调用交给 reviewer;`path` 仍全部受 delegation envelope 保护。`external_directory` 允许内置只读路径工具(`read`、`find`、`grep`、`ls`)接受 reviewer 的 `allow`,`write`、`edit`、Bash、未知工具及其他外部目录访问仍降级为人工确认。
|
||||||
|
|
||||||
reviewer 默认使用 `openai-codex/codex-auto-review`、low reasoning 和内置 Codex Guardian 风格策略,并读取当前 session active branch 中的可信用户证据。可通过 `/permission-auto-review` 查看或调整全局/项目配置;无配置时使用源码内置默认值。
|
reviewer 默认使用 `openai-codex/codex-auto-review`、low reasoning 和内置 Codex Guardian 风格策略,并读取当前 session active branch 中的可信用户证据。可通过 `/permission-auto-review` 查看或调整全局/项目配置;无配置时使用源码内置默认值。
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ Transcript rendering uses separate 10k-token message and tool budgets with per-e
|
|||||||
- Model, authentication, timeout, provider, or response-format failures defer to the normal human prompt.
|
- Model, authentication, timeout, provider, or response-format failures defer to the normal human prompt.
|
||||||
- Unexpected internal review failures also defer to the human prompt instead of escaping into the permission gate.
|
- Unexpected internal review failures also defer to the human prompt instead of escaping into the permission gate.
|
||||||
- Three consecutive denials, or ten denials in the latest fifty reviews, open a circuit breaker until the next Pi turn.
|
- Three consecutive denials, or ten denials in the latest fifty reviews, open a circuit breaker until the next Pi turn.
|
||||||
- my-pi's maintained pi-permission-system keeps every `path` request inside the delegation envelope. For `external_directory`, only the built-in `read` tool may accept an auto-review `allow`; write, edit, bash, unknown tools, and undetermined surfaces are downgraded to the normal human prompt.
|
- my-pi's maintained pi-permission-system keeps every `path` request inside the delegation envelope. For `external_directory`, built-in read-only path tools (`read`, `find`, `grep`, `ls`) may accept an auto-review `allow`; write, edit, bash, unknown tools, and undetermined surfaces are downgraded to the normal human prompt.
|
||||||
|
|
||||||
### Diagnostics
|
### Diagnostics
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) codin
|
|||||||
> **Fork notice:** This package is a full fork of [MasuRii/pi-permission-system](https://github.com/MasuRii/pi-permission-system), published to npm as `@gotgenes/pi-permission-system`.
|
> **Fork notice:** This package is a full fork of [MasuRii/pi-permission-system](https://github.com/MasuRii/pi-permission-system), published to npm as `@gotgenes/pi-permission-system`.
|
||||||
> It has diverged substantially from upstream in config format, internal architecture, and permission model.
|
> It has diverged substantially from upstream in config format, internal architecture, and permission model.
|
||||||
>
|
>
|
||||||
> **my-pi maintenance note:** This directory was imported from upstream tag `pi-permission-system-v26.2.1` at commit `ec4fdb11343dc94f7185b113e559a4cf9f8dc035`. It is loaded from source and maintained directly by my-pi; npm `dist` output is not used. The local delegation envelope accepts an authorizer `allow` for built-in `read` access to `external_directory`, while write, edit, bash, unknown tools, and all `path` asks still defer to human confirmation.
|
> **my-pi maintenance note:** This directory was imported from upstream tag `pi-permission-system-v26.2.1` at commit `ec4fdb11343dc94f7185b113e559a4cf9f8dc035`. It is loaded from source and maintained directly by my-pi; npm `dist` output is not used. The local delegation envelope accepts an authorizer `allow` for built-in read-only path tools (`read`, `find`, `grep`, `ls`) accessing `external_directory`, while write, edit, bash, unknown tools, and all `path` asks still defer to human confirmation.
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
|
|||||||
@@ -856,7 +856,7 @@ src/
|
|||||||
│ ├── authorizer-chain.ts `composeAuthorizerChain(links, terminal, query, log)` - folds non-terminal `NamedAuthorizer` links ahead of the context-selected terminal (`defer` → next link, `allow`/`deny` → decision stamped `decidedBy: {kind: "authorizer", name, verdict, reason}` at the point the loop breaks, so a link that deferred is not credited), injecting `query` and the review-log `log` into each link; zero links returns the terminal instance (identity)
|
│ ├── authorizer-chain.ts `composeAuthorizerChain(links, terminal, query, log)` - folds non-terminal `NamedAuthorizer` links ahead of the context-selected terminal (`defer` → next link, `allow`/`deny` → decision stamped `decidedBy: {kind: "authorizer", name, verdict, reason}` at the point the loop breaks, so a link that deferred is not credited), injecting `query` and the review-log `log` into each link; zero links returns the terminal instance (identity)
|
||||||
│ ├── decision-source.ts `DecisionSource` discriminated union (`user | authorizer | rule | session_approval | yolo | infrastructure_read | unavailable | gate_error | forwarded`) + depth-bounded tolerant guard `asDecisionSource`. Constraint: each variant is self-contained (it repeats its own surface/pattern/origin/name/reason) because the forwarded response file carries no such columns to lean on; the recursive `forwarded` variant is read off disk, so its guard is depth-bounded and rejects an over-deep chain whole rather than truncating it
|
│ ├── decision-source.ts `DecisionSource` discriminated union (`user | authorizer | rule | session_approval | yolo | infrastructure_read | unavailable | gate_error | forwarded`) + depth-bounded tolerant guard `asDecisionSource`. Constraint: each variant is self-contained (it repeats its own surface/pattern/origin/name/reason) because the forwarded response file carries no such columns to lean on; the recursive `forwarded` variant is read off disk, so its guard is depth-bounded and rejects an over-deep chain whole rather than truncating it
|
||||||
│ ├── authorizer-registry.ts `AuthorizerRegistry` (+ `AuthorizerLookup`/`AuthorizerRegistrar` ISP interfaces) - name → link `authorize` map mirroring `ToolAccessExtractorRegistry`; one instance in `index.ts`, exposed cross-extension via `PermissionsService.registerAuthorizer`; throw-on-duplicate, identity-guarded disposer
|
│ ├── authorizer-registry.ts `AuthorizerRegistry` (+ `AuthorizerLookup`/`AuthorizerRegistrar` ISP interfaces) - name → link `authorize` map mirroring `ToolAccessExtractorRegistry`; one instance in `index.ts`, exposed cross-extension via `PermissionsService.registerAuthorizer`; throw-on-duplicate, identity-guarded disposer
|
||||||
│ ├── delegation-envelope.ts `encloseInDelegationEnvelope(authorize)` + `DELEGATION_EXCLUDED_SURFACES` - the bounded-delegation checkpoint (ADR 0007 §5): caps a link's `allow` on `path`, undetermined surfaces, and `external_directory` except for the built-in `read` tool; deny/defer pass through
|
│ ├── delegation-envelope.ts `encloseInDelegationEnvelope(authorize)` + `DELEGATION_EXCLUDED_SURFACES` - the bounded-delegation checkpoint (ADR 0007 §5): caps a link's `allow` on `path`, undetermined surfaces, and `external_directory` except for built-in read-only path tools (`read`, `find`, `grep`, `ls`); deny/defer pass through
|
||||||
│ ├── local-user-authorizer.ts `LocalUserAuthorizer` class - `TerminalAuthorizer` for a session with UI and the single `permissions:ui_prompt` emit site: renders a forwarded ask's provenance as a non-degraded broadcast + `(Subagent)` title, then dispatches to the inline keybind dialog (TUI) or the `select`/`input` fallback
|
│ ├── local-user-authorizer.ts `LocalUserAuthorizer` class - `TerminalAuthorizer` for a session with UI and the single `permissions:ui_prompt` emit site: renders a forwarded ask's provenance as a non-degraded broadcast + `(Subagent)` title, then dispatches to the inline keybind dialog (TUI) or the `select`/`input` fallback
|
||||||
│ ├── permission-dialog.ts Dialog option semantics + `requestPermissionDecisionFromUi` (`select`/`input` fallback) + `PermissionPromptDecision` (whose `decidedBy` is required) and `UnattributedDecision` (the same minus it); the mode dispatch lives in `permission-prompt-component.ts`
|
│ ├── permission-dialog.ts Dialog option semantics + `requestPermissionDecisionFromUi` (`select`/`input` fallback) + `PermissionPromptDecision` (whose `decidedBy` is required) and `UnattributedDecision` (the same minus it); the mode dispatch lives in `permission-prompt-component.ts`
|
||||||
│ ├── permission-prompt-decision.ts Pure decision model (`reducePrompt` + `PromptModelConfig`/`PromptViewState`) for the inline keybind dialog - hotkey arming (double-press), step transitions, reason validation; no SDK/TUI imports
|
│ ├── permission-prompt-decision.ts Pure decision model (`reducePrompt` + `PromptModelConfig`/`PromptViewState`) for the inline keybind dialog - hotkey arming (double-press), step transitions, reason validation; no SDK/TUI imports
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ Three invariants govern the chain:
|
|||||||
3. **Registration alone grants no authority.**
|
3. **Registration alone grants no authority.**
|
||||||
Installing a judge extension gives it nothing; a link decides nothing until you name it here (opt-in activation).
|
Installing a judge extension gives it nothing; a link decides nothing until you name it here (opt-in activation).
|
||||||
|
|
||||||
The chain owner caps every link with a **bounded-delegation checkpoint**. The `path` surface is always excluded. On `external_directory`, my-pi permits an `allow` only for the built-in `read` tool; write, edit, bash, extension/unknown tools, and missing tool identity are downgraded to `defer`.
|
The chain owner caps every link with a **bounded-delegation checkpoint**. The `path` surface is always excluded. On `external_directory`, my-pi permits an `allow` for the built-in read-only path tools (`read`, `find`, `grep`, `ls`); write, edit, bash, extension/unknown tools, and missing tool identity are downgraded to `defer`.
|
||||||
Deny and defer are never capped.
|
Deny and defer are never capped.
|
||||||
The gate surface remains authoritative: a `write` blocked by a `path` rule is capped even though the displayed tool name is `write`.
|
The gate surface remains authoritative: a `write` blocked by a `path` rule is capped even though the displayed tool name is `write`.
|
||||||
This holds for an ask forwarded up from a subagent exactly as it does for a local one.
|
This holds for an ask forwarded up from a subagent exactly as it does for a local one.
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
* verdict — it never turns a `defer`/`deny` into an `allow`.
|
* verdict — it never turns a `defer`/`deny` into an `allow`.
|
||||||
*
|
*
|
||||||
* The excluded set is the whole `path` surface plus `external_directory`, with
|
* The excluded set is the whole `path` surface plus `external_directory`, with
|
||||||
* one bundle-maintained exception: the built-in `read` tool may accept a link's
|
* one bundle-maintained exception: built-in read-only path tools may accept a
|
||||||
* `allow` for an external-directory ask. Mutating tools, bash, extension tools,
|
* link's `allow` for an external-directory ask. Mutating tools, bash, extension
|
||||||
* and unknown tools remain capped to the terminal human authority. A finer
|
* tools, and unknown tools remain capped to the terminal human authority. A
|
||||||
* secret-shaped-`path` exclusion remains deferred; `path` stays fully excluded.
|
* finer secret-shaped-`path` exclusion remains deferred; `path` stays fully
|
||||||
|
* excluded.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { READ_ONLY_PATH_BEARING_TOOLS } from "#src/access-intent/path-surfaces";
|
||||||
import type { Authorizer } from "./authorizer";
|
import type { Authorizer } from "./authorizer";
|
||||||
import type { PromptPermissionDetails } from "./permission-prompter";
|
import type { PromptPermissionDetails } from "./permission-prompter";
|
||||||
|
|
||||||
@@ -23,9 +25,6 @@ export const DELEGATION_EXCLUDED_SURFACES: ReadonlySet<string> = new Set([
|
|||||||
"path",
|
"path",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** Read-only external-directory access explicitly delegable in my-pi. */
|
|
||||||
const DELEGABLE_EXTERNAL_DIRECTORY_TOOLS: ReadonlySet<string> = new Set(["read"]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap a link's `authorize` so an `allow` on an excluded surface is capped to
|
* Wrap a link's `authorize` so an `allow` on an excluded surface is capped to
|
||||||
* `defer`. All other verdicts, and `allow`s on non-excluded surfaces, pass
|
* `defer`. All other verdicts, and `allow`s on non-excluded surfaces, pass
|
||||||
@@ -47,8 +46,8 @@ export function encloseInDelegationEnvelope(
|
|||||||
/**
|
/**
|
||||||
* Whether an allow verdict exceeds the delegation envelope. The gate-computed
|
* Whether an allow verdict exceeds the delegation envelope. The gate-computed
|
||||||
* surface is authoritative. `path` remains fully excluded; external-directory
|
* surface is authoritative. `path` remains fully excluded; external-directory
|
||||||
* access is excluded unless it comes from the built-in `read` tool. Unknown
|
* access is excluded unless it comes from a built-in read-only path tool.
|
||||||
* surfaces fail safe to the terminal authority.
|
* Unknown surfaces fail safe to the terminal authority.
|
||||||
*/
|
*/
|
||||||
function isExcludedSurface(details: PromptPermissionDetails): boolean {
|
function isExcludedSurface(details: PromptPermissionDetails): boolean {
|
||||||
const surface = details.accessIntent?.surface ?? details.surface ?? undefined;
|
const surface = details.accessIntent?.surface ?? details.surface ?? undefined;
|
||||||
@@ -58,7 +57,7 @@ function isExcludedSurface(details: PromptPermissionDetails): boolean {
|
|||||||
if (surface === "external_directory") {
|
if (surface === "external_directory") {
|
||||||
return !(
|
return !(
|
||||||
details.toolName !== undefined &&
|
details.toolName !== undefined &&
|
||||||
DELEGABLE_EXTERNAL_DIRECTORY_TOOLS.has(details.toolName)
|
READ_ONLY_PATH_BEARING_TOOLS.has(details.toolName)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return DELEGATION_EXCLUDED_SURFACES.has(surface);
|
return DELEGATION_EXCLUDED_SURFACES.has(surface);
|
||||||
|
|||||||
@@ -91,15 +91,20 @@ describe("encloseInDelegationEnvelope", () => {
|
|||||||
expect(verdict).toEqual({ kind: "allow" });
|
expect(verdict).toEqual({ kind: "allow" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps an allow on external_directory for the built-in read tool", async () => {
|
it.each(["read", "find", "grep", "ls"] as const)(
|
||||||
const enclosed = encloseInDelegationEnvelope(makeLink({ kind: "allow" }));
|
"keeps an allow on external_directory for the built-in read-only tool %s",
|
||||||
const verdict = await enclosed(
|
async (toolName) => {
|
||||||
makeDetails("external_directory", undefined, "read"),
|
const enclosed = encloseInDelegationEnvelope(
|
||||||
query,
|
makeLink({ kind: "allow" }),
|
||||||
log,
|
);
|
||||||
);
|
const verdict = await enclosed(
|
||||||
expect(verdict).toEqual({ kind: "allow" });
|
makeDetails("external_directory", undefined, toolName),
|
||||||
});
|
query,
|
||||||
|
log,
|
||||||
|
);
|
||||||
|
expect(verdict).toEqual({ kind: "allow" });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("never caps a deny, even on an excluded surface", async () => {
|
it("never caps a deny, even on an excluded surface", async () => {
|
||||||
const enclosed = encloseInDelegationEnvelope(
|
const enclosed = encloseInDelegationEnvelope(
|
||||||
|
|||||||
Reference in New Issue
Block a user