mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Cross-extension service accessor backed by `Symbol.for()` on `globalThis`.
|
||||
*
|
||||
* `Symbol.for()` is process-global by spec, so it survives jiti's per-extension
|
||||
* module isolation (`moduleCache: false`). A consumer doing
|
||||
* `import("@gotgenes/pi-permission-system")` gets a fresh module copy, but
|
||||
* `getPermissionsService()` reads from the same `globalThis` slot the provider
|
||||
* wrote to — enabling direct, synchronous, type-safe function calls.
|
||||
*
|
||||
* Best practice: call `getPermissionsService()` per use rather than caching the
|
||||
* reference — this ensures resilience across `/reload` and load-order edge cases.
|
||||
*/
|
||||
|
||||
import type { Authorizer } from "./authority/authorizer";
|
||||
import type { ToolAccessExtractor } from "./tool-access-extractor-registry";
|
||||
import type { ToolInputFormatter } from "./tool-input-formatter-registry";
|
||||
import type { PermissionCheckResult, PermissionState } from "./types";
|
||||
|
||||
export type {
|
||||
Authorizer,
|
||||
AuthorizerVerdict,
|
||||
} from "./authority/authorizer";
|
||||
|
||||
/**
|
||||
* The narrow review-log seam handed to a chain link at `authorize` time
|
||||
* (ADR 0007 §3, same injection pattern as {@link PermissionQuery}).
|
||||
*
|
||||
* A link uses it to record a positive decision trail to the permission review
|
||||
* log — `review` for the durable, default-on audit entry (one per handled
|
||||
* ask), `debug` for verbose or short-circuit detail gated behind the
|
||||
* `debugLog` toggle. The session's own logger is passed straight through, so a
|
||||
* link's entries land in the same `pi-permission-system-permission-review.jsonl`
|
||||
* as the gate decisions, keying to a gate entry by `requestId`.
|
||||
*/
|
||||
export interface AuthorizerLog {
|
||||
review(event: string, details?: Record<string, unknown>): void;
|
||||
debug(event: string, details?: Record<string, unknown>): void;
|
||||
}
|
||||
export type { PromptPermissionDetails } from "./authority/permission-prompter";
|
||||
export type {
|
||||
ForwardedPromptContext,
|
||||
PermissionDecisionEvent,
|
||||
PermissionsReadyEvent,
|
||||
PermissionUiPromptEvent,
|
||||
PermissionUiPromptSource,
|
||||
} from "./permission-events";
|
||||
export {
|
||||
PERMISSIONS_DECISION_CHANNEL,
|
||||
PERMISSIONS_READY_CHANNEL,
|
||||
PERMISSIONS_UI_PROMPT_CHANNEL,
|
||||
} from "./permission-events";
|
||||
// The declaration bundle already inlines these through `PromptPermissionDetails`
|
||||
// and `PermissionUiPromptEvent`; the named exports are what a consumer needs to
|
||||
// annotate a variable of their own.
|
||||
export type {
|
||||
PromptAnnotation,
|
||||
PromptEvidence,
|
||||
PromptPayload,
|
||||
PromptPayloadKind,
|
||||
PromptRequester,
|
||||
PromptRequestFacts,
|
||||
} from "./presentation/prompt-payload";
|
||||
export type { PermissionCheckResult, PermissionState, ToolInputFormatter };
|
||||
|
||||
/** Process-global key for the service slot. */
|
||||
const SERVICE_KEY = Symbol.for("@gotgenes/pi-permission-system:service");
|
||||
|
||||
/**
|
||||
* The narrow, read-only projection of {@link PermissionsService}: answer a
|
||||
* policy query for a surface, and report a tool-level state. This is the
|
||||
* capability an Authorizer chain link is handed (ISP) — it never sees the
|
||||
* registration surface.
|
||||
*/
|
||||
export interface PermissionQuery {
|
||||
/**
|
||||
* Query the permission policy for a surface and value.
|
||||
*
|
||||
* @param surface - Permission surface: "bash", "read", "mcp", "skill",
|
||||
* "external_directory", etc.
|
||||
* @param value - The value to evaluate: command string, tool name, skill
|
||||
* name, or path. Omit or pass `undefined` for a
|
||||
* surface-level query.
|
||||
* @param agentName - Optional agent name for per-agent policy resolution.
|
||||
* @returns Full check result including state, matched pattern, and origin.
|
||||
*/
|
||||
checkPermission(
|
||||
surface: string,
|
||||
value?: string,
|
||||
agentName?: string,
|
||||
): PermissionCheckResult;
|
||||
|
||||
/**
|
||||
* Query the tool-level permission state for pre-filtering tools before
|
||||
* creating a child session.
|
||||
*
|
||||
* Returns `"deny"` | `"allow"` | `"ask"` based on the composed policy.
|
||||
* Does not consider command-level rules (e.g. per-bash-command patterns) —
|
||||
* use `checkPermission` for runtime invocation gates.
|
||||
*
|
||||
* @param toolName - Tool name (e.g. `"bash"`, `"read"`, `"my-extension:tool"`).
|
||||
* @param agentName - Optional agent name for per-agent policy resolution.
|
||||
*/
|
||||
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface exposed to other extensions via `getPermissionsService()`.
|
||||
*
|
||||
* `checkPermission` takes a surface + optional value + optional agent name,
|
||||
* and delegates to `PermissionManager.checkPermission()` with current session
|
||||
* rules internally.
|
||||
*/
|
||||
export interface PermissionsService extends PermissionQuery {
|
||||
/**
|
||||
* Register a custom preview formatter for a specific tool name.
|
||||
*
|
||||
* The formatter is consulted first inside `ToolPreviewFormatter.formatToolInputForPrompt`;
|
||||
* returning `undefined` falls through to the built-in switch (and ultimately
|
||||
* the JSON default).
|
||||
*
|
||||
* Only one formatter may be registered per tool name — a second call for the
|
||||
* same name throws. The returned disposer unregisters the formatter.
|
||||
*
|
||||
* @param toolName - Exact tool name to register for (e.g. `"mcp"`, `"my-server:run"`).
|
||||
* @param formatter - Receives the raw `input` record; return a string to use
|
||||
* as the prompt preview, or `undefined` to decline.
|
||||
*/
|
||||
registerToolInputFormatter(
|
||||
toolName: string,
|
||||
formatter: ToolInputFormatter,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Register a custom access-intent extractor for a specific tool name.
|
||||
*
|
||||
* The extractor declares the filesystem path a tool will access so the
|
||||
* cross-cutting `path` and `external_directory` gates can see it. Use it for
|
||||
* tools whose path lives under a non-standard key — built-in file tools and
|
||||
* any tool exposing `input.path` (plus MCP via `input.arguments.path`) are
|
||||
* already covered by convention without registration.
|
||||
*
|
||||
* The extractor receives the raw `input` record and returns the path string,
|
||||
* or `undefined` to decline. Only one extractor may be registered per tool
|
||||
* name — a second call for the same name throws. The returned disposer
|
||||
* unregisters the extractor.
|
||||
*
|
||||
* @param toolName - Exact tool name to register for (e.g. `"ffgrep"`).
|
||||
* @param extractor - Receives the raw `input` record; return the path string,
|
||||
* or `undefined` to decline.
|
||||
*/
|
||||
registerToolAccessExtractor(
|
||||
toolName: string,
|
||||
extractor: ToolAccessExtractor,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Register a named live-authority chain link (ADR 0007 §4).
|
||||
*
|
||||
* A link reviews an `ask` and returns `allow` / `deny` (with an optional
|
||||
* teaching `reason`) / `defer`. It is handed a narrow, session-scoped
|
||||
* {@link PermissionQuery} at `authorize` time so it can query the
|
||||
* deterministic engine at gate parity. Register from a `permissions:ready`
|
||||
* handler so registration is robust to load order and survives `/reload`.
|
||||
*
|
||||
* Registration alone grants **no authority**: the link decides nothing until
|
||||
* the operator names it in the `authorizerChain` config (opt-in activation),
|
||||
* and the chain owner caps every verdict with the bounded-delegation
|
||||
* checkpoint (an `allow` on an excluded surface downgrades to `defer`). Only
|
||||
* one link may be registered per name — a second call for the same name
|
||||
* throws. The returned disposer unregisters the link.
|
||||
*
|
||||
* @param name - Operator-facing link name referenced from `authorizerChain`.
|
||||
* @param authorize - The link's decision callback
|
||||
* (`(details, query, log) => verdict`); `log` is an
|
||||
* {@link AuthorizerLog} for recording a decision trail to
|
||||
* the shared permission review log.
|
||||
*/
|
||||
registerAuthorizer(
|
||||
name: string,
|
||||
authorize: Authorizer["authorize"],
|
||||
): () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a `PermissionsService` on `globalThis` so other extensions can
|
||||
* retrieve it via `getPermissionsService()`.
|
||||
*
|
||||
* Called at `session_start` by the top-level (parent) instance only — an
|
||||
* in-process subagent child skips publishing so it cannot clobber the parent's
|
||||
* service. Overwrites any previously published service, which keeps `/reload`
|
||||
* working: a reloaded parent re-publishes its fresh service.
|
||||
*/
|
||||
export function publishPermissionsService(service: PermissionsService): void {
|
||||
(globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the published `PermissionsService`, or `undefined` if the
|
||||
* permission-system extension has not loaded (or has been unloaded).
|
||||
*/
|
||||
export function getPermissionsService(): PermissionsService | undefined {
|
||||
return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
|
||||
| PermissionsService
|
||||
| undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove `service` from `globalThis`, but only when the current slot still
|
||||
* holds it (identity compare-and-delete).
|
||||
*
|
||||
* Called during `session_shutdown` to avoid stale references after the
|
||||
* extension is torn down. Scoping the delete to the publishing instance keeps
|
||||
* two cases correct:
|
||||
*
|
||||
* - An in-process subagent child never published the parent's service, so its
|
||||
* shutdown is a no-op and the parent's slot survives.
|
||||
* - A superseded `/reload` generation no longer owns the slot, so its late
|
||||
* shutdown cannot wipe the new generation's freshly published service.
|
||||
*/
|
||||
export function unpublishPermissionsService(service: PermissionsService): void {
|
||||
if (getPermissionsService() !== service) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Symbol-keyed global property; Map.delete() is not applicable
|
||||
delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
|
||||
}
|
||||
Reference in New Issue
Block a user