@hank-warren/pi-auto-permissions 0.15.0 → 0.16.1

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
@@ -1,5 +1,28 @@
1
1
  # @hank-warren/pi-auto-permissions
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - d0c46a5: Two fixes found by the live canary:
8
+
9
+ - `/auto-permissions` → Recent denials → "Allow on retry" dispatched the retry while the settings menu was still open; the approval prompt then stacked on top of it and the composer stopped submitting until Pi was restarted. The menu now closes before the retry is sent.
10
+ - With `ui.placement: "toolRow"`, a review waiting its turn behind another review rendered as `✗ blocked`; it now shows the queued frame.
11
+
12
+ Internally `index.ts` was split into `classify.ts`, `guardian-reviewer.ts`, `review-display.ts`, `session-overrides.ts`, `settings-command.ts`, `prompt-select.ts` and `review-scope.ts`; the decision pipeline, prompts, log formats, session entries and events are unchanged and are now pinned by an extension-level test suite.
13
+
14
+ ## 0.16.0
15
+
16
+ ### Minor Changes
17
+
18
+ - c079c51: Serialize guardian-backed reviews, show a queued command instead of a blank gap, and stage `request_override`.
19
+
20
+ Two guarded commands issued in the same assistant turn used to open two reviewer conversations at once and race for the same widget and bash review row, letting the second verdict land against the first command's display. Guardian reviews now run one at a time behind a FIFO queue. Denies, convention blocks, trusted commands, and already-approved execution never enter it, so the ordinary path is unchanged.
21
+
22
+ Because the critical section spans the human approval prompt, a queued command now renders a `queued behind another review` row as soon as it starts waiting, and the wait is cancellable: `Esc` or a reviewer-lifecycle reset releases it immediately instead of stranding it behind a review it is no longer waiting for. An aborted waiter never lets the command behind it overtake the review still in progress.
23
+
24
+ `request_override` activates only after a convention denial in an interactive session, so a session that is never blocked does not carry its schema. The tool stays registered for transcript replay.
25
+
3
26
  ## 0.15.0
4
27
 
5
28
  ### Minor Changes
package/approval-pulse.ts CHANGED
@@ -2,7 +2,7 @@ import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
2
2
 
3
3
  export const APPROVAL_PULSE_INTERVAL_MS = 600;
4
4
 
5
- export interface ApprovalPulseTheme {
5
+ interface ApprovalPulseTheme {
6
6
  fg(role: "warning" | "dim", text: string): string;
7
7
  bold(text: string): string;
8
8
  }
package/classify.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type { AutoPermissionsConfig } from "./config.js";
2
+ import { ALL_SHELL_GATE, findGates, type Gate } from "./gates.js";
3
+
4
+ /**
5
+ * What the ruleset says about one command, before any guardian is involved.
6
+ *
7
+ * `pass` means nothing gated it: no rule matched, or the only rules that did
8
+ * are ones this project trusts or has already granted an override for.
9
+ */
10
+ export type CommandClassification =
11
+ | { kind: "pass" }
12
+ | { kind: "deny" | "convention" | "review"; gate: Gate };
13
+
14
+ /**
15
+ * Rules that matched and still apply.
16
+ *
17
+ * Deny rules are hard policy boundaries: a project-scoped trusted-ops file
18
+ * must not be able to lift one, or a checked-in file could disarm the
19
+ * circuit breaker. Only guarded and convention matches honor the bypass.
20
+ */
21
+ export function untrustedMatches(
22
+ command: string,
23
+ config: AutoPermissionsConfig,
24
+ trustedGroups: ReadonlySet<string>,
25
+ ): Gate[] {
26
+ return findGates(command, config.rules).filter(
27
+ (gate) => gate.level === "deny" || !trustedGroups.has(gate.group),
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Decide a command's fate from the ruleset alone.
33
+ *
34
+ * Level priority across all matches, never first-match: a convention or
35
+ * guarded rule earlier in config order must not shadow a deny rule later.
36
+ */
37
+ export function classifyCommand(
38
+ command: string,
39
+ config: AutoPermissionsConfig,
40
+ trustedGroups: ReadonlySet<string>,
41
+ allowedConventionCommands: ReadonlySet<string>,
42
+ ): CommandClassification {
43
+ const ruleMatches = findGates(command, config.rules);
44
+ const matches = ruleMatches.filter(
45
+ (gate) => gate.level === "deny" || !trustedGroups.has(gate.group),
46
+ );
47
+
48
+ const deny = matches.find((gate) => gate.level === "deny");
49
+ if (deny) return { kind: "deny", gate: deny };
50
+
51
+ const convention = matches.find((gate) => gate.level === "convention");
52
+ if (convention && !allowedConventionCommands.has(command)) {
53
+ return { kind: "convention", gate: convention };
54
+ }
55
+
56
+ let gate = matches.find((candidate) => candidate.level === "guarded");
57
+ // With reviewAllShell on, a command no rule names at all still gets a
58
+ // guardian review under the generic gate. "No rule" is judged before the
59
+ // trusted-groups filter: a command whose matching group the project
60
+ // trusts was explicitly waved through, not left unnamed, and must not be
61
+ // re-captured by the blanket gate.
62
+ if (!gate && !ruleMatches.length && config.reviewAllShell && !trustedGroups.has(ALL_SHELL_GATE.group)) {
63
+ gate = ALL_SHELL_GATE;
64
+ }
65
+ return gate ? { kind: "review", gate } : { kind: "pass" };
66
+ }
package/config-writer.ts CHANGED
@@ -23,7 +23,7 @@ export interface ReviewerBlock {
23
23
  timeoutMs: number;
24
24
  }
25
25
 
26
- export interface ConfigPatch {
26
+ interface ConfigPatch {
27
27
  enabled?: boolean;
28
28
  reviewer?: ReviewerBlock;
29
29
  /**
package/config.ts CHANGED
@@ -194,7 +194,7 @@ function compileRule(value: unknown, index: number): Gate {
194
194
  }
195
195
 
196
196
  /** The literal a `rules` array uses to splice the built-in ruleset in place. */
197
- export const DEFAULT_RULES_TOKEN = "$defaults";
197
+ const DEFAULT_RULES_TOKEN = "$defaults";
198
198
 
199
199
  /**
200
200
  * Compile an authored `rules` array, splicing `defaults` wherever the literal
@@ -248,7 +248,7 @@ function resolvePrompt(
248
248
  * `loop-context.ts` names `PI_LOOP_ACTIVE`: no import, no dependency, and no
249
249
  * behaviour at all when pi-loop is not installed.
250
250
  */
251
- export const DEFAULT_USER_MESSAGE_TYPES = ["loop-objective"] as const;
251
+ const DEFAULT_USER_MESSAGE_TYPES = ["loop-objective"] as const;
252
252
 
253
253
  const EVIDENCE_PRUNING_DEFAULTS = {
254
254
  toolRecordMaxChars: 500,
package/denial-log.ts CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { dirname } from "node:path";
4
4
 
5
5
  /** Rotate once the sidecar grows past this size; one previous generation is kept. */
6
- export const DENIAL_LOG_ROTATE_BYTES = 16 * 1024 * 1024;
6
+ const DENIAL_LOG_ROTATE_BYTES = 16 * 1024 * 1024;
7
7
 
8
8
  /** The two ways a review can end without the command running. */
9
9
  export type DenialVerdict = "revise" | "block";
package/evaluation-log.ts CHANGED
@@ -65,7 +65,7 @@ export interface PromptEvaluationRecord {
65
65
  * command and compact evidence for offline labelling against the same
66
66
  * criteria as prompted records.
67
67
  */
68
- export interface PrefilterEvaluationRecord {
68
+ interface PrefilterEvaluationRecord {
69
69
  version: 2;
70
70
  timestamp: string;
71
71
  sessionId: string;
@@ -83,7 +83,7 @@ export interface PrefilterEvaluationRecord {
83
83
  decisionSource: "prefilter";
84
84
  }
85
85
 
86
- export type EvaluationRecord = PromptEvaluationRecord | PrefilterEvaluationRecord;
86
+ type EvaluationRecord = PromptEvaluationRecord | PrefilterEvaluationRecord;
87
87
 
88
88
  export function classifyPromptChoice(choice: string | undefined): PromptChoiceClassification | undefined {
89
89
  if (choice === "Allow") return { allowsExecution: true };