@gotgenes/pi-permission-system 26.2.1 → 26.2.2
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 +7 -0
- package/package.json +1 -1
- package/src/handlers/gates/bash-external-directory.ts +5 -2
- package/src/handlers/gates/bash-path.ts +3 -2
- package/src/handlers/gates/external-directory.ts +1 -2
- package/src/handlers/gates/path.ts +1 -2
- package/src/handlers/gates/tool-call-gate-pipeline.ts +19 -10
- package/src/handlers/gates/tool.ts +31 -14
- package/src/path/approval-pattern.ts +27 -0
- package/src/path/path-flavor.ts +22 -2
- package/src/path-normalizer.ts +15 -0
- package/src/pattern-suggest.ts +27 -18
- package/src/session-rules.ts +0 -31
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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
|
+
## [26.2.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v26.2.1...pi-permission-system-v26.2.2) (2026-08-18)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** derive session-approval patterns through the injected PathFlavor ([cf561de](https://github.com/gotgenes/pi-packages/commit/cf561de4c19895ed9495d82f22280fe3ba215aa5)), closes [#655](https://github.com/gotgenes/pi-packages/issues/655)
|
|
14
|
+
|
|
8
15
|
## [26.2.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v26.2.0...pi-permission-system-v26.2.1) (2026-08-17)
|
|
9
16
|
|
|
10
17
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { BashProgram } from "#src/access-intent/bash/program";
|
|
2
|
+
import type { PathNormalizer } from "#src/path-normalizer";
|
|
2
3
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
4
|
import { buildBashExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
|
|
4
5
|
import { SessionApproval } from "#src/session-approval";
|
|
5
|
-
import { deriveApprovalPattern } from "#src/session-rules";
|
|
6
6
|
import type { GateResult } from "./descriptor";
|
|
7
7
|
import { selectUncoveredExternalPaths } from "./external-directory-policy";
|
|
8
8
|
import { accessFactsFromPath } from "./helpers";
|
|
@@ -25,6 +25,7 @@ export function describeBashExternalDirectoryGate(
|
|
|
25
25
|
tcc: ToolCallContext,
|
|
26
26
|
bashProgram: BashProgram | null,
|
|
27
27
|
resolver: ScopedPermissionResolver,
|
|
28
|
+
normalizer: PathNormalizer,
|
|
28
29
|
): GateResult {
|
|
29
30
|
if (!bashProgram) return null;
|
|
30
31
|
const command = bashProgram.commandText();
|
|
@@ -94,7 +95,9 @@ export function describeBashExternalDirectoryGate(
|
|
|
94
95
|
matchedPattern: preCheck.matchedPattern,
|
|
95
96
|
});
|
|
96
97
|
|
|
97
|
-
const patterns =
|
|
98
|
+
const patterns = uncoveredEntries.map(({ path }) =>
|
|
99
|
+
normalizer.approvalPatternFor(path),
|
|
100
|
+
);
|
|
98
101
|
|
|
99
102
|
return {
|
|
100
103
|
surface: "external_directory",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
2
|
import type { BashProgram } from "#src/access-intent/bash/program";
|
|
3
|
+
import type { PathNormalizer } from "#src/path-normalizer";
|
|
3
4
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
5
|
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
|
5
6
|
import { SessionApproval } from "#src/session-approval";
|
|
6
|
-
import { deriveApprovalPattern } from "#src/session-rules";
|
|
7
7
|
import type { PermissionCheckResult } from "#src/types";
|
|
8
8
|
import { pickMostRestrictive } from "./candidate-check";
|
|
9
9
|
import type { GateResult } from "./descriptor";
|
|
@@ -33,6 +33,7 @@ export function describeBashPathGate(
|
|
|
33
33
|
tcc: ToolCallContext,
|
|
34
34
|
bashProgram: BashProgram | null,
|
|
35
35
|
resolver: ScopedPermissionResolver,
|
|
36
|
+
normalizer: PathNormalizer,
|
|
36
37
|
): GateResult {
|
|
37
38
|
if (!bashProgram) return null;
|
|
38
39
|
const command = bashProgram.commandText();
|
|
@@ -120,7 +121,7 @@ export function describeBashPathGate(
|
|
|
120
121
|
// Derive the pattern from the lexical absolute form (the cd-aware resolved
|
|
121
122
|
// path), so it matches the values a later call produces. For an unknown base
|
|
122
123
|
// (`forLiteral`) `value()` is the raw token.
|
|
123
|
-
const pattern =
|
|
124
|
+
const pattern = normalizer.approvalPatternFor(worstEntry.path);
|
|
124
125
|
const payload = buildPathAskPayload({
|
|
125
126
|
toolName: tcc.toolName,
|
|
126
127
|
pathValue: worstToken,
|
|
@@ -3,7 +3,6 @@ import type { PathNormalizer } from "#src/path-normalizer";
|
|
|
3
3
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
4
|
import { buildExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
|
|
5
5
|
import { SessionApproval } from "#src/session-approval";
|
|
6
|
-
import { deriveApprovalPattern } from "#src/session-rules";
|
|
7
6
|
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
8
7
|
import type { GateResult } from "./descriptor";
|
|
9
8
|
import { resolveExternalDirectoryPolicy } from "./external-directory-policy";
|
|
@@ -78,7 +77,7 @@ export function describeExternalDirectoryGate(
|
|
|
78
77
|
resolver,
|
|
79
78
|
tcc.agentName ?? undefined,
|
|
80
79
|
);
|
|
81
|
-
const pattern =
|
|
80
|
+
const pattern = normalizer.approvalPatternFor(accessPath);
|
|
82
81
|
|
|
83
82
|
const payload = buildExternalDirectoryAskPayload({
|
|
84
83
|
toolName: tcc.toolName,
|
|
@@ -3,7 +3,6 @@ import type { PathNormalizer } from "#src/path-normalizer";
|
|
|
3
3
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
4
|
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
|
5
5
|
import { SessionApproval } from "#src/session-approval";
|
|
6
|
-
import { deriveApprovalPattern } from "#src/session-rules";
|
|
7
6
|
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
8
7
|
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
9
8
|
import { accessFactsFromPath } from "./helpers";
|
|
@@ -46,7 +45,7 @@ export function describePathGate(
|
|
|
46
45
|
|
|
47
46
|
// Derive the approval pattern from the lexical absolute form so it matches
|
|
48
47
|
// the policy values a later call produces.
|
|
49
|
-
const pattern =
|
|
48
|
+
const pattern = normalizer.approvalPatternFor(accessPath);
|
|
50
49
|
|
|
51
50
|
const payload = buildPathAskPayload({
|
|
52
51
|
toolName: tcc.toolName,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
1
|
import { BashProgram } from "#src/access-intent/bash/program";
|
|
3
2
|
import { getPathBearingToolPath } from "#src/access-intent/tool-input-path";
|
|
4
3
|
import {
|
|
@@ -24,7 +23,7 @@ import { describeExternalDirectoryGate } from "./external-directory";
|
|
|
24
23
|
import { describePathGate } from "./path";
|
|
25
24
|
import type { GateRunner } from "./runner";
|
|
26
25
|
import { describeSkillReadGate } from "./skill-read";
|
|
27
|
-
import { describeToolGate } from "./tool";
|
|
26
|
+
import { describeToolGate, type ToolPathAccess } from "./tool";
|
|
28
27
|
import type { GateOutcome, ToolCallContext } from "./types";
|
|
29
28
|
|
|
30
29
|
/**
|
|
@@ -115,10 +114,16 @@ export class ToolCallGatePipeline {
|
|
|
115
114
|
normalizer,
|
|
116
115
|
this.customExtractors,
|
|
117
116
|
),
|
|
118
|
-
() =>
|
|
119
|
-
|
|
117
|
+
() =>
|
|
118
|
+
describeBashExternalDirectoryGate(
|
|
119
|
+
tcc,
|
|
120
|
+
bashProgram,
|
|
121
|
+
this.resolver,
|
|
122
|
+
normalizer,
|
|
123
|
+
),
|
|
124
|
+
() => describeBashPathGate(tcc, bashProgram, this.resolver, normalizer),
|
|
120
125
|
() => {
|
|
121
|
-
const { toolCheck,
|
|
126
|
+
const { toolCheck, pathAccess } = this.resolvePerToolCheck(
|
|
122
127
|
tcc,
|
|
123
128
|
shell,
|
|
124
129
|
bashProgram,
|
|
@@ -128,7 +133,7 @@ export class ToolCallGatePipeline {
|
|
|
128
133
|
tcc,
|
|
129
134
|
toolCheck,
|
|
130
135
|
formatter,
|
|
131
|
-
|
|
136
|
+
pathAccess,
|
|
132
137
|
shell,
|
|
133
138
|
);
|
|
134
139
|
toolDescriptor.preCheck = toolCheck;
|
|
@@ -153,15 +158,16 @@ export class ToolCallGatePipeline {
|
|
|
153
158
|
* #502); every other tool (and a path-bearing tool with no path) keeps the
|
|
154
159
|
* raw `tool` intent the manager normalizes.
|
|
155
160
|
*
|
|
156
|
-
* Returns the
|
|
157
|
-
*
|
|
161
|
+
* Returns the resolved path alongside the check, already paired with the
|
|
162
|
+
* session scope approving it grants — derived here, where the normalizer
|
|
163
|
+
* lives, rather than inside the gate (#655).
|
|
158
164
|
*/
|
|
159
165
|
private resolvePerToolCheck(
|
|
160
166
|
tcc: ToolCallContext,
|
|
161
167
|
shell: ShellInvocation | null,
|
|
162
168
|
bashProgram: BashProgram | null,
|
|
163
169
|
normalizer: PathNormalizer,
|
|
164
|
-
): { toolCheck: PermissionCheckResult;
|
|
170
|
+
): { toolCheck: PermissionCheckResult; pathAccess?: ToolPathAccess } {
|
|
165
171
|
if (shell) {
|
|
166
172
|
if (bashProgram) {
|
|
167
173
|
return {
|
|
@@ -190,7 +196,10 @@ export class ToolCallGatePipeline {
|
|
|
190
196
|
if (filePath !== null) {
|
|
191
197
|
const accessPath = normalizer.forPath(filePath);
|
|
192
198
|
return {
|
|
193
|
-
|
|
199
|
+
pathAccess: {
|
|
200
|
+
path: accessPath,
|
|
201
|
+
approvalPattern: normalizer.approvalPatternFor(accessPath),
|
|
202
|
+
},
|
|
194
203
|
toolCheck: this.resolver.resolve({
|
|
195
204
|
kind: "access-path",
|
|
196
205
|
surface: tcc.toolName,
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
classifyToolKind,
|
|
6
6
|
type ShellInvocation,
|
|
7
7
|
} from "#src/access-intent/tool-kind";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
suggestPathSessionPattern,
|
|
10
|
+
suggestSessionPattern,
|
|
11
|
+
} from "#src/pattern-suggest";
|
|
9
12
|
import { buildToolAskPayload } from "#src/presentation/tool-ask-payload";
|
|
10
13
|
import { SessionApproval } from "#src/session-approval";
|
|
11
14
|
import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
|
|
@@ -18,18 +21,30 @@ import {
|
|
|
18
21
|
} from "./helpers";
|
|
19
22
|
import type { ToolCallContext } from "./types";
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A path-bearing tool call's resolved path, paired with the session scope
|
|
26
|
+
* approving it would grant.
|
|
27
|
+
*
|
|
28
|
+
* The pattern is derived by the pipeline's `PathNormalizer`, which owns the
|
|
29
|
+
* session's `PathFlavor`, rather than re-derived here from `path.value()` — so
|
|
30
|
+
* the gate carries the platform's separator semantics without holding them
|
|
31
|
+
* (#655).
|
|
32
|
+
*/
|
|
33
|
+
export interface ToolPathAccess {
|
|
34
|
+
readonly path: AccessPath;
|
|
35
|
+
readonly approvalPattern: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
/**
|
|
22
39
|
* Derive the value used for session-approval pattern suggestions.
|
|
23
40
|
*
|
|
24
|
-
* Bash → command string; MCP → qualified target;
|
|
25
|
-
* path-bearing
|
|
26
|
-
*
|
|
27
|
-
* others (or a path-bearing tool with no path) → catch-all wildcard.
|
|
41
|
+
* Bash → command string; MCP → qualified target; everything else → catch-all
|
|
42
|
+
* wildcard. A path-bearing tool that resolved a path never reaches here — its
|
|
43
|
+
* suggestion comes from the already-derived {@link ToolPathAccess} pattern.
|
|
28
44
|
*/
|
|
29
45
|
function deriveSuggestionValue(
|
|
30
46
|
toolName: string,
|
|
31
47
|
check: PermissionCheckResult,
|
|
32
|
-
accessPath?: AccessPath,
|
|
33
48
|
): string {
|
|
34
49
|
switch (classifyToolKind(toolName)) {
|
|
35
50
|
case "bash":
|
|
@@ -37,7 +52,7 @@ function deriveSuggestionValue(
|
|
|
37
52
|
case "mcp":
|
|
38
53
|
return check.target ?? "mcp";
|
|
39
54
|
default:
|
|
40
|
-
return
|
|
55
|
+
return "*";
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
|
|
@@ -51,7 +66,7 @@ export function describeToolGate(
|
|
|
51
66
|
tcc: ToolCallContext,
|
|
52
67
|
check: PermissionCheckResult,
|
|
53
68
|
formatter: ToolPreviewFormatter,
|
|
54
|
-
|
|
69
|
+
pathAccess?: ToolPathAccess,
|
|
55
70
|
shell?: ShellInvocation | null,
|
|
56
71
|
): GateDescriptor {
|
|
57
72
|
// A shell invocation (native `bash` or an aliased shell tool) is gated on the
|
|
@@ -67,10 +82,12 @@ export function describeToolGate(
|
|
|
67
82
|
);
|
|
68
83
|
|
|
69
84
|
// Compute session approval suggestion for the "for this session" option.
|
|
70
|
-
const suggestion =
|
|
71
|
-
gateSurface,
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
const suggestion = pathAccess
|
|
86
|
+
? suggestPathSessionPattern(gateSurface, pathAccess.approvalPattern)
|
|
87
|
+
: suggestSessionPattern(
|
|
88
|
+
gateSurface,
|
|
89
|
+
deriveSuggestionValue(gateSurface, check),
|
|
90
|
+
);
|
|
74
91
|
|
|
75
92
|
const payload = buildToolAskPayload({
|
|
76
93
|
check,
|
|
@@ -89,8 +106,8 @@ export function describeToolGate(
|
|
|
89
106
|
|
|
90
107
|
// A path-bearing tool carries the AccessPath's alias set; every other surface
|
|
91
108
|
// (bash command, MCP target, plain tool) carries its already-portable value.
|
|
92
|
-
const accessIntent =
|
|
93
|
-
? accessFactsFromPath(gateSurface,
|
|
109
|
+
const accessIntent = pathAccess
|
|
110
|
+
? accessFactsFromPath(gateSurface, pathAccess.path)
|
|
94
111
|
: accessFactsFromValue(gateSurface, decisionValue);
|
|
95
112
|
|
|
96
113
|
return {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PathFlavor } from "#src/path/path-flavor";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive the wildcard glob to record when a user approves an accessed path for
|
|
5
|
+
* the session: the path's directory scope, with `*` appended.
|
|
6
|
+
*
|
|
7
|
+
* The scope is the value up to and including its last path separator, so the
|
|
8
|
+
* pattern is spelled with the separator the value itself carries. That matters
|
|
9
|
+
* on a win32 host, where Git Bash tokens are POSIX-shaped while Node's own
|
|
10
|
+
* `sep` is a backslash: deriving `/tmp/logs\*` from `/tmp/logs/` widens the
|
|
11
|
+
* grant to the parent directory once the `windowsSeparators` fold (#653)
|
|
12
|
+
* normalizes both operands. A value carrying no separator falls back to the
|
|
13
|
+
* current directory, which is what callers see only if they skipped resolving
|
|
14
|
+
* the path to its absolute form first (#438).
|
|
15
|
+
*
|
|
16
|
+
* The platform's separator alphabet arrives as an injected {@link PathFlavor},
|
|
17
|
+
* never an ambient `node:path` read, so win32 derivation is decidable — and
|
|
18
|
+
* testable — on a POSIX host (#655).
|
|
19
|
+
*/
|
|
20
|
+
export function deriveApprovalPattern(
|
|
21
|
+
pathValue: string,
|
|
22
|
+
flavor: PathFlavor,
|
|
23
|
+
): string {
|
|
24
|
+
const lastSeparator = flavor.lastSeparatorIndex(pathValue);
|
|
25
|
+
if (lastSeparator < 0) return `.${flavor.impl.sep}*`;
|
|
26
|
+
return `${pathValue.slice(0, lastSeparator + 1)}*`;
|
|
27
|
+
}
|
package/src/path/path-flavor.ts
CHANGED
|
@@ -17,7 +17,8 @@ import type { WildcardMatchOptions } from "#src/wildcard-matcher";
|
|
|
17
17
|
* fold or separator fold is a silent bypass (the #382 / #508 class). `PathFlavor`
|
|
18
18
|
* captures that mapping once so the leaves consume the resolved capability
|
|
19
19
|
* instead of re-interpreting a raw `NodeJS.Platform` string. It owns platform
|
|
20
|
-
* **semantics** — syntax ({@link hasPathSeparator}
|
|
20
|
+
* **semantics** — syntax ({@link hasPathSeparator} /
|
|
21
|
+
* {@link lastSeparatorIndex}), token shape
|
|
21
22
|
* ({@link bashTokenShape}), and the equivalence relation ({@link fold} /
|
|
22
23
|
* {@link comparable} / {@link isWithin} / {@link matchOptions}); domain policy
|
|
23
24
|
* (lexical cleanup, alias generation, safe-system-path exclusions, rule
|
|
@@ -50,6 +51,15 @@ export interface PathFlavor {
|
|
|
50
51
|
* POSIX; `/` or `\` on win32 (where a backslash is a separator, #520).
|
|
51
52
|
*/
|
|
52
53
|
hasPathSeparator(token: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Index of the last path separator in `value`, or `-1` when it holds none.
|
|
56
|
+
*
|
|
57
|
+
* Reads the same separator alphabet as {@link hasPathSeparator}, so a caller
|
|
58
|
+
* that must split a path at its directory boundary uses the separator the
|
|
59
|
+
* value was written with rather than this platform's default `sep` — the two
|
|
60
|
+
* differ for a Git Bash token on a win32 host (`/dev/null`, `/tmp/logs/`).
|
|
61
|
+
*/
|
|
62
|
+
lastSeparatorIndex(value: string): number;
|
|
53
63
|
/**
|
|
54
64
|
* The MSYS/Git-Bash interpretation of a bash-command token. On win32 this
|
|
55
65
|
* carries device / drive-mount / posix-absolute / plain semantics; on POSIX
|
|
@@ -60,6 +70,8 @@ export interface PathFlavor {
|
|
|
60
70
|
|
|
61
71
|
class PlatformPathFlavor implements PathFlavor {
|
|
62
72
|
readonly matchOptions: WildcardMatchOptions | undefined;
|
|
73
|
+
/** Every separator spelling this platform recognizes, the one alphabet both separator answers read. */
|
|
74
|
+
private readonly separators: readonly string[];
|
|
63
75
|
|
|
64
76
|
constructor(
|
|
65
77
|
readonly impl: PlatformPath,
|
|
@@ -68,6 +80,7 @@ class PlatformPathFlavor implements PathFlavor {
|
|
|
68
80
|
this.matchOptions = windows
|
|
69
81
|
? { caseInsensitive: true, windowsSeparators: true }
|
|
70
82
|
: undefined;
|
|
83
|
+
this.separators = windows ? ["/", "\\"] : ["/"];
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
fold(value: string): string {
|
|
@@ -91,7 +104,14 @@ class PlatformPathFlavor implements PathFlavor {
|
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
hasPathSeparator(token: string): boolean {
|
|
94
|
-
return
|
|
107
|
+
return this.lastSeparatorIndex(token) >= 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
lastSeparatorIndex(value: string): number {
|
|
111
|
+
return this.separators.reduce(
|
|
112
|
+
(last, separator) => Math.max(last, value.lastIndexOf(separator)),
|
|
113
|
+
-1,
|
|
114
|
+
);
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
bashTokenShape(token: string): BashTokenShape {
|
package/src/path-normalizer.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
normalizePathForComparison,
|
|
9
9
|
normalizePathPolicyLiteral,
|
|
10
10
|
} from "./access-intent/path-normalization";
|
|
11
|
+
import { deriveApprovalPattern } from "./path/approval-pattern";
|
|
11
12
|
import { isPathOutsideWorkingDirectory } from "./path/path-containment";
|
|
12
13
|
import { isPiInfrastructureRead } from "./path/pi-infrastructure-read";
|
|
13
14
|
|
|
@@ -99,6 +100,20 @@ export class PathNormalizer {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The session-approval glob for an accessed path: its directory scope plus
|
|
105
|
+
* `*`, derived through the baked flavor.
|
|
106
|
+
*
|
|
107
|
+
* Takes the already-built {@link AccessPath} — the lexical form is what a
|
|
108
|
+
* later tool call is matched on, so the pattern must be derived from the
|
|
109
|
+
* same representation the decision displayed (#438). Deriving it here rather
|
|
110
|
+
* than at each gate keeps the platform's separator alphabet with the object
|
|
111
|
+
* that owns the flavor, instead of an ambient `node:path` read (#655).
|
|
112
|
+
*/
|
|
113
|
+
approvalPatternFor(accessPath: AccessPath): string {
|
|
114
|
+
return deriveApprovalPattern(accessPath.value(), this.flavor);
|
|
115
|
+
}
|
|
116
|
+
|
|
102
117
|
/** Platform-aware absoluteness (`win32` vs `posix` rules). */
|
|
103
118
|
isAbsolute(pathValue: string): boolean {
|
|
104
119
|
return this.flavor.impl.isAbsolute(pathValue);
|
package/src/pattern-suggest.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PATH_BEARING_TOOLS } from "./access-intent/path-surfaces";
|
|
2
2
|
import { prefix, stripBashCommentLines } from "./bash-arity";
|
|
3
|
-
import { deriveApprovalPattern } from "./session-rules";
|
|
4
3
|
|
|
5
4
|
/** The suggestion returned for a "Yes, for this session" dialog option. */
|
|
6
5
|
export interface SessionApprovalSuggestion {
|
|
@@ -115,14 +114,16 @@ function buildLabel(pattern: string, surface: string): string {
|
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
/**
|
|
118
|
-
* Suggest a session-approval pattern
|
|
117
|
+
* Suggest a session-approval pattern from a surface's own value vocabulary —
|
|
118
|
+
* a bash command, an MCP target, a skill name.
|
|
119
119
|
*
|
|
120
120
|
* Returns a `SessionApprovalSuggestion` with the surface, the wildcard pattern
|
|
121
|
-
* to store in `SessionRules`, and a human-readable dialog label.
|
|
121
|
+
* to store in `SessionRules`, and a human-readable dialog label. Any surface
|
|
122
|
+
* with no vocabulary of its own falls back to the catch-all wildcard, which is
|
|
123
|
+
* also what a path-bearing tool invoked without a path resolves to.
|
|
122
124
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* matches the policy values a later tool call produces.
|
|
125
|
+
* A path surface goes through {@link suggestPathSessionPattern} instead: its
|
|
126
|
+
* pattern is a path-language product, and this module holds no path semantics.
|
|
126
127
|
*/
|
|
127
128
|
export function suggestSessionPattern(
|
|
128
129
|
surface: string,
|
|
@@ -140,22 +141,30 @@ export function suggestSessionPattern(
|
|
|
140
141
|
case "skill":
|
|
141
142
|
pattern = value;
|
|
142
143
|
break;
|
|
143
|
-
case "external_directory":
|
|
144
|
-
pattern = deriveApprovalPattern(value);
|
|
145
|
-
break;
|
|
146
|
-
case "path":
|
|
147
|
-
pattern = deriveApprovalPattern(value);
|
|
148
|
-
break;
|
|
149
144
|
default:
|
|
150
|
-
//
|
|
151
|
-
if (PATH_BEARING_TOOLS.has(surface) && value !== "*") {
|
|
152
|
-
pattern = deriveApprovalPattern(value);
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
// Extension tools / fallback.
|
|
145
|
+
// Extension tools, and path-bearing tools invoked without a path.
|
|
156
146
|
pattern = "*";
|
|
157
147
|
break;
|
|
158
148
|
}
|
|
159
149
|
|
|
160
150
|
return { surface, pattern, label: buildLabel(pattern, surface) };
|
|
161
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build the suggestion for a path surface from a pattern the caller already
|
|
155
|
+
* derived through its `PathNormalizer` (#655).
|
|
156
|
+
*
|
|
157
|
+
* The derivation belongs to the normalizer, which owns the session's
|
|
158
|
+
* `PathFlavor`; this module labels the result and must not re-interpret the
|
|
159
|
+
* separators it is handed.
|
|
160
|
+
*/
|
|
161
|
+
export function suggestPathSessionPattern(
|
|
162
|
+
surface: string,
|
|
163
|
+
approvalPattern: string,
|
|
164
|
+
): SessionApprovalSuggestion {
|
|
165
|
+
return {
|
|
166
|
+
surface,
|
|
167
|
+
pattern: approvalPattern,
|
|
168
|
+
label: buildLabel(approvalPattern, surface),
|
|
169
|
+
};
|
|
170
|
+
}
|
package/src/session-rules.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { dirname, sep } from "node:path";
|
|
2
|
-
|
|
3
1
|
import type { Ruleset } from "./rule";
|
|
4
2
|
import type { SessionApproval } from "./session-approval";
|
|
5
3
|
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
@@ -48,32 +46,3 @@ export class SessionRules implements SessionApprovalRecorder {
|
|
|
48
46
|
this.rules = [];
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Derive the wildcard glob pattern to approve from a normalized path.
|
|
54
|
-
*
|
|
55
|
-
* Returns `<parent-dir>/*` so that `evaluate()` / `wildcardMatch()` matches
|
|
56
|
-
* all paths under the approved directory — identical semantics to the former
|
|
57
|
-
* `SessionApprovalCache` prefix matching, using the unified wildcard engine.
|
|
58
|
-
*
|
|
59
|
-
* For paths that already end with a separator (directories), the separator
|
|
60
|
-
* is treated as the directory boundary and `*` is appended directly.
|
|
61
|
-
*
|
|
62
|
-
* The path is expected to be the canonical (cwd-resolved, absolute) form used
|
|
63
|
-
* for policy matching, so the derived pattern matches the same policy values a
|
|
64
|
-
* later tool call produces. Callers that hold a working directory resolve the
|
|
65
|
-
* path to that form first; the function itself stays free of cwd state.
|
|
66
|
-
*/
|
|
67
|
-
export function deriveApprovalPattern(normalizedPath: string): string {
|
|
68
|
-
// If the path already ends with a separator, it's a directory — glob its contents.
|
|
69
|
-
if (normalizedPath.endsWith(sep)) {
|
|
70
|
-
return `${normalizedPath}*`;
|
|
71
|
-
}
|
|
72
|
-
const dir = dirname(normalizedPath);
|
|
73
|
-
if (dir === normalizedPath) {
|
|
74
|
-
// Root path — dirname('/') === '/'
|
|
75
|
-
return `${dir}*`;
|
|
76
|
-
}
|
|
77
|
-
const prefix = dir.endsWith(sep) ? dir : `${dir}${sep}`;
|
|
78
|
-
return `${prefix}*`;
|
|
79
|
-
}
|