mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: delegate external read-only tools to auto-review
This commit is contained in:
@@ -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`.
|
||||
> 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
│ ├── 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
|
||||
│ ├── 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
|
||||
│ ├── 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
|
||||
|
||||
@@ -227,7 +227,7 @@ Three invariants govern the chain:
|
||||
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).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
* verdict — it never turns a `defer`/`deny` into an `allow`.
|
||||
*
|
||||
* 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
|
||||
* `allow` for an external-directory ask. Mutating tools, bash, extension tools,
|
||||
* and unknown tools remain capped to the terminal human authority. A finer
|
||||
* secret-shaped-`path` exclusion remains deferred; `path` stays fully excluded.
|
||||
* one bundle-maintained exception: built-in read-only path tools may accept a
|
||||
* link's `allow` for an external-directory ask. Mutating tools, bash, extension
|
||||
* tools, and unknown tools remain capped to the terminal human authority. A
|
||||
* 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 { PromptPermissionDetails } from "./permission-prompter";
|
||||
|
||||
@@ -23,9 +25,6 @@ export const DELEGATION_EXCLUDED_SURFACES: ReadonlySet<string> = new Set([
|
||||
"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
|
||||
* `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
|
||||
* surface is authoritative. `path` remains fully excluded; external-directory
|
||||
* access is excluded unless it comes from the built-in `read` tool. Unknown
|
||||
* surfaces fail safe to the terminal authority.
|
||||
* access is excluded unless it comes from a built-in read-only path tool.
|
||||
* Unknown surfaces fail safe to the terminal authority.
|
||||
*/
|
||||
function isExcludedSurface(details: PromptPermissionDetails): boolean {
|
||||
const surface = details.accessIntent?.surface ?? details.surface ?? undefined;
|
||||
@@ -58,7 +57,7 @@ function isExcludedSurface(details: PromptPermissionDetails): boolean {
|
||||
if (surface === "external_directory") {
|
||||
return !(
|
||||
details.toolName !== undefined &&
|
||||
DELEGABLE_EXTERNAL_DIRECTORY_TOOLS.has(details.toolName)
|
||||
READ_ONLY_PATH_BEARING_TOOLS.has(details.toolName)
|
||||
);
|
||||
}
|
||||
return DELEGATION_EXCLUDED_SURFACES.has(surface);
|
||||
|
||||
@@ -91,15 +91,20 @@ describe("encloseInDelegationEnvelope", () => {
|
||||
expect(verdict).toEqual({ kind: "allow" });
|
||||
});
|
||||
|
||||
it("keeps an allow on external_directory for the built-in read tool", async () => {
|
||||
const enclosed = encloseInDelegationEnvelope(makeLink({ kind: "allow" }));
|
||||
const verdict = await enclosed(
|
||||
makeDetails("external_directory", undefined, "read"),
|
||||
query,
|
||||
log,
|
||||
);
|
||||
expect(verdict).toEqual({ kind: "allow" });
|
||||
});
|
||||
it.each(["read", "find", "grep", "ls"] as const)(
|
||||
"keeps an allow on external_directory for the built-in read-only tool %s",
|
||||
async (toolName) => {
|
||||
const enclosed = encloseInDelegationEnvelope(
|
||||
makeLink({ kind: "allow" }),
|
||||
);
|
||||
const verdict = await enclosed(
|
||||
makeDetails("external_directory", undefined, toolName),
|
||||
query,
|
||||
log,
|
||||
);
|
||||
expect(verdict).toEqual({ kind: "allow" });
|
||||
},
|
||||
);
|
||||
|
||||
it("never caps a deny, even on an excluded surface", async () => {
|
||||
const enclosed = encloseInDelegationEnvelope(
|
||||
|
||||
Reference in New Issue
Block a user