@gotgenes/pi-permission-system 20.5.0 → 20.7.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,30 @@ 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.7.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.6.0...pi-permission-system-v20.7.0) (2026-07-14)
9
+
10
+
11
+ ### Features
12
+
13
+ * **pi-permission-system:** add doublePressToConfirm config toggle ([bd401bf](https://github.com/gotgenes/pi-packages/commit/bd401bff0bd926313acfafe5e50b579c88e4002f))
14
+ * **pi-permission-system:** add inline permission prompt decision model ([08d6ec6](https://github.com/gotgenes/pi-packages/commit/08d6ec6b7e9abbcbf05b49579d307e3f5934cb3a))
15
+ * **pi-permission-system:** dispatch TUI permission prompts to the inline keybind dialog ([197c0f9](https://github.com/gotgenes/pi-packages/commit/197c0f91178f7010e84ed80eb3682dc27c1f737f))
16
+ * **pi-permission-system:** render inline keybind permission prompt ([7ac7dad](https://github.com/gotgenes/pi-packages/commit/7ac7dad3ef49f03e19b279056ea54d4c5a456c80))
17
+
18
+ ## [20.6.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.5.0...pi-permission-system-v20.6.0) (2026-07-13)
19
+
20
+
21
+ ### Features
22
+
23
+ * **pi-permission-system:** add resolveShellInvocation dispatch point ([5c5edc8](https://github.com/gotgenes/pi-packages/commit/5c5edc84cd46dafb5dade9fd51b2946632b72776))
24
+ * **pi-permission-system:** gate aliased shell tools through the bash stack ([2c17f2c](https://github.com/gotgenes/pi-packages/commit/2c17f2ce4b6207684f999d0bc30fe630c79934bb)), closes [#574](https://github.com/gotgenes/pi-packages/issues/574)
25
+ * **pi-permission-system:** resolve and gate aliased shell workdir ([d9b2af8](https://github.com/gotgenes/pi-packages/commit/d9b2af86107671366935242e704d1f17821db73f)), closes [#574](https://github.com/gotgenes/pi-packages/issues/574)
26
+
27
+
28
+ ### Documentation
29
+
30
+ * **pi-permission-system:** document live shellTools enforcement ([b8048ed](https://github.com/gotgenes/pi-packages/commit/b8048eddba5f8c0e9b82cf0c3e31055e7813b745))
31
+
8
32
  ## [20.5.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.4.2...pi-permission-system-v20.5.0) (2026-07-13)
9
33
 
10
34
 
package/README.md CHANGED
@@ -65,7 +65,8 @@ All permissions use one of three states:
65
65
  | `ask` | Prompts the user for confirmation via UI |
66
66
 
67
67
  When the dialog prompts, you can approve once or approve a pattern for the rest of the session.
68
- See [docs/session-approvals.md](docs/session-approvals.md) for details on session-scoped rules and pattern suggestions.
68
+ In an interactive TUI session the prompt is an inline keybind dialog — `y` approve, `s` approve for this session, `n` deny, `r` deny with a reason — where each hotkey arms and a second press confirms (configurable via `doublePressToConfirm`).
69
+ See [docs/configuration.md](docs/configuration.md#inline-permission-dialog-tui) for the hotkeys and [docs/session-approvals.md](docs/session-approvals.md) for session-scoped rules and pattern suggestions.
69
70
 
70
71
  The `path` surface is a cross-cutting gate that applies to **all** file access — Pi tools, bash commands, MCP calls, and extension tools alike.
71
72
  Extension and MCP tools that operate on paths (via `input.path`, MCP's `input.arguments.path`, or a registered access extractor) are gated by default, so a `path` deny cannot be overridden by a per-tool allow — making it the right place to protect sensitive files like `.env` or `~/.ssh/*` from every tool at once.
@@ -109,7 +110,7 @@ Project overrides global; per-agent YAML frontmatter overrides both.
109
110
 
110
111
  Within a surface map like `bash` or `mcp`, **last matching rule wins** — put broad catch-alls first and specific overrides after.
111
112
 
112
- The optional `shellTools` field records which non-`bash` tools carry shell semantics (e.g. an `exec_command` tool that replaces native `bash`), so they can be gated through the same enforcement — see [docs/configuration.md](docs/configuration.md#shelltools--gating-aliased-shell-tools).
113
+ The optional `shellTools` field records which non-`bash` tools carry shell semantics (e.g. an `exec_command` tool that replaces native `bash`), so they are gated at full parity with native `bash` — see [docs/configuration.md](docs/configuration.md#shelltools--gating-aliased-shell-tools).
113
114
 
114
115
  For the full reference — all surfaces, runtime knobs, per-agent overrides, merge semantics, and common recipes — see [docs/configuration.md](docs/configuration.md).
115
116
 
@@ -4,6 +4,7 @@
4
4
  "debugLog": false,
5
5
  "permissionReviewLog": true,
6
6
  "yoloMode": false,
7
+ "doublePressToConfirm": true,
7
8
 
8
9
  "toolInputPreviewMaxLength": 400,
9
10
  "toolTextSummaryMaxLength": 120,
@@ -30,7 +30,7 @@ Project config overrides global config; per-agent frontmatter overrides both.
30
30
  4. Project agent frontmatter
31
31
 
32
32
  The `permission` object uses deep-shallow merge: string-vs-string replaces; both-object shallow-merges pattern maps; string-vs-object the override wins entirely.
33
- Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`) use simple replacement.
33
+ Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`, `doublePressToConfirm`) use simple replacement.
34
34
 
35
35
  ## Full Example
36
36
 
@@ -42,6 +42,7 @@ Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`) use simple replace
42
42
  "debugLog": false,
43
43
  "permissionReviewLog": true,
44
44
  "yoloMode": false,
45
+ "doublePressToConfirm": true,
45
46
  "toolInputPreviewMaxLength": 400,
46
47
  "toolTextSummaryMaxLength": 120,
47
48
  "piInfrastructureReadPaths": [],
@@ -85,6 +86,7 @@ Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`) use simple replace
85
86
  | `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
86
87
  | `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl` |
87
88
  | `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled |
89
+ | `doublePressToConfirm` | `true` | Requires a confirming second press of a decision hotkey in the inline TUI dialog (see below). TUI sessions only; set to `false` for single-press. |
88
90
  | `toolInputPreviewMaxLength` | `200` | Max characters of inline JSON shown in permission prompts for tool inputs. Omit to use the default. Set to a large value to disable truncation. |
89
91
  | `toolTextSummaryMaxLength` | `80` | Max characters of inline pattern/path summaries (grep patterns, find globs, ls paths) in permission prompts. Omit to use the default. |
90
92
  | `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate. Supports `~`/`$HOME` expansion and wildcard patterns (`*`, `?`). |
@@ -92,6 +94,23 @@ Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`) use simple replace
92
94
  Both logs write to `~/.pi/agent/extensions/pi-permission-system/logs/`.
93
95
  No debug output is printed to the terminal.
94
96
 
97
+ ### Inline permission dialog (TUI)
98
+
99
+ In an interactive **TUI** session, an `ask` decision opens an inline keybind dialog with one-key shortcuts:
100
+
101
+ | Key | Action |
102
+ | --- | ----------------------------------------------------------------- |
103
+ | `y` | Approve once |
104
+ | `s` | Approve for this session |
105
+ | `n` | Deny |
106
+ | `r` | Deny with a reason (opens an inline editor; a reason is required) |
107
+
108
+ Arrow keys / `j`/`k` move the highlight, `enter` confirms the highlighted option, and `esc` denies.
109
+ With `doublePressToConfirm` enabled (the default), a letter hotkey **arms** its action and shows a `Press y again to approve.` hint; press the same key again to commit.
110
+ Set `doublePressToConfirm` to `false` to commit on the first press.
111
+
112
+ Non-TUI contexts (RPC / frontend-driven sessions) keep the single-select prompt and are unaffected by `doublePressToConfirm`.
113
+
95
114
  ### `piInfrastructureReadPaths` patterns
96
115
 
97
116
  Each entry is either a plain directory prefix or a wildcard pattern.
@@ -115,7 +134,7 @@ The native `bash` tool goes through the full bash enforcement stack: command dec
115
134
  Some extensions replace `bash` with a differently-named tool — for example [`@howaboua/pi-codex-conversion`](https://github.com/IgorWarzocha/howaboua-pi-stuff) registers `exec_command`, which carries the shell command in a `cmd` argument and an optional working directory in `workdir`.
116
135
  Without a hint, the permission system cannot tell that such a tool is really a shell, so it gates it as a generic extension tool and the bash rules never apply.
117
136
 
118
- `shellTools` records that hint.
137
+ `shellTools` records that hint, and an aliased tool is then gated at full parity with native `bash` — command decomposition, wrapper flooring, path and external-directory token gates, and `bash:` rules — with the invoked tool name preserved in the review log.
119
138
  Each key is a tool name; its value maps the tool's input arguments (the keys of the tool call's `arguments` object):
120
139
 
121
140
  ```jsonc
@@ -131,6 +150,8 @@ Each key is a tool name; its value maps the tool's input arguments (the keys of
131
150
  | `commandArgument` | yes | The tool's input argument holding the shell command string (e.g. `cmd`). |
132
151
  | `workdirArgument` | no | The tool's input argument holding the working directory (e.g. `workdir`). |
133
152
 
153
+ When `workdirArgument` is set, the tool's working directory is the base the command's relative paths resolve against, and the working directory itself is gated by `external_directory` when it falls outside the session's working directory.
154
+
134
155
  Merge semantics: `shellTools` **shallow-merges by tool name** across global → project.
135
156
  A project entry overrides a specific tool's mapping on a key collision but never drops a global entry — so adding a project-scoped alias cannot silently remove enforcement for a tool the global config already covers.
136
157
  To change a specific tool's mapping, set that tool's key at the project scope (the alias object is replaced wholesale, not deep-merged).
@@ -138,8 +159,6 @@ To change a specific tool's mapping, set that tool's key at the project scope (t
138
159
  `shellTools` only ever *tightens* enforcement and is inert when the named tool is not registered in the current session.
139
160
  Opting a project out of a shell-aliasing extension is a package-disable concern, not a `shellTools` edit.
140
161
 
141
- > **Note:** `shellTools` records the alias; the enforcement wiring that consumes it is tracked in [#574](https://github.com/gotgenes/pi-packages/issues/574).
142
-
143
162
  ---
144
163
 
145
164
  ## Policy Reference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "20.5.0",
3
+ "version": "20.7.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -25,6 +25,12 @@
25
25
  "default": false,
26
26
  "type": "boolean"
27
27
  },
28
+ "doublePressToConfirm": {
29
+ "description": "Require a confirming second press of a decision hotkey in the inline permission dialog. Applies to TUI sessions only.",
30
+ "markdownDescription": "Require a confirming second press of a decision hotkey (`y`/`s`/`n`/`r`) in the inline permission dialog before it commits — the first press arms the action and shows a `Press y again to approve.` hint.\n\nApplies to interactive **TUI** sessions only; the non-TUI (RPC/frontend) prompt keeps its single-select flow. Set to `false` to commit decisions on the first hotkey press.",
31
+ "default": true,
32
+ "type": "boolean"
33
+ },
28
34
  "toolInputPreviewMaxLength": {
29
35
  "description": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts. Omit to use the default (200). Set to a large value to disable truncation.",
30
36
  "markdownDescription": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts.\n\nOmit to use the default (200). Set to a large value (e.g. `10000`) to effectively disable truncation and see the full input.",
@@ -101,20 +101,57 @@ export class BashPathResolver {
101
101
  constructor(
102
102
  private readonly normalizer: PathNormalizer,
103
103
  private readonly isPromotablePathToken: PathRuleTokenMatcher = NO_PROMOTION,
104
+ private readonly workdir?: string,
104
105
  ) {}
105
106
 
106
107
  /**
107
108
  * Resolve a parsed bash program's path references into its external-path and
108
109
  * rule-candidate slices, walking the AST exactly once.
110
+ *
111
+ * When a `workdir` is set (an aliased shell tool's working directory, #574),
112
+ * it seeds the initial effective base — as if the program were prefixed with
113
+ * `cd <workdir>` — so relative tokens resolve against it, and the `workdir`
114
+ * itself is added to the external paths when it resolves outside the cwd.
115
+ * Containment is always measured against the session cwd baked into the
116
+ * normalizer, so a `workdir` outside the cwd does not widen the sandbox.
109
117
  */
110
118
  resolve(rootNode: TSNode): ResolvedBashPaths {
111
- const candidates = this.collectPathCandidates(rootNode);
119
+ const initialBase =
120
+ this.workdir === undefined
121
+ ? CWD_BASE
122
+ : this.deriveBaseFromCdTarget(CWD_BASE, this.workdir);
123
+ const candidates = this.collectPathCandidates(rootNode, initialBase);
112
124
  return {
113
- externalPaths: this.projectExternalPaths(candidates),
125
+ externalPaths: this.withWorkdirExternal(
126
+ this.projectExternalPaths(candidates),
127
+ ),
114
128
  ruleCandidates: this.projectRuleCandidates(candidates),
115
129
  };
116
130
  }
117
131
 
132
+ /**
133
+ * Prepend the `workdir`'s own {@link AccessPath} to the external paths when it
134
+ * resolves outside the cwd. A real `cd /etc` flags `/etc` via its argument
135
+ * token; the seeded base carries no such token, so it is added explicitly and
136
+ * deduplicated against the command's own external tokens (#574).
137
+ */
138
+ private withWorkdirExternal(
139
+ tokenExternals: readonly AccessPath[],
140
+ ): AccessPath[] {
141
+ if (this.workdir === undefined) return [...tokenExternals];
142
+ const wdPath = this.normalizer.forBashToken(this.workdir);
143
+ const canonical = wdPath.boundaryValue();
144
+ const isExternal = canonical
145
+ ? this.normalizer.isBoundaryOutsideWorkingDirectory(canonical)
146
+ : true;
147
+ if (!isExternal) return [...tokenExternals];
148
+ const key = canonical || wdPath.value();
149
+ const alreadyPresent = tokenExternals.some(
150
+ (p) => (p.boundaryValue() || p.value()) === key,
151
+ );
152
+ return alreadyPresent ? [...tokenExternals] : [wdPath, ...tokenExternals];
153
+ }
154
+
118
155
  // ── AST walk — collect PathCandidates ──────────────────────────────────
119
156
 
120
157
  /**
@@ -129,9 +166,12 @@ export class BashPathResolver {
129
166
  * inherit the enclosing base without folding their own `cd`s (a conservative
130
167
  * first tier).
131
168
  */
132
- private collectPathCandidates(rootNode: TSNode): PathCandidate[] {
169
+ private collectPathCandidates(
170
+ rootNode: TSNode,
171
+ initialBase: EffectiveBase,
172
+ ): PathCandidate[] {
133
173
  const out: PathCandidate[] = [];
134
- this.walkForCandidates(rootNode, CWD_BASE, out);
174
+ this.walkForCandidates(rootNode, initialBase, out);
135
175
  return out;
136
176
  }
137
177
 
@@ -330,6 +370,25 @@ export class BashPathResolver {
330
370
  if (extractCommandName(commandNode) !== "cd") return base;
331
371
  const target = cdLiteralTarget(commandNode);
332
372
  if (target === null) return UNKNOWN_BASE;
373
+ return this.deriveBaseFromCdTarget(base, target);
374
+ }
375
+
376
+ /**
377
+ * Fold a literal `cd`/working-directory target string into the effective
378
+ * base, delegating the platform/MSYS interpretation to the
379
+ * {@link PathNormalizer}. Owns only the base-folding state:
380
+ *
381
+ * - `absolute` → a fresh known base (recovers from an earlier unknown base).
382
+ * - `unknown` → the base becomes conservatively unknown.
383
+ * - `relative` → join into a known base, or stay unknown if already unknown.
384
+ *
385
+ * Shared by {@link foldCd} (inline `cd` commands) and the initial-base seed
386
+ * (an aliased shell tool's `workdir`, an implicit leading `cd <workdir>`).
387
+ */
388
+ private deriveBaseFromCdTarget(
389
+ base: EffectiveBase,
390
+ target: string,
391
+ ): EffectiveBase {
333
392
  const interpreted = this.normalizer.interpretBashCdTarget(target);
334
393
  switch (interpreted.kind) {
335
394
  case "absolute":
@@ -25,6 +25,7 @@ export type { BashCommand, BashPathRuleCandidate };
25
25
  */
26
26
  export class BashProgram {
27
27
  private constructor(
28
+ private readonly sourceCommand: string,
28
29
  private readonly commandUnits: readonly BashCommand[],
29
30
  private readonly resolvedExternalPaths: readonly AccessPath[],
30
31
  private readonly resolvedRuleCandidates: readonly BashPathRuleCandidate[],
@@ -44,22 +45,30 @@ export class BashProgram {
44
45
  * specific `path` deny/ask rule (#509). Defaults to promoting nothing, so
45
46
  * callers that only read `externalPaths()` (e.g. `bash-path-extractor.ts`)
46
47
  * are unaffected.
48
+ *
49
+ * `options.workdir`, when supplied (an aliased shell tool's working directory,
50
+ * #574), seeds the initial effective base — as if the command were prefixed
51
+ * with `cd <workdir>` — so relative tokens resolve against it, and the workdir
52
+ * itself is flagged as external when it resolves outside the cwd.
47
53
  */
48
54
  static async parse(
49
55
  command: string,
50
56
  normalizer: PathNormalizer,
51
57
  isPromotablePathToken?: PathRuleTokenMatcher,
58
+ options?: { workdir?: string },
52
59
  ): Promise<BashProgram> {
53
60
  const parser = await getParser();
54
61
  const tree = parser.parse(command);
55
- if (!tree) return new BashProgram([], [], []);
62
+ if (!tree) return new BashProgram(command, [], [], []);
56
63
 
57
64
  try {
58
65
  const { externalPaths, ruleCandidates } = new BashPathResolver(
59
66
  normalizer,
60
67
  isPromotablePathToken,
68
+ options?.workdir,
61
69
  ).resolve(tree.rootNode);
62
70
  return new BashProgram(
71
+ command,
63
72
  collectCommands(tree.rootNode),
64
73
  externalPaths,
65
74
  ruleCandidates,
@@ -69,6 +78,18 @@ export class BashProgram {
69
78
  }
70
79
  }
71
80
 
81
+ /**
82
+ * The source command string this program was parsed from.
83
+ *
84
+ * The bash gates read this for prompts, logs, and decision display instead of
85
+ * receiving the command as a separate parameter — the program is the parsed
86
+ * command, so it owns its source text (#574). Native `bash` and an aliased
87
+ * shell tool alike reach the gates through this single collaborator.
88
+ */
89
+ commandText(): string {
90
+ return this.sourceCommand;
91
+ }
92
+
72
93
  /**
73
94
  * The top-level command-pattern units of the chain, in source order.
74
95
  *
@@ -1,3 +1,5 @@
1
+ import type { ShellToolsConfig } from "#src/config-schema";
2
+ import { getNonEmptyString, toRecord } from "#src/value-guards";
1
3
  import { PATH_BEARING_TOOLS } from "./path-surfaces";
2
4
 
3
5
  /**
@@ -39,6 +41,59 @@ export function classifyToolKind(toolName: string): ToolKind {
39
41
  return "extension";
40
42
  }
41
43
 
44
+ /** A shell invocation's effective command and optional working directory. */
45
+ export interface ShellInvocation {
46
+ /** The shell command string to decompose and gate. */
47
+ command: string;
48
+ /** The working directory the command runs in, if the tool projects one. */
49
+ workdir: string | undefined;
50
+ }
51
+
52
+ /**
53
+ * Decide whether a tool invocation carries shell semantics, and if so extract
54
+ * its command and working directory.
55
+ *
56
+ * Native `bash` and any tool recorded in `shellTools` both yield a
57
+ * {@link ShellInvocation}; every other tool yields `null`. This is the single
58
+ * dispatch point the bash gate pipeline consults instead of re-deriving
59
+ * `toolName === "bash"` and reading `input.command`, so an aliased shell tool
60
+ * (e.g. `@howaboua/pi-codex-conversion`'s `exec_command`) is routed through the
61
+ * same bash enforcement stack as native `bash` (#574).
62
+ *
63
+ * The command and workdir are read through {@link getNonEmptyString} (trimmed,
64
+ * empty → `""`/`undefined`), matching the pipeline's existing native-bash
65
+ * extraction. Kept separate from {@link classifyToolKind} because it needs
66
+ * config (the alias map) and returns a richer product than a {@link ToolKind}
67
+ * string — `classifyToolKind` stays AccessPath-free and config-free.
68
+ */
69
+ export function resolveShellInvocation(
70
+ toolName: string,
71
+ input: unknown,
72
+ aliases: ShellToolsConfig | undefined,
73
+ ): ShellInvocation | null {
74
+ const name = toolName.trim();
75
+ const record = toRecord(input);
76
+
77
+ if (name === "bash") {
78
+ return {
79
+ command: getNonEmptyString(record.command) ?? "",
80
+ workdir: undefined,
81
+ };
82
+ }
83
+
84
+ const alias = aliases?.[name];
85
+ if (alias) {
86
+ return {
87
+ command: getNonEmptyString(record[alias.commandArgument]) ?? "",
88
+ workdir: alias.workdirArgument
89
+ ? (getNonEmptyString(record[alias.workdirArgument]) ?? undefined)
90
+ : undefined,
91
+ };
92
+ }
93
+
94
+ return null;
95
+ }
96
+
42
97
  /** The resolved-check fields that decide MCP-ness. */
43
98
  interface McpKindFields {
44
99
  toolName: string;
@@ -1,8 +1,9 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
2
3
  import type {
3
- PermissionPromptDecision,
4
- requestPermissionDecisionFromUi,
5
- } from "#src/authority/permission-dialog";
4
+ PromptPreferences,
5
+ requestPermissionDecision,
6
+ } from "#src/authority/permission-prompt-component";
6
7
  import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
7
8
  import type { PermissionEventBus } from "#src/permission-events";
8
9
  import type { DebugReviewLogger } from "#src/session-logger";
@@ -33,8 +34,10 @@ export interface AuthorizerSelectionDeps {
33
34
  detection: SubagentDetector;
34
35
  /** Event bus used by `LocalUserAuthorizer` for the `permissions:ui_prompt` broadcast. */
35
36
  events: PermissionEventBus;
37
+ /** Read live at prompt time; threaded into `LocalUserAuthorizer`. */
38
+ getPromptPreferences: () => PromptPreferences;
36
39
  /** Injected for testability; production callers pass the real function. */
37
- requestPermissionDecisionFromUi: typeof requestPermissionDecisionFromUi;
40
+ requestPermissionDecision: typeof requestPermissionDecision;
38
41
  /** Forwarding directory `ParentAuthorizer` reads/writes request and response files under. */
39
42
  forwardingDir: string;
40
43
  /** In-process subagent session registry for forwarding target resolution. */
@@ -57,8 +60,10 @@ export function selectAuthorizer(
57
60
  if (ctx.hasUI) {
58
61
  return new LocalUserAuthorizer({
59
62
  ui: ctx.ui,
63
+ mode: ctx.mode,
60
64
  events: deps.events,
61
- requestPermissionDecisionFromUi: deps.requestPermissionDecisionFromUi,
65
+ getPromptPreferences: deps.getPromptPreferences,
66
+ requestPermissionDecision: deps.requestPermissionDecision,
62
67
  });
63
68
  }
64
69
  if (deps.detection.isSubagent(ctx)) {
@@ -1,9 +1,13 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
1
2
  import type {
2
- PermissionDecisionUi,
3
3
  PermissionPromptDecision,
4
4
  RequestPermissionOptions,
5
- requestPermissionDecisionFromUi,
6
5
  } from "#src/authority/permission-dialog";
6
+ import type {
7
+ PermissionPromptUi,
8
+ PromptPreferences,
9
+ requestPermissionDecision,
10
+ } from "#src/authority/permission-prompt-component";
7
11
  import { buildForwardedScopeLabels } from "#src/pattern-suggest";
8
12
  import {
9
13
  emitUiPromptEvent,
@@ -15,12 +19,16 @@ import type { PromptPermissionDetails } from "./permission-prompter";
15
19
 
16
20
  /** Dependencies required by {@link LocalUserAuthorizer}. */
17
21
  export interface LocalUserAuthorizerDeps {
18
- /** The active session's UI surface. */
19
- ui: PermissionDecisionUi;
22
+ /** The active session's UI surface (select/input plus the inline `custom` dialog). */
23
+ ui: PermissionPromptUi;
24
+ /** The session run mode; the dispatcher renders the inline dialog only in `"tui"`. */
25
+ mode: ExtensionContext["mode"];
20
26
  /** Event bus used for the `permissions:ui_prompt` broadcast. */
21
27
  events: PermissionEventBus;
28
+ /** Read live at prompt time so a settings-modal toggle takes effect on the next prompt. */
29
+ getPromptPreferences: () => PromptPreferences;
22
30
  /** Injected for testability; production callers pass the real function. */
23
- requestPermissionDecisionFromUi: typeof requestPermissionDecisionFromUi;
31
+ requestPermissionDecision: typeof requestPermissionDecision;
24
32
  }
25
33
 
26
34
  /**
@@ -41,8 +49,13 @@ export class LocalUserAuthorizer implements Authorizer {
41
49
  ): Promise<PermissionPromptDecision> {
42
50
  const uiPrompt = buildUiPrompt(details);
43
51
  emitUiPromptEvent(this.deps.events, uiPrompt);
44
- return this.deps.requestPermissionDecisionFromUi(
45
- this.deps.ui,
52
+ return this.deps.requestPermissionDecision(
53
+ {
54
+ mode: this.deps.mode,
55
+ ui: this.deps.ui,
56
+ doublePressToConfirm:
57
+ this.deps.getPromptPreferences().doublePressToConfirm,
58
+ },
46
59
  details.forwarding
47
60
  ? "Permission Required (Subagent)"
48
61
  : "Permission Required",