@gotgenes/pi-permission-system 20.9.0 → 20.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [20.10.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.9.1...pi-permission-system-v20.10.0) (2026-07-21)
9
+
10
+
11
+ ### Features
12
+
13
+ * **pi-permission-system:** thread a review-log seam into the authorizer chain ([b086474](https://github.com/gotgenes/pi-packages/commit/b086474e91c79b02632ee76bbaa3e72e39f29e40))
14
+
15
+ ## [20.9.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.9.0...pi-permission-system-v20.9.1) (2026-07-20)
16
+
17
+
18
+ ### Documentation
19
+
20
+ * **pi-permission-system:** cite pi-permission-model-judge as a registerAuthorizer example ([6bc1e67](https://github.com/gotgenes/pi-packages/commit/6bc1e6710ea70c7d95b87d77fb4e744f0b81e614))
21
+
8
22
  ## [20.9.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.8.0...pi-permission-system-v20.9.0) (2026-07-19)
9
23
 
10
24
 
package/README.md CHANGED
@@ -114,6 +114,7 @@ The optional `shellTools` field records which non-`bash` tools carry shell seman
114
114
 
115
115
  The optional `authorizerChain` field names registered case-by-case decision links (e.g. a light model judge) to consult when a request lands on `ask`, ahead of the interactive prompt.
116
116
  A downstream extension registers a link via `getPermissionsService().registerAuthorizer(name, authorize)`; it decides nothing until you name it here (opt-in), config order fixes the chain order, and the chain owner caps any link's `allow` on `external_directory`/`path` to keep it within your policy — see [docs/configuration.md](docs/configuration.md#authorizer-chain--case-by-case-decision-links).
117
+ [`@gotgenes/pi-permission-model-judge`](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-permission-model-judge) is a first-party reference implementation of such a link — a deny-first reviewer that auto-denies mistyped out-of-directory paths.
117
118
 
118
119
  For the full reference — all surfaces, runtime knobs, per-agent overrides, merge semantics, and common recipes — see [docs/configuration.md](docs/configuration.md).
119
120
 
package/dist/public.d.ts CHANGED
@@ -217,10 +217,12 @@ type AuthorizerVerdict = {
217
217
  * decide it or defer to the next link (ADR 0007). The chain injects a narrow,
218
218
  * session-scoped {@link PermissionQuery} at `authorize` time (§3), so a link
219
219
  * queries the deterministic engine at gate parity rather than reaching for the
220
- * cross-extension service via `Symbol.for()`.
220
+ * cross-extension service via `Symbol.for()`. It also injects an
221
+ * {@link AuthorizerLog} so a link can record its decision trail to the shared
222
+ * permission review log (same §3 injection pattern).
221
223
  */
222
224
  interface Authorizer {
223
- authorize(details: PromptPermissionDetails, query: PermissionQuery): Promise<AuthorizerVerdict>;
225
+ authorize(details: PromptPermissionDetails, query: PermissionQuery, log: AuthorizerLog): Promise<AuthorizerVerdict>;
224
226
  }
225
227
 
226
228
  /**
@@ -257,6 +259,22 @@ type ToolInputFormatter = (input: Record<string, unknown>) => string | undefined
257
259
  * reference — this ensures resilience across `/reload` and load-order edge cases.
258
260
  */
259
261
 
262
+ /**
263
+ * The narrow review-log seam handed to a chain link at `authorize` time
264
+ * (ADR 0007 §3, same injection pattern as {@link PermissionQuery}).
265
+ *
266
+ * A link uses it to record a positive decision trail to the permission review
267
+ * log — `review` for the durable, default-on audit entry (one per handled
268
+ * ask), `debug` for verbose or short-circuit detail gated behind the
269
+ * `debugLog` toggle. The session's own logger is passed straight through, so a
270
+ * link's entries land in the same `pi-permission-system-permission-review.jsonl`
271
+ * as the gate decisions, keying to a gate entry by `requestId`.
272
+ */
273
+ interface AuthorizerLog {
274
+ review(event: string, details?: Record<string, unknown>): void;
275
+ debug(event: string, details?: Record<string, unknown>): void;
276
+ }
277
+
260
278
  /**
261
279
  * The narrow, read-only projection of {@link PermissionsService}: answer a
262
280
  * policy query for a surface, and report a tool-level state. This is the
@@ -348,7 +366,10 @@ interface PermissionsService extends PermissionQuery {
348
366
  * throws. The returned disposer unregisters the link.
349
367
  *
350
368
  * @param name - Operator-facing link name referenced from `authorizerChain`.
351
- * @param authorize - The link's decision callback (`(details, query) => verdict`).
369
+ * @param authorize - The link's decision callback
370
+ * (`(details, query, log) => verdict`); `log` is an
371
+ * {@link AuthorizerLog} for recording a decision trail to
372
+ * the shared permission review log.
352
373
  */
353
374
  registerAuthorizer(name: string, authorize: Authorizer["authorize"]): () => void;
354
375
  }
@@ -383,4 +404,4 @@ declare function getPermissionsService(): PermissionsService | undefined;
383
404
  declare function unpublishPermissionsService(service: PermissionsService): void;
384
405
 
385
406
  export { PERMISSIONS_DECISION_CHANNEL, PERMISSIONS_READY_CHANNEL, PERMISSIONS_UI_PROMPT_CHANNEL, getPermissionsService, publishPermissionsService, unpublishPermissionsService };
386
- export type { Authorizer, AuthorizerVerdict, ForwardedPromptContext, PermissionCheckResult, PermissionDecisionEvent, PermissionQuery, PermissionState, PermissionUiPromptEvent, PermissionUiPromptSource, PermissionsReadyEvent, PermissionsService, PromptPermissionDetails, ToolInputFormatter };
407
+ export type { Authorizer, AuthorizerLog, AuthorizerVerdict, ForwardedPromptContext, PermissionCheckResult, PermissionDecisionEvent, PermissionQuery, PermissionState, PermissionUiPromptEvent, PermissionUiPromptSource, PermissionsReadyEvent, PermissionsService, PromptPermissionDetails, ToolInputFormatter };
@@ -194,6 +194,7 @@ Deny and defer are never capped.
194
194
 
195
195
  Extension authors: register a link from a `permissions:ready` handler via `getPermissionsService().registerAuthorizer(name, authorize)`; the callback receives the ask details and a narrow, session-scoped `PermissionQuery` (`checkPermission` / `getToolPermission`) so it can consult the deterministic engine at gate parity.
196
196
  Registration returns a disposer, and only one link may hold a given name.
197
+ For a complete working example, see [`@gotgenes/pi-permission-model-judge`](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-permission-model-judge): it registers a `model-judge` link on `permissions:ready` that reviews `external_directory` asks and auto-denies mistyped paths with a corrective reason.
197
198
 
198
199
  ---
199
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "20.9.0",
3
+ "version": "20.10.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,4 +1,4 @@
1
- import type { PermissionQuery } from "#src/service";
1
+ import type { AuthorizerLog, PermissionQuery } from "#src/service";
2
2
  import type {
3
3
  Authorizer,
4
4
  AuthorizerVerdict,
@@ -16,16 +16,18 @@ import { createDeniedPermissionDecision } from "./permission-dialog";
16
16
  * (returns a full decision), so a deferring link cannot occupy the terminal
17
17
  * slot.
18
18
  *
19
- * Each link is handed the session-scoped `query` at `authorize` time (ADR 0007
20
- * §3) so it queries the deterministic engine at gate parity; the terminal never
21
- * queries. With zero links the composed chain **is** the terminal instance
22
- * (identity), so behavior is byte-identical to the pre-chain spine the
23
- * empty-links case that ships until a link registers.
19
+ * Each link is handed the session-scoped `query` and the review-log `log` at
20
+ * `authorize` time (ADR 0007 §3) so it queries the deterministic engine at gate
21
+ * parity and records its decision trail; the terminal receives neither. With
22
+ * zero links the composed chain **is** the terminal instance (identity), so
23
+ * behavior is byte-identical to the pre-chain spine the empty-links case that
24
+ * ships until a link registers.
24
25
  */
25
26
  export function composeAuthorizerChain(
26
27
  links: readonly Authorizer[],
27
28
  terminal: TerminalAuthorizer,
28
29
  query: PermissionQuery,
30
+ log: AuthorizerLog,
29
31
  ): TerminalAuthorizer {
30
32
  if (links.length === 0) {
31
33
  return terminal;
@@ -33,7 +35,7 @@ export function composeAuthorizerChain(
33
35
  return {
34
36
  async authorize(details) {
35
37
  for (const link of links) {
36
- const verdict = await link.authorize(details, query);
38
+ const verdict = await link.authorize(details, query, log);
37
39
  const decision = decideFromVerdict(verdict);
38
40
  if (decision) {
39
41
  return decision;
@@ -126,6 +126,7 @@ export class AuthorizerSelection
126
126
  this.resolveConfiguredLinks(),
127
127
  this.terminal,
128
128
  this.deps.getPermissionQuery(),
129
+ this.deps.logger,
129
130
  );
130
131
  return this.deps.prompter.prompt(chain, details);
131
132
  }
@@ -6,7 +6,7 @@ import type {
6
6
  } from "#src/authority/permission-prompt-component";
7
7
  import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
8
8
  import type { PermissionEventBus } from "#src/permission-events";
9
- import type { PermissionQuery } from "#src/service";
9
+ import type { AuthorizerLog, PermissionQuery } from "#src/service";
10
10
  import type { DebugReviewLogger } from "#src/session-logger";
11
11
  import { ParentAuthorizer } from "./approval-escalator";
12
12
  import { DenyingAuthorizer } from "./denying-authorizer";
@@ -29,12 +29,15 @@ export type AuthorizerVerdict =
29
29
  * decide it or defer to the next link (ADR 0007). The chain injects a narrow,
30
30
  * session-scoped {@link PermissionQuery} at `authorize` time (§3), so a link
31
31
  * queries the deterministic engine at gate parity rather than reaching for the
32
- * cross-extension service via `Symbol.for()`.
32
+ * cross-extension service via `Symbol.for()`. It also injects an
33
+ * {@link AuthorizerLog} so a link can record its decision trail to the shared
34
+ * permission review log (same §3 injection pattern).
33
35
  */
34
36
  export interface Authorizer {
35
37
  authorize(
36
38
  details: PromptPermissionDetails,
37
39
  query: PermissionQuery,
40
+ log: AuthorizerLog,
38
41
  ): Promise<AuthorizerVerdict>;
39
42
  }
40
43
 
@@ -26,13 +26,14 @@ export const DELEGATION_EXCLUDED_SURFACES: ReadonlySet<string> = new Set([
26
26
  /**
27
27
  * Wrap a link's `authorize` so an `allow` on an excluded surface is capped to
28
28
  * `defer`. All other verdicts, and `allow`s on non-excluded surfaces, pass
29
- * through unchanged. `details` and the injected `query` are forwarded as-is.
29
+ * through unchanged. `details`, the injected `query`, and the review-log `log`
30
+ * are forwarded as-is.
30
31
  */
31
32
  export function encloseInDelegationEnvelope(
32
33
  authorize: Authorizer["authorize"],
33
34
  ): Authorizer["authorize"] {
34
- return async (details, query) => {
35
- const verdict = await authorize(details, query);
35
+ return async (details, query, log) => {
36
+ const verdict = await authorize(details, query, log);
36
37
  if (verdict.kind === "allow" && isExcludedSurface(details)) {
37
38
  return { kind: "defer" };
38
39
  }
package/src/service.ts CHANGED
@@ -20,6 +20,22 @@ export type {
20
20
  Authorizer,
21
21
  AuthorizerVerdict,
22
22
  } from "./authority/authorizer";
23
+
24
+ /**
25
+ * The narrow review-log seam handed to a chain link at `authorize` time
26
+ * (ADR 0007 §3, same injection pattern as {@link PermissionQuery}).
27
+ *
28
+ * A link uses it to record a positive decision trail to the permission review
29
+ * log — `review` for the durable, default-on audit entry (one per handled
30
+ * ask), `debug` for verbose or short-circuit detail gated behind the
31
+ * `debugLog` toggle. The session's own logger is passed straight through, so a
32
+ * link's entries land in the same `pi-permission-system-permission-review.jsonl`
33
+ * as the gate decisions, keying to a gate entry by `requestId`.
34
+ */
35
+ export interface AuthorizerLog {
36
+ review(event: string, details?: Record<string, unknown>): void;
37
+ debug(event: string, details?: Record<string, unknown>): void;
38
+ }
23
39
  export type { PromptPermissionDetails } from "./authority/permission-prompter";
24
40
  export type {
25
41
  ForwardedPromptContext,
@@ -143,7 +159,10 @@ export interface PermissionsService extends PermissionQuery {
143
159
  * throws. The returned disposer unregisters the link.
144
160
  *
145
161
  * @param name - Operator-facing link name referenced from `authorizerChain`.
146
- * @param authorize - The link's decision callback (`(details, query) => verdict`).
162
+ * @param authorize - The link's decision callback
163
+ * (`(details, query, log) => verdict`); `log` is an
164
+ * {@link AuthorizerLog} for recording a decision trail to
165
+ * the shared permission review log.
147
166
  */
148
167
  registerAuthorizer(
149
168
  name: string,