mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
refactor(permission-system): simplify policy and autoreview flow
This commit is contained in:
@@ -9,7 +9,6 @@ import {
|
||||
} from "./authorizer";
|
||||
import { composeAuthorizerChain } from "./authorizer-chain";
|
||||
import type { AuthorizerLookup } from "./authorizer-registry";
|
||||
import { encloseInDelegationEnvelope } from "./delegation-envelope";
|
||||
import type {
|
||||
PermissionPrompterApi,
|
||||
PromptPermissionDetails,
|
||||
@@ -112,8 +111,8 @@ export class AuthorizerSelection
|
||||
* Resolve the operator's `authorizerChain` names to registered links, in
|
||||
* config order (ADR 0007 invariant 1). An unregistered name is skipped with a
|
||||
* warning (invariant 2 — more prompting, never less); each resolved link is
|
||||
* wrapped in the bounded-delegation envelope so an `allow` on an excluded
|
||||
* surface cannot exceed the operator's policy.
|
||||
* Authorizer verdicts are used directly. A configured authorizer may approve
|
||||
* any ordinary-policy `ask`, including the `path` surface.
|
||||
*
|
||||
* The resolved names are recorded against the ask before any link runs — a
|
||||
* link that defers decides nothing and would otherwise leave no evidence it
|
||||
@@ -136,7 +135,7 @@ export class AuthorizerSelection
|
||||
continue;
|
||||
}
|
||||
resolved.push(name);
|
||||
links.push({ name, authorize: encloseInDelegationEnvelope(authorize) });
|
||||
links.push({ name, authorize });
|
||||
}
|
||||
if (resolved.length > 0) {
|
||||
this.deps.logger.review("authorizer_chain_resolved", {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* The bounded-delegation enforcement checkpoint (ADR 0007 §5).
|
||||
*
|
||||
* The chain owner caps every registered link's verdict so a buggy or over-eager
|
||||
* external judge can never exceed the operator's policy: a link's `allow` on an
|
||||
* excluded surface is downgraded to `defer`, letting the `ask` fall through to
|
||||
* the terminal (a prompt) instead. The checkpoint only ever *tightens* a
|
||||
* 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: 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";
|
||||
|
||||
/** Surfaces on which a link may never grant an `allow` (ADR 0007 §5). */
|
||||
export const DELEGATION_EXCLUDED_SURFACES: ReadonlySet<string> = new Set([
|
||||
"external_directory",
|
||||
"path",
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* through unchanged. `details`, the injected `query`, and the review-log `log`
|
||||
* are forwarded as-is.
|
||||
*/
|
||||
export function encloseInDelegationEnvelope(
|
||||
authorize: Authorizer["authorize"],
|
||||
): Authorizer["authorize"] {
|
||||
return async (details, query, log) => {
|
||||
const verdict = await authorize(details, query, log);
|
||||
if (verdict.kind === "allow" && isExcludedSurface(details)) {
|
||||
return { kind: "defer" };
|
||||
}
|
||||
return verdict;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
if (surface === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (surface === "external_directory") {
|
||||
return !(
|
||||
details.toolName !== undefined &&
|
||||
READ_ONLY_PATH_BEARING_TOOLS.has(details.toolName)
|
||||
);
|
||||
}
|
||||
return DELEGATION_EXCLUDED_SURFACES.has(surface);
|
||||
}
|
||||
@@ -89,9 +89,8 @@ export interface ForwardedRequestServerDeps {
|
||||
* judges a forwarded ask on the same evidence as a local one (ADR 0008; #635).
|
||||
*
|
||||
* The display `surface` and the fact `surface` are distinct and both belong
|
||||
* here: the former is the child's tool name (what the UI shows), the latter the
|
||||
* gate surface the rule fired on (what the bounded-delegation checkpoint
|
||||
* excludes on).
|
||||
* here: the former is the child's tool name (what the UI shows), while the
|
||||
* latter preserves the gate surface as authorizer evidence.
|
||||
*/
|
||||
function buildForwardedAskDetails(
|
||||
request: ForwardedPermissionRequest,
|
||||
@@ -113,9 +112,8 @@ function buildForwardedAskDetails(
|
||||
...(request.sessionApproval
|
||||
? { sessionApproval: request.sessionApproval }
|
||||
: {}),
|
||||
// Absent for a version-skew request that carried no intent — which the
|
||||
// delegation envelope reads as "surface undetermined" and fail-safes to
|
||||
// excluded, so absence must stay absence rather than become `undefined`.
|
||||
// Absent for a version-skew request that carried no access intent; preserve
|
||||
// that absence rather than manufacturing authorizer evidence.
|
||||
...(request.accessIntent
|
||||
? { accessIntent: toAccessFacts(request.accessIntent) }
|
||||
: {}),
|
||||
|
||||
@@ -88,7 +88,7 @@ export interface ForwardedSessionApproval {
|
||||
* (`docs/decisions/0002-path-values-string-boundary.md`).
|
||||
*/
|
||||
export interface ForwardedAccessFacts {
|
||||
/** Gate surface: `"path"`, `"external_directory"`, `"bash"`, a tool name, or a skill name. */
|
||||
/** Gate surface: `"path"`, `"bash"`, a tool name, or a skill name. */
|
||||
surface: string;
|
||||
/**
|
||||
* The child-fixed match set. Path surface: `AccessPath.matchValues()`
|
||||
|
||||
Reference in New Issue
Block a user