@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.
@@ -4,7 +4,6 @@ import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
4
  import { SessionApproval } from "#src/session-approval";
5
5
  import { deriveApprovalPattern } from "#src/session-rules";
6
6
  import type { PermissionCheckResult } from "#src/types";
7
- import { getNonEmptyString, toRecord } from "#src/value-guards";
8
7
  import { pickMostRestrictive } from "./candidate-check";
9
8
  import type { GateResult } from "./descriptor";
10
9
  import { formatPathAskPrompt } from "./path";
@@ -20,22 +19,22 @@ import type { ToolCallContext } from "./types";
20
19
  * restrictive result, while prompts, logs, and session approvals use the raw
21
20
  * token.
22
21
  *
23
- * Returns `null` when the gate does not apply (tool is not bash, no command,
24
- * no tokens extracted, or all tokens evaluate to `allow`).
22
+ * Returns `null` when the gate does not apply (not a shell invocation, no
23
+ * command, no tokens extracted, or all tokens evaluate to `allow`).
25
24
  * Returns a `GateBypass` when all tokens are session-covered.
26
25
  * Returns a `GateDescriptor` for the most restrictive token needing a check.
26
+ *
27
+ * The shell command (native `bash` or an aliased shell tool) is read from the
28
+ * injected `BashProgram`, which owns the source text it was parsed from, so
29
+ * this gate does not re-derive the input field name (#574).
27
30
  */
28
31
  export function describeBashPathGate(
29
32
  tcc: ToolCallContext,
30
33
  bashProgram: BashProgram | null,
31
34
  resolver: ScopedPermissionResolver,
32
35
  ): GateResult {
33
- if (tcc.toolName !== "bash") return null;
34
-
35
- const command = getNonEmptyString(toRecord(tcc.input).command);
36
- if (!command) return null;
37
-
38
36
  if (!bashProgram) return null;
37
+ const command = bashProgram.commandText();
39
38
 
40
39
  const candidates = bashProgram.pathRuleCandidates();
41
40
  if (candidates.length === 0) return null;
@@ -1,7 +1,11 @@
1
1
  import type { AccessPath } from "#src/access-intent/access-path";
2
2
  import { BashProgram } from "#src/access-intent/bash/program";
3
3
  import { getPathBearingToolPath } from "#src/access-intent/tool-input-path";
4
- import { classifyToolKind } from "#src/access-intent/tool-kind";
4
+ import {
5
+ resolveShellInvocation,
6
+ type ShellInvocation,
7
+ } from "#src/access-intent/tool-kind";
8
+ import type { ShellToolsConfig } from "#src/config-schema";
5
9
  import type { PathNormalizer } from "#src/path-normalizer";
6
10
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
7
11
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
@@ -12,7 +16,6 @@ import {
12
16
  type ToolPreviewFormatterOptions,
13
17
  } from "#src/tool-preview-formatter";
14
18
  import type { PathRuleTokenMatcher, PermissionCheckResult } from "#src/types";
15
- import { getNonEmptyString, toRecord } from "#src/value-guards";
16
19
  import { resolveBashCommandCheck } from "./bash-command";
17
20
  import { describeBashExternalDirectoryGate } from "./bash-external-directory";
18
21
  import { describeBashPathGate } from "./bash-path";
@@ -43,6 +46,12 @@ export interface ToolCallGateInputs {
43
46
  getToolPreviewLimits(): ToolPreviewFormatterOptions;
44
47
  /** The session's path normalizer (platform + cwd baked in). */
45
48
  getPathNormalizer(): PathNormalizer;
49
+ /**
50
+ * The configured shell-tool aliases (`shellTools`), or `undefined` when none
51
+ * are set. Consulted by {@link resolveShellInvocation} so an aliased shell
52
+ * tool is gated through the bash stack at parity with native `bash` (#574).
53
+ */
54
+ getShellToolAliases(): ShellToolsConfig | undefined;
46
55
  /**
47
56
  * Predicate deciding whether a bare bash token should be promoted into the
48
57
  * `path` rule-candidate surface (#509), scoped to the given agent.
@@ -73,20 +82,24 @@ export class ToolCallGatePipeline {
73
82
  tcc: ToolCallContext,
74
83
  runner: GateRunner,
75
84
  ): Promise<GateOutcome> {
76
- // Parse the bash command exactly once per evaluate; the three bash gates
77
- // share this single BashProgram instead of each re-parsing (#308).
78
- const command = getNonEmptyString(toRecord(tcc.input).command);
85
+ // Resolve the shell invocation once: native `bash` and any tool recorded in
86
+ // `shellTools` both yield a command (+ optional workdir); every other tool
87
+ // yields null (#574). The three bash gates then share the single BashProgram
88
+ // parsed from that command instead of each re-parsing (#308).
89
+ const shell = resolveShellInvocation(
90
+ tcc.toolName,
91
+ tcc.input,
92
+ this.inputs.getShellToolAliases(),
93
+ );
79
94
  const normalizer = this.inputs.getPathNormalizer();
80
- const bashProgram =
81
- classifyToolKind(tcc.toolName) === "bash" && command
82
- ? await BashProgram.parse(
83
- command,
84
- normalizer,
85
- this.inputs.getPromotablePathTokenMatcher(
86
- tcc.agentName ?? undefined,
87
- ),
88
- )
89
- : null;
95
+ const bashProgram = shell?.command
96
+ ? await BashProgram.parse(
97
+ shell.command,
98
+ normalizer,
99
+ this.inputs.getPromotablePathTokenMatcher(tcc.agentName ?? undefined),
100
+ { workdir: shell.workdir },
101
+ )
102
+ : null;
90
103
 
91
104
  const formatter = new ToolPreviewFormatter(
92
105
  this.inputs.getToolPreviewLimits(),
@@ -115,8 +128,8 @@ export class ToolCallGatePipeline {
115
128
  () => {
116
129
  const { toolCheck, accessPath } = this.resolvePerToolCheck(
117
130
  tcc,
131
+ shell,
118
132
  bashProgram,
119
- command,
120
133
  normalizer,
121
134
  );
122
135
  const toolDescriptor = describeToolGate(
@@ -124,6 +137,7 @@ export class ToolCallGatePipeline {
124
137
  toolCheck,
125
138
  formatter,
126
139
  accessPath,
140
+ shell,
127
141
  );
128
142
  toolDescriptor.preCheck = toolCheck;
129
143
  return toolDescriptor;
@@ -156,18 +170,31 @@ export class ToolCallGatePipeline {
156
170
  */
157
171
  private resolvePerToolCheck(
158
172
  tcc: ToolCallContext,
173
+ shell: ShellInvocation | null,
159
174
  bashProgram: BashProgram | null,
160
- command: string | null,
161
175
  normalizer: PathNormalizer,
162
176
  ): { toolCheck: PermissionCheckResult; accessPath?: AccessPath } {
163
- if (classifyToolKind(tcc.toolName) === "bash" && bashProgram) {
177
+ if (shell) {
178
+ if (bashProgram) {
179
+ return {
180
+ toolCheck: resolveBashCommandCheck(
181
+ bashProgram.commandText(),
182
+ bashProgram.commands(),
183
+ tcc.agentName ?? undefined,
184
+ this.resolver,
185
+ ),
186
+ };
187
+ }
188
+ // A shell invocation whose command did not parse (e.g. empty) still
189
+ // resolves on the `bash` surface, so an aliased tool never falls through
190
+ // to its own extension-tool surface.
164
191
  return {
165
- toolCheck: resolveBashCommandCheck(
166
- command ?? "",
167
- bashProgram.commands(),
168
- tcc.agentName ?? undefined,
169
- this.resolver,
170
- ),
192
+ toolCheck: this.resolver.resolve({
193
+ kind: "tool",
194
+ surface: "bash",
195
+ input: { command: shell.command },
196
+ agentName: tcc.agentName ?? undefined,
197
+ }),
171
198
  };
172
199
  }
173
200
 
@@ -1,7 +1,10 @@
1
1
  import type { AccessPath } from "#src/access-intent/access-path";
2
2
  import { PATH_BEARING_TOOLS } from "#src/access-intent/path-surfaces";
3
3
  import { getPathBearingToolPath } from "#src/access-intent/tool-input-path";
4
- import { classifyToolKind } from "#src/access-intent/tool-kind";
4
+ import {
5
+ classifyToolKind,
6
+ type ShellInvocation,
7
+ } from "#src/access-intent/tool-kind";
5
8
  import { suggestSessionPattern } from "#src/pattern-suggest";
6
9
  import { formatAskPrompt } from "#src/permission-prompts";
7
10
  import { SessionApproval } from "#src/session-approval";
@@ -20,11 +23,11 @@ import type { ToolCallContext } from "./types";
20
23
  * others (or a path-bearing tool with no path) → catch-all wildcard.
21
24
  */
22
25
  function deriveSuggestionValue(
23
- tcc: ToolCallContext,
26
+ toolName: string,
24
27
  check: PermissionCheckResult,
25
28
  accessPath?: AccessPath,
26
29
  ): string {
27
- switch (classifyToolKind(tcc.toolName)) {
30
+ switch (classifyToolKind(toolName)) {
28
31
  case "bash":
29
32
  return check.command ?? "";
30
33
  case "mcp":
@@ -45,7 +48,14 @@ export function describeToolGate(
45
48
  check: PermissionCheckResult,
46
49
  formatter: ToolPreviewFormatter,
47
50
  accessPath?: AccessPath,
51
+ shell?: ShellInvocation | null,
48
52
  ): GateDescriptor {
53
+ // A shell invocation (native `bash` or an aliased shell tool) is gated on the
54
+ // `bash` surface — its session rule, decision value, and suggestion are
55
+ // bash-shaped — while the invoked tool name is preserved in the prompt and
56
+ // review log so a user sees which tool actually ran (#574).
57
+ const gateSurface = shell ? "bash" : tcc.toolName;
58
+
49
59
  const permissionLogContext = formatter.getPermissionLogContext(
50
60
  check,
51
61
  tcc.input,
@@ -54,8 +64,8 @@ export function describeToolGate(
54
64
 
55
65
  // Compute session approval suggestion for the "for this session" option.
56
66
  const suggestion = suggestSessionPattern(
57
- tcc.toolName,
58
- deriveSuggestionValue(tcc, check, accessPath),
67
+ gateSurface,
68
+ deriveSuggestionValue(gateSurface, check, accessPath),
59
69
  );
60
70
 
61
71
  const askMessage = formatAskPrompt(
@@ -66,7 +76,7 @@ export function describeToolGate(
66
76
  );
67
77
 
68
78
  return {
69
- surface: tcc.toolName,
79
+ surface: gateSurface,
70
80
  input: tcc.input,
71
81
  denialContext: {
72
82
  kind: "tool",
@@ -95,9 +105,9 @@ export function describeToolGate(
95
105
  ...permissionLogContext,
96
106
  },
97
107
  decision: {
98
- surface: tcc.toolName,
108
+ surface: gateSurface,
99
109
  value: deriveDecisionValue(
100
- tcc.toolName,
110
+ gateSurface,
101
111
  check,
102
112
  getPathBearingToolPath(tcc.toolName, tcc.input) ?? undefined,
103
113
  ),
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  type ServingPolicy,
9
9
  } from "./authority/forwarded-request-server";
10
10
  import { ForwardingManager } from "./authority/forwarding-manager";
11
- import { requestPermissionDecisionFromUi } from "./authority/permission-dialog";
11
+ import { requestPermissionDecision } from "./authority/permission-prompt-component";
12
12
  import { PermissionPrompter } from "./authority/permission-prompter";
13
13
  import { SubagentDetection } from "./authority/subagent-detection";
14
14
  import { subscribeSubagentLifecycle } from "./authority/subagent-lifecycle-events";
@@ -100,7 +100,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
100
100
  const authorizerSelection = new AuthorizerSelection({
101
101
  detection: subagentDetection,
102
102
  events: pi.events,
103
- requestPermissionDecisionFromUi,
103
+ getPromptPreferences: () => ({
104
+ doublePressToConfirm: configStore.current().doublePressToConfirm,
105
+ }),
106
+ requestPermissionDecision,
104
107
  forwardingDir: paths.forwardingDir,
105
108
  registry: subagentRegistry,
106
109
  logger,
@@ -5,6 +5,7 @@ import {
5
5
  getActiveAgentNameFromSystemPrompt,
6
6
  } from "./active-agent";
7
7
  import type { AuthorizerSelectionLifecycle } from "./authority/authorizer-selection";
8
+ import type { ShellToolsConfig } from "./config-schema";
8
9
  import type { SessionConfigStore } from "./config-store";
9
10
  import type { PermissionSystemExtensionConfig } from "./extension-config";
10
11
  import type { ExtensionPaths } from "./extension-paths";
@@ -205,6 +206,16 @@ export class PermissionSession implements ToolCallGateInputs {
205
206
  return resolveToolPreviewLimits(this.config);
206
207
  }
207
208
 
209
+ /**
210
+ * The configured shell-tool aliases (`shellTools`), mapping a non-`bash` tool
211
+ * name to the input arguments holding its command and optional working
212
+ * directory. `undefined` when no aliases are configured. Consumed by the
213
+ * gate pipeline's {@link resolveShellInvocation} consult (#574).
214
+ */
215
+ getShellToolAliases(): ShellToolsConfig | undefined {
216
+ return this.config.shellTools;
217
+ }
218
+
208
219
  // ── Path normalization ────────────────────────────────────────────────
209
220
 
210
221
  /**