@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 +24 -0
- package/README.md +3 -2
- package/config/config.example.json +1 -0
- package/docs/configuration.md +23 -4
- package/package.json +1 -1
- package/schemas/permissions.schema.json +6 -0
- package/src/access-intent/bash/bash-path-resolver.ts +63 -4
- package/src/access-intent/bash/program.ts +22 -1
- package/src/access-intent/tool-kind.ts +55 -0
- package/src/authority/authorizer.ts +10 -5
- package/src/authority/local-user-authorizer.ts +20 -7
- package/src/authority/permission-prompt-component.ts +273 -0
- package/src/authority/permission-prompt-decision.ts +258 -0
- package/src/config-loader.ts +9 -1
- package/src/config-modal.ts +15 -0
- package/src/config-schema.ts +7 -0
- package/src/extension-config.ts +4 -0
- package/src/handlers/gates/bash-external-directory.ts +6 -7
- package/src/handlers/gates/bash-path.ts +7 -8
- package/src/handlers/gates/tool-call-gate-pipeline.ts +51 -24
- package/src/handlers/gates/tool.ts +18 -8
- package/src/index.ts +5 -2
- package/src/permission-session.ts +11 -0
|
@@ -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 (
|
|
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 {
|
|
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
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 (
|
|
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:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 {
|
|
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
|
-
|
|
26
|
+
toolName: string,
|
|
24
27
|
check: PermissionCheckResult,
|
|
25
28
|
accessPath?: AccessPath,
|
|
26
29
|
): string {
|
|
27
|
-
switch (classifyToolKind(
|
|
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
|
-
|
|
58
|
-
deriveSuggestionValue(
|
|
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:
|
|
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:
|
|
108
|
+
surface: gateSurface,
|
|
99
109
|
value: deriveDecisionValue(
|
|
100
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
/**
|