@gotgenes/pi-permission-system 18.0.2 → 18.1.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 +15 -0
- package/package.json +1 -1
- package/src/access-intent/bash/bash-path-resolver.ts +30 -10
- package/src/access-intent/bash/program.ts +9 -0
- package/src/access-intent/bash/token-classification.ts +39 -9
- package/src/handlers/gates/tool-call-gate-pipeline.ts +13 -2
- package/src/permission-manager.ts +51 -1
- package/src/permission-session.ts +12 -0
- package/src/rule.ts +1 -1
- package/src/types.ts +11 -0
- package/test/access-intent/bash/program.test.ts +57 -0
- package/test/access-intent/bash/token-classification.test.ts +47 -0
- package/test/composition-root.test.ts +73 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +26 -0
- package/test/helpers/gate-fixtures.ts +7 -1
- package/test/helpers/session-fixtures.ts +8 -1
- package/test/permission-manager-unified.test.ts +81 -0
- package/test/permission-resolver.test.ts +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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
|
+
## [18.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v18.0.2...pi-permission-system-v18.1.0) (2026-07-02)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** add rule-driven bare-token classifier ([0d693a7](https://github.com/gotgenes/pi-packages/commit/0d693a79478b3e7d9d23c573ec5a085ce8afccfd)), closes [#509](https://github.com/gotgenes/pi-packages/issues/509)
|
|
14
|
+
* **pi-permission-system:** derive promotable path-token matcher from config ([4a29882](https://github.com/gotgenes/pi-packages/commit/4a298827768fe8730b6b51898740d084676d46cd)), closes [#509](https://github.com/gotgenes/pi-packages/issues/509)
|
|
15
|
+
* **pi-permission-system:** gate bash bare filenames via path rules ([4ee201a](https://github.com/gotgenes/pi-packages/commit/4ee201a0f771c732e9ac62562a09d54204b425aa)), closes [#509](https://github.com/gotgenes/pi-packages/issues/509)
|
|
16
|
+
* **pi-permission-system:** promote bare tokens in bash path projection ([f887b7c](https://github.com/gotgenes/pi-packages/commit/f887b7cd3820f085d97105bfa6dc470b35500835)), closes [#509](https://github.com/gotgenes/pi-packages/issues/509)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* **pi-permission-system:** document bash bare-filename path promotion ([db7159b](https://github.com/gotgenes/pi-packages/commit/db7159b00af083dc9a7af13c41589ba95376f9ac)), closes [#509](https://github.com/gotgenes/pi-packages/issues/509)
|
|
22
|
+
|
|
8
23
|
## [18.0.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v18.0.1...pi-permission-system-v18.0.2) (2026-07-01)
|
|
9
24
|
|
|
10
25
|
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "#src/access-intent/bash/node-text";
|
|
6
6
|
import type { TSNode } from "#src/access-intent/bash/parser";
|
|
7
7
|
import {
|
|
8
|
+
classifyPromotedRuleCandidate,
|
|
8
9
|
classifyTokenAsPathCandidate,
|
|
9
10
|
classifyTokenAsRuleCandidate,
|
|
10
11
|
} from "#src/access-intent/bash/token-classification";
|
|
@@ -17,6 +18,10 @@ import {
|
|
|
17
18
|
import { normalizePathPolicyLiteral } from "#src/access-intent/path-normalization";
|
|
18
19
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
19
20
|
import { isSafeSystemPath } from "#src/safe-system-paths";
|
|
21
|
+
import type { PathRuleTokenMatcher } from "#src/types";
|
|
22
|
+
|
|
23
|
+
/** Default promotion predicate: promotes nothing (#509). */
|
|
24
|
+
const NO_PROMOTION: PathRuleTokenMatcher = () => false;
|
|
20
25
|
|
|
21
26
|
// ── Internal types ───────────────────────────────────────────────────────────
|
|
22
27
|
|
|
@@ -74,20 +79,29 @@ const UNKNOWN_BASE: EffectiveBase = { kind: "unknown" };
|
|
|
74
79
|
/**
|
|
75
80
|
* Resolves the filesystem paths a parsed bash program references.
|
|
76
81
|
*
|
|
77
|
-
* Holds a {@link PathNormalizer} (platform + cwd baked in) as its
|
|
78
|
-
* and answers all platform/cwd-dependent questions through it —
|
|
79
|
-
* folding (`isAbsolute`/`joinBase`), per-candidate resolution
|
|
80
|
-
* `forLiteral`/`resolveBase`), and the outside-cwd boundary
|
|
81
|
-
* walk step re-reads the platform or threads the cwd.
|
|
82
|
+
* Holds a {@link PathNormalizer} (platform + cwd baked in) as its primary
|
|
83
|
+
* collaborator and answers all platform/cwd-dependent questions through it —
|
|
84
|
+
* `cd`-base folding (`isAbsolute`/`joinBase`), per-candidate resolution
|
|
85
|
+
* (`forPath`/`forLiteral`/`resolveBase`), and the outside-cwd boundary
|
|
86
|
+
* decision — so no walk step re-reads the platform or threads the cwd.
|
|
87
|
+
*
|
|
88
|
+
* Also holds an `isPromotablePathToken` predicate (default: promotes nothing)
|
|
89
|
+
* deciding whether a bare token that fails the broad rule-candidate shape gate
|
|
90
|
+
* should still be promoted because it matches an active, specific `path` rule
|
|
91
|
+
* (#509). The resolver never sees the rules themselves — the predicate is
|
|
92
|
+
* built and owned by `PermissionManager.getPromotablePathTokenMatcher`.
|
|
82
93
|
*
|
|
83
94
|
* Tell-don't-ask: callers hand it a parsed tree and receive the resolved
|
|
84
95
|
* {@link ResolvedBashPaths} slices in one {@link resolve} call; the AST walk,
|
|
85
96
|
* the `cd`-folding state, and the intermediate path candidates stay private.
|
|
86
97
|
* One instance per parse ({@link BashProgram.parse} constructs it with the
|
|
87
|
-
* session normalizer).
|
|
98
|
+
* session normalizer and promotion predicate).
|
|
88
99
|
*/
|
|
89
100
|
export class BashPathResolver {
|
|
90
|
-
constructor(
|
|
101
|
+
constructor(
|
|
102
|
+
private readonly normalizer: PathNormalizer,
|
|
103
|
+
private readonly isPromotablePathToken: PathRuleTokenMatcher = NO_PROMOTION,
|
|
104
|
+
) {}
|
|
91
105
|
|
|
92
106
|
/**
|
|
93
107
|
* Resolve a parsed bash program's path references into its external-path and
|
|
@@ -387,8 +401,12 @@ export class BashPathResolver {
|
|
|
387
401
|
* policy lookup values.
|
|
388
402
|
*
|
|
389
403
|
* Filters candidates through the broad path classifier
|
|
390
|
-
* (`classifyTokenAsRuleCandidate`)
|
|
391
|
-
*
|
|
404
|
+
* (`classifyTokenAsRuleCandidate`), falling back to the rule-driven promoted
|
|
405
|
+
* classifier (`classifyPromotedRuleCandidate`, #509) for a bare token the
|
|
406
|
+
* broad classifier rejects for shape — promoted only when the injected
|
|
407
|
+
* `isPromotablePathToken` predicate matches an active, specific `path` rule.
|
|
408
|
+
* Pairs each qualifying token with its set of policy values (absolute +
|
|
409
|
+
* project-relative + raw).
|
|
392
410
|
* A token after a non-literal `cd` keeps only its literal value so no
|
|
393
411
|
* spurious absolute rule can match (#393).
|
|
394
412
|
*/
|
|
@@ -399,7 +417,9 @@ export class BashPathResolver {
|
|
|
399
417
|
const result: BashPathRuleCandidate[] = [];
|
|
400
418
|
|
|
401
419
|
for (const { token, base } of candidates) {
|
|
402
|
-
const candidate =
|
|
420
|
+
const candidate =
|
|
421
|
+
classifyTokenAsRuleCandidate(token) ??
|
|
422
|
+
classifyPromotedRuleCandidate(token, this.isPromotablePathToken);
|
|
403
423
|
if (!candidate) continue;
|
|
404
424
|
|
|
405
425
|
const path = this.buildRuleCandidatePath(candidate, base);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "#src/access-intent/bash/command-enumeration";
|
|
10
10
|
import { getParser } from "#src/access-intent/bash/parser";
|
|
11
11
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
12
|
+
import type { PathRuleTokenMatcher } from "#src/types";
|
|
12
13
|
|
|
13
14
|
export type { BashCommand, BashPathRuleCandidate };
|
|
14
15
|
|
|
@@ -37,10 +38,17 @@ export class BashProgram {
|
|
|
37
38
|
* through the injected {@link PathNormalizer} (platform + cwd baked in).
|
|
38
39
|
* Heredoc bodies, comments, and other non-argument content are skipped. An
|
|
39
40
|
* unparseable command yields an empty program.
|
|
41
|
+
*
|
|
42
|
+
* `isPromotablePathToken`, when supplied, promotes a bare filename token
|
|
43
|
+
* (e.g. `id_rsa`) into `pathRuleCandidates()` when it matches an active,
|
|
44
|
+
* specific `path` deny/ask rule (#509). Defaults to promoting nothing, so
|
|
45
|
+
* callers that only read `externalPaths()` (e.g. `bash-path-extractor.ts`)
|
|
46
|
+
* are unaffected.
|
|
40
47
|
*/
|
|
41
48
|
static async parse(
|
|
42
49
|
command: string,
|
|
43
50
|
normalizer: PathNormalizer,
|
|
51
|
+
isPromotablePathToken?: PathRuleTokenMatcher,
|
|
44
52
|
): Promise<BashProgram> {
|
|
45
53
|
const parser = await getParser();
|
|
46
54
|
const tree = parser.parse(command);
|
|
@@ -49,6 +57,7 @@ export class BashProgram {
|
|
|
49
57
|
try {
|
|
50
58
|
const { externalPaths, ruleCandidates } = new BashPathResolver(
|
|
51
59
|
normalizer,
|
|
60
|
+
isPromotablePathToken,
|
|
52
61
|
).resolve(tree.rootNode);
|
|
53
62
|
return new BashProgram(
|
|
54
63
|
collectCommands(tree.rootNode),
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure, synchronous token-classification helpers for bash path extraction.
|
|
3
3
|
*
|
|
4
|
-
* Exports
|
|
4
|
+
* Exports three classifiers consumed by `bash-path-resolver.ts`:
|
|
5
5
|
* - `classifyTokenAsPathCandidate` — strict gate for the external-directory guard.
|
|
6
6
|
* - `classifyTokenAsRuleCandidate` — broader gate for cross-cutting `path` rules.
|
|
7
|
+
* - `classifyPromotedRuleCandidate` — rule-driven promotion of a bare filename
|
|
8
|
+
* (e.g. `id_rsa`) that `classifyTokenAsRuleCandidate` rejects for shape, but
|
|
9
|
+
* which matches an active, specific (non-`*`) `path` deny/ask rule (#509).
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
* the seven rejection cases common to
|
|
10
|
-
* extracted to eliminate).
|
|
11
|
+
* All three classifiers share the private `rejectNonPathToken` predicate that
|
|
12
|
+
* captures the seven rejection cases common to them (the production clone this
|
|
13
|
+
* module was extracted to eliminate).
|
|
11
14
|
*
|
|
12
|
-
* Both
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Both `classifyTokenAsPathCandidate` and `classifyTokenAsRuleCandidate` recognize
|
|
16
|
+
* Windows drive-letter absolute paths (`C:/…`, `C:\…`) unconditionally on all
|
|
17
|
+
* platforms. On POSIX the token resolves as a real in-CWD relative path and is
|
|
18
|
+
* gated by the `path` surface; on Windows the `PathNormalizer` routes it through
|
|
19
|
+
* the absolute-path branch. Shape recognition is platform-independent string
|
|
20
|
+
* matching; the platform-sensitive absoluteness decision belongs to `PathNormalizer`.
|
|
17
21
|
*/
|
|
22
|
+
import type { PathRuleTokenMatcher } from "#src/types";
|
|
18
23
|
|
|
19
24
|
// ── Public classifiers ─────────────────────────────────────────────────────
|
|
20
25
|
|
|
@@ -70,6 +75,31 @@ export function classifyTokenAsRuleCandidate(token: string): string | null {
|
|
|
70
75
|
return null;
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Rule-driven promotion classifier for bare filenames (#509).
|
|
80
|
+
*
|
|
81
|
+
* A bare token (`id_rsa`) has none of the shapes `classifyTokenAsRuleCandidate`
|
|
82
|
+
* accepts, so it is dropped before rule evaluation by default — most bash
|
|
83
|
+
* argument tokens are not file paths (subcommands, branch names, search
|
|
84
|
+
* patterns). This classifier promotes a bare token into the rule-candidate
|
|
85
|
+
* surface only when the caller-supplied `isPromotable` predicate says it
|
|
86
|
+
* matches an active, specific `path` deny/ask rule, closing the bypass without
|
|
87
|
+
* treating every bare argument as a path.
|
|
88
|
+
*
|
|
89
|
+
* Still runs the shared `rejectNonPathToken` prelude first, so a flag,
|
|
90
|
+
* env-assignment, URL, `@scope` token, or regex-shaped token is never
|
|
91
|
+
* promoted even if it happens to match a configured pattern.
|
|
92
|
+
*
|
|
93
|
+
* Returns the raw token string if it qualifies, or `null` to skip.
|
|
94
|
+
*/
|
|
95
|
+
export function classifyPromotedRuleCandidate(
|
|
96
|
+
token: string,
|
|
97
|
+
isPromotable: PathRuleTokenMatcher,
|
|
98
|
+
): string | null {
|
|
99
|
+
if (rejectNonPathToken(token)) return null;
|
|
100
|
+
return isPromotable(token) ? token : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
// ── Private rejection predicate ────────────────────────────────────────────
|
|
74
104
|
|
|
75
105
|
/**
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
ToolPreviewFormatter,
|
|
11
11
|
type ToolPreviewFormatterOptions,
|
|
12
12
|
} from "#src/tool-preview-formatter";
|
|
13
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
13
|
+
import type { PathRuleTokenMatcher, PermissionCheckResult } from "#src/types";
|
|
14
14
|
import { getNonEmptyString, toRecord } from "#src/value-guards";
|
|
15
15
|
import { resolveBashCommandCheck } from "./bash-command";
|
|
16
16
|
import { describeBashExternalDirectoryGate } from "./bash-external-directory";
|
|
@@ -42,6 +42,11 @@ export interface ToolCallGateInputs {
|
|
|
42
42
|
getToolPreviewLimits(): ToolPreviewFormatterOptions;
|
|
43
43
|
/** The session's path normalizer (platform + cwd baked in). */
|
|
44
44
|
getPathNormalizer(): PathNormalizer;
|
|
45
|
+
/**
|
|
46
|
+
* Predicate deciding whether a bare bash token should be promoted into the
|
|
47
|
+
* `path` rule-candidate surface (#509), scoped to the given agent.
|
|
48
|
+
*/
|
|
49
|
+
getPromotablePathTokenMatcher(agentName?: string): PathRuleTokenMatcher;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -73,7 +78,13 @@ export class ToolCallGatePipeline {
|
|
|
73
78
|
const normalizer = this.inputs.getPathNormalizer();
|
|
74
79
|
const bashProgram =
|
|
75
80
|
tcc.toolName === "bash" && command
|
|
76
|
-
? await BashProgram.parse(
|
|
81
|
+
? await BashProgram.parse(
|
|
82
|
+
command,
|
|
83
|
+
normalizer,
|
|
84
|
+
this.inputs.getPromotablePathTokenMatcher(
|
|
85
|
+
tcc.agentName ?? undefined,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
77
88
|
: null;
|
|
78
89
|
|
|
79
90
|
const formatter = new ToolPreviewFormatter(
|
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
type ResolvedPolicyPaths,
|
|
16
16
|
} from "./policy-loader";
|
|
17
17
|
import type { Rule, RuleOrigin, Ruleset } from "./rule";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
evaluate,
|
|
20
|
+
evaluateAnyValue,
|
|
21
|
+
evaluateFirst,
|
|
22
|
+
pathMatchOptions,
|
|
23
|
+
} from "./rule";
|
|
19
24
|
import { mergeScopesWithOrigins } from "./scope-merge";
|
|
20
25
|
import {
|
|
21
26
|
composeRuleset,
|
|
@@ -24,10 +29,12 @@ import {
|
|
|
24
29
|
} from "./synthesize";
|
|
25
30
|
import type {
|
|
26
31
|
FlatPermissionConfig,
|
|
32
|
+
PathRuleTokenMatcher,
|
|
27
33
|
PermissionCheckResult,
|
|
28
34
|
PermissionState,
|
|
29
35
|
} from "./types";
|
|
30
36
|
import { isPermissionState } from "./value-guards";
|
|
37
|
+
import { wildcardMatch } from "./wildcard-matcher";
|
|
31
38
|
|
|
32
39
|
const BUILT_IN_TOOL_PERMISSION_NAMES = new Set([
|
|
33
40
|
"bash",
|
|
@@ -43,6 +50,9 @@ const SPECIAL_PERMISSION_KEYS = new Set(["external_directory", "path"]);
|
|
|
43
50
|
/** Universal fallback when permission["*"] is absent from all scopes. */
|
|
44
51
|
const DEFAULT_UNIVERSAL_FALLBACK: PermissionState = "ask";
|
|
45
52
|
|
|
53
|
+
/** Promotion predicate matching no token — the no-`path`-rules default (#509). */
|
|
54
|
+
const NO_PROMOTION: PathRuleTokenMatcher = () => false;
|
|
55
|
+
|
|
46
56
|
type FileCacheEntry<TValue> = {
|
|
47
57
|
stamp: string;
|
|
48
58
|
value: TValue;
|
|
@@ -76,6 +86,15 @@ export interface ScopedPermissionManager {
|
|
|
76
86
|
): PermissionCheckResult;
|
|
77
87
|
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
|
78
88
|
getConfigIssues(agentName?: string): string[];
|
|
89
|
+
/**
|
|
90
|
+
* Build a predicate deciding whether a bare bash token should be promoted
|
|
91
|
+
* into the `path` rule-candidate surface (#509).
|
|
92
|
+
*
|
|
93
|
+
* Matches against specific (non-`*`) `path`-surface config rules whose
|
|
94
|
+
* action is `deny` or `ask` — an allow rule never gates, and `"*"` would
|
|
95
|
+
* promote every bare bash argument.
|
|
96
|
+
*/
|
|
97
|
+
getPromotablePathTokenMatcher(agentName?: string): PathRuleTokenMatcher;
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
export interface PermissionManagerOptions extends PolicyLoaderOptions {
|
|
@@ -211,6 +230,37 @@ export class PermissionManager implements ScopedPermissionManager {
|
|
|
211
230
|
return composedRules.filter((r) => r.layer === "config");
|
|
212
231
|
}
|
|
213
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Build a predicate deciding whether a bare bash token should be promoted
|
|
235
|
+
* into the `path` rule-candidate surface (#509).
|
|
236
|
+
*
|
|
237
|
+
* Filters the composed config ruleset to specific (non-`*`) `path`-surface
|
|
238
|
+
* deny/ask patterns, then returns a closure matching a token against them
|
|
239
|
+
* with the platform-correct fold (Windows case-and-separator matching, same
|
|
240
|
+
* as {@link pathMatchOptions} applies for evaluation) so promotion agrees
|
|
241
|
+
* with the later `path`-surface decision.
|
|
242
|
+
*
|
|
243
|
+
* Returns a matcher rejecting every token when no such rule exists — the
|
|
244
|
+
* default-config case is unaffected by promotion.
|
|
245
|
+
*/
|
|
246
|
+
getPromotablePathTokenMatcher(agentName?: string): PathRuleTokenMatcher {
|
|
247
|
+
const { composedRules } = this.resolvePermissions(agentName);
|
|
248
|
+
const patterns = composedRules
|
|
249
|
+
.filter(
|
|
250
|
+
(r) =>
|
|
251
|
+
r.layer === "config" &&
|
|
252
|
+
r.surface === "path" &&
|
|
253
|
+
r.pattern !== "*" &&
|
|
254
|
+
r.action !== "allow",
|
|
255
|
+
)
|
|
256
|
+
.map((r) => r.pattern);
|
|
257
|
+
if (patterns.length === 0) return NO_PROMOTION;
|
|
258
|
+
|
|
259
|
+
const matchOptions = pathMatchOptions("path", this.platform);
|
|
260
|
+
return (token) =>
|
|
261
|
+
patterns.some((pattern) => wildcardMatch(pattern, token, matchOptions));
|
|
262
|
+
}
|
|
263
|
+
|
|
214
264
|
/**
|
|
215
265
|
* Get the tool-level permission state for a tool, without considering
|
|
216
266
|
* command-level rules. Used for tool injection decisions.
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveToolPreviewLimits,
|
|
20
20
|
type ToolPreviewFormatterOptions,
|
|
21
21
|
} from "./tool-preview-formatter";
|
|
22
|
+
import type { PathRuleTokenMatcher } from "./types";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Encapsulates all mutable session state and exposes operations instead of
|
|
@@ -214,4 +215,15 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
214
215
|
getPathNormalizer(): PathNormalizer {
|
|
215
216
|
return this.pathNormalizer;
|
|
216
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Predicate deciding whether a bare bash token should be promoted into the
|
|
221
|
+
* `path` rule-candidate surface (#509), scoped to the given agent.
|
|
222
|
+
*
|
|
223
|
+
* Straight delegate to `permissionManager.getPromotablePathTokenMatcher` —
|
|
224
|
+
* the manager owns the composed ruleset and the platform-correct match.
|
|
225
|
+
*/
|
|
226
|
+
getPromotablePathTokenMatcher(agentName?: string): PathRuleTokenMatcher {
|
|
227
|
+
return this.permissionManager.getPromotablePathTokenMatcher(agentName);
|
|
228
|
+
}
|
|
217
229
|
}
|
package/src/rule.ts
CHANGED
|
@@ -74,7 +74,7 @@ export function evaluate(
|
|
|
74
74
|
* pattern→value match (case and separators) so mixed-case / forward-slash
|
|
75
75
|
* overrides still match. The surface→surface match stays exact.
|
|
76
76
|
*/
|
|
77
|
-
function pathMatchOptions(
|
|
77
|
+
export function pathMatchOptions(
|
|
78
78
|
surface: string,
|
|
79
79
|
platform: NodeJS.Platform,
|
|
80
80
|
): { caseInsensitive: true; windowsSeparators: true } | undefined {
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,17 @@ export interface DenyWithReason {
|
|
|
16
16
|
/** A pattern value: a PermissionState string OR a DenyWithReason object. */
|
|
17
17
|
export type PatternValue = PermissionState | DenyWithReason;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Predicate deciding whether a bare bash token should be promoted into the
|
|
21
|
+
* `path` rule-candidate surface.
|
|
22
|
+
*
|
|
23
|
+
* Built by `PermissionManager.getPromotablePathTokenMatcher` from the
|
|
24
|
+
* composed config ruleset (specific, non-`*` `path` deny/ask patterns) and
|
|
25
|
+
* threaded through to `BashPathResolver` so promotion policy stays in the
|
|
26
|
+
* manager while the bash layer only asks the predicate.
|
|
27
|
+
*/
|
|
28
|
+
export type PathRuleTokenMatcher = (token: string) => boolean;
|
|
29
|
+
|
|
19
30
|
/**
|
|
20
31
|
* The on-disk permission shape inside the `"permission"` key.
|
|
21
32
|
* A surface value is a PermissionState string (shorthand for `{ "*": action }`)
|
|
@@ -91,6 +91,63 @@ describe("BashProgram", () => {
|
|
|
91
91
|
expect(fileCandidate?.path.matchValues()).toEqual(["src/foo.ts"]);
|
|
92
92
|
expect(fileCandidate?.path.boundaryValue()).toBe("");
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
describe("rule-driven bare-token promotion (#509)", () => {
|
|
96
|
+
it("promotes a bare token when the matcher says it is promotable", async () => {
|
|
97
|
+
const isPromotable = (token: string): boolean => token === "id_rsa";
|
|
98
|
+
const program = await BashProgram.parse(
|
|
99
|
+
"cat id_rsa",
|
|
100
|
+
normalizer,
|
|
101
|
+
isPromotable,
|
|
102
|
+
);
|
|
103
|
+
const candidates = program.pathRuleCandidates();
|
|
104
|
+
expect(candidates.map(({ token }) => token)).toEqual(["id_rsa"]);
|
|
105
|
+
expect(candidates[0].path.matchValues()).toEqual([
|
|
106
|
+
"/projects/my-app/id_rsa",
|
|
107
|
+
"id_rsa",
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not promote a bare token the matcher rejects", async () => {
|
|
112
|
+
const isPromotable = (token: string): boolean => token === "id_rsa";
|
|
113
|
+
const program = await BashProgram.parse(
|
|
114
|
+
"git status",
|
|
115
|
+
normalizer,
|
|
116
|
+
isPromotable,
|
|
117
|
+
);
|
|
118
|
+
expect(program.pathRuleCandidates()).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not promote any bare token with the default no-op matcher", async () => {
|
|
122
|
+
const program = await BashProgram.parse("cat id_rsa", normalizer);
|
|
123
|
+
expect(program.pathRuleCandidates()).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("keeps a promoted token literal-only after an unknown cd (#393)", async () => {
|
|
127
|
+
const isPromotable = (token: string): boolean => token === "id_rsa";
|
|
128
|
+
const program = await BashProgram.parse(
|
|
129
|
+
'cd "$DIR" && cat id_rsa',
|
|
130
|
+
normalizer,
|
|
131
|
+
isPromotable,
|
|
132
|
+
);
|
|
133
|
+
const candidate = program
|
|
134
|
+
.pathRuleCandidates()
|
|
135
|
+
.find((c) => c.token === "id_rsa");
|
|
136
|
+
expect(candidate?.path.matchValues()).toEqual(["id_rsa"]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does not double-promote a token the shape gate already accepts", async () => {
|
|
140
|
+
// ./id_rsa already passes classifyTokenAsRuleCandidate; the promoted
|
|
141
|
+
// fallback must not run (and must not duplicate the candidate).
|
|
142
|
+
const isPromotable = (): boolean => true;
|
|
143
|
+
const program = await BashProgram.parse(
|
|
144
|
+
"cat ./id_rsa",
|
|
145
|
+
normalizer,
|
|
146
|
+
isPromotable,
|
|
147
|
+
);
|
|
148
|
+
expect(program.pathRuleCandidates()).toHaveLength(1);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
94
151
|
});
|
|
95
152
|
|
|
96
153
|
describe("externalPaths", () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
classifyPromotedRuleCandidate,
|
|
4
5
|
classifyTokenAsPathCandidate,
|
|
5
6
|
classifyTokenAsRuleCandidate,
|
|
6
7
|
} from "#src/access-intent/bash/token-classification";
|
|
@@ -314,3 +315,49 @@ describe("classifyTokenAsRuleCandidate", () => {
|
|
|
314
315
|
}
|
|
315
316
|
});
|
|
316
317
|
});
|
|
318
|
+
|
|
319
|
+
describe("classifyPromotedRuleCandidate", () => {
|
|
320
|
+
test("shape-eligible bare token promoted when predicate returns true", () => {
|
|
321
|
+
expect(classifyPromotedRuleCandidate("id_rsa", () => true)).toBe("id_rsa");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("shape-eligible bare token rejected when predicate returns false", () => {
|
|
325
|
+
expect(classifyPromotedRuleCandidate("id_rsa", () => false)).toBeNull();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("predicate receives the raw token", () => {
|
|
329
|
+
const isPromotable = (token: string): boolean => token === "key.pem";
|
|
330
|
+
expect(classifyPromotedRuleCandidate("key.pem", isPromotable)).toBe(
|
|
331
|
+
"key.pem",
|
|
332
|
+
);
|
|
333
|
+
expect(classifyPromotedRuleCandidate("other.pem", isPromotable)).toBeNull();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("shared rejection still applies regardless of the predicate", () => {
|
|
337
|
+
test("flag (leading dash) → null", () => {
|
|
338
|
+
expect(classifyPromotedRuleCandidate("-r", () => true)).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("env assignment → null", () => {
|
|
342
|
+
expect(classifyPromotedRuleCandidate("FOO=/bar", () => true)).toBeNull();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("URL → null", () => {
|
|
346
|
+
expect(
|
|
347
|
+
classifyPromotedRuleCandidate("https://example.com", () => true),
|
|
348
|
+
).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("@scope/package → null", () => {
|
|
352
|
+
expect(classifyPromotedRuleCandidate("@foo/bar", () => true)).toBeNull();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("regex metacharacters → null", () => {
|
|
356
|
+
expect(classifyPromotedRuleCandidate("foo.*", () => true)).toBeNull();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("empty string → null", () => {
|
|
360
|
+
expect(classifyPromotedRuleCandidate("", () => true)).toBeNull();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
@@ -501,6 +501,79 @@ describe("service path queries evaluate the supplied path (#503)", () => {
|
|
|
501
501
|
});
|
|
502
502
|
});
|
|
503
503
|
|
|
504
|
+
describe("bash bare-filename path gating (#509)", () => {
|
|
505
|
+
// Before #509 a bash bare-filename argument (`cat id_rsa`) bypassed the
|
|
506
|
+
// `path` surface entirely: the broad classifier only accepted tokens
|
|
507
|
+
// starting with `.`, containing `/`, containing `..`, or a Windows
|
|
508
|
+
// drive-letter absolute path. The same file accessed via a prefixed path
|
|
509
|
+
// (`cat ./id_rsa`) or the `read` tool was already gated. Rule-driven
|
|
510
|
+
// promotion closes the gap for a bare token matching a specific, non-`*`
|
|
511
|
+
// `path` deny/ask rule — the literal repro from the issue.
|
|
512
|
+
|
|
513
|
+
async function fireBashToolCall(
|
|
514
|
+
pi: ReturnType<typeof makeFakePi>,
|
|
515
|
+
ctx: unknown,
|
|
516
|
+
command: string,
|
|
517
|
+
): Promise<{ block?: true; reason?: string }> {
|
|
518
|
+
return (await pi.fire(
|
|
519
|
+
"tool_call",
|
|
520
|
+
{ name: "bash", input: { command }, toolCallId: "tc-1" },
|
|
521
|
+
ctx,
|
|
522
|
+
)) as { block?: true; reason?: string };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
it("denies a bare filename matching a specific path deny rule", async () => {
|
|
526
|
+
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-bare-token-cwd-"));
|
|
527
|
+
writeGlobalConfig({
|
|
528
|
+
permission: { "*": "allow", path: { id_rsa: "deny" } },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const pi = makeFakePi({ events: createEventBus() });
|
|
532
|
+
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
|
|
533
|
+
const ctx = makeChildCtx(cwd, "bare-token-session-deny");
|
|
534
|
+
await fireSessionStart(pi, ctx);
|
|
535
|
+
|
|
536
|
+
const result = await fireBashToolCall(pi, ctx, "cat id_rsa");
|
|
537
|
+
expect(result.block).toBe(true);
|
|
538
|
+
|
|
539
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("denies a bare filename matching a wildcard path deny rule", async () => {
|
|
543
|
+
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-bare-token-cwd-"));
|
|
544
|
+
writeGlobalConfig({
|
|
545
|
+
permission: { "*": "allow", path: { "*.pem": "deny" } },
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const pi = makeFakePi({ events: createEventBus() });
|
|
549
|
+
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
|
|
550
|
+
const ctx = makeChildCtx(cwd, "bare-token-session-wildcard");
|
|
551
|
+
await fireSessionStart(pi, ctx);
|
|
552
|
+
|
|
553
|
+
const result = await fireBashToolCall(pi, ctx, "cat key.pem");
|
|
554
|
+
expect(result.block).toBe(true);
|
|
555
|
+
|
|
556
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("leaves a bare token that does not match any path rule unaffected", async () => {
|
|
560
|
+
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-bare-token-cwd-"));
|
|
561
|
+
writeGlobalConfig({
|
|
562
|
+
permission: { "*": "allow", path: { id_rsa: "deny" } },
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const pi = makeFakePi({ events: createEventBus() });
|
|
566
|
+
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
|
|
567
|
+
const ctx = makeChildCtx(cwd, "bare-token-session-unaffected");
|
|
568
|
+
await fireSessionStart(pi, ctx);
|
|
569
|
+
|
|
570
|
+
const result = await fireBashToolCall(pi, ctx, "git status");
|
|
571
|
+
expect(result.block).toBeUndefined();
|
|
572
|
+
|
|
573
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
504
577
|
describe("multi-instance global service interplay", () => {
|
|
505
578
|
// The fix (#302) scopes the process-global service slot to the publishing
|
|
506
579
|
// instance. The parent publishes at its session_start; an in-process child
|
|
@@ -186,6 +186,7 @@ describe("ToolCallGatePipeline", () => {
|
|
|
186
186
|
expect(mockBashProgramParse).toHaveBeenCalledWith(
|
|
187
187
|
"echo hello",
|
|
188
188
|
expect.any(PathNormalizer),
|
|
189
|
+
expect.any(Function),
|
|
189
190
|
);
|
|
190
191
|
});
|
|
191
192
|
|
|
@@ -202,6 +203,31 @@ describe("ToolCallGatePipeline", () => {
|
|
|
202
203
|
|
|
203
204
|
expect(mockBashProgramParse).not.toHaveBeenCalled();
|
|
204
205
|
});
|
|
206
|
+
|
|
207
|
+
it("passes the session's promotable path-token matcher into BashProgram.parse (#509)", async () => {
|
|
208
|
+
const resolver = makeResolver(makeCheckResult());
|
|
209
|
+
const isPromotable = vi.fn((token: string) => token === "id_rsa");
|
|
210
|
+
const getPromotablePathTokenMatcher = vi.fn(() => isPromotable);
|
|
211
|
+
const inputs = makeGateInputs({ getPromotablePathTokenMatcher });
|
|
212
|
+
const { runner } = makeGateRunner();
|
|
213
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
214
|
+
|
|
215
|
+
await pipeline.evaluate(
|
|
216
|
+
makeTcc({
|
|
217
|
+
toolName: "bash",
|
|
218
|
+
input: { command: "cat id_rsa" },
|
|
219
|
+
agentName: "my-agent",
|
|
220
|
+
}),
|
|
221
|
+
runner,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(getPromotablePathTokenMatcher).toHaveBeenCalledWith("my-agent");
|
|
225
|
+
expect(mockBashProgramParse).toHaveBeenCalledWith(
|
|
226
|
+
"cat id_rsa",
|
|
227
|
+
expect.any(PathNormalizer),
|
|
228
|
+
isPromotable,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
205
231
|
});
|
|
206
232
|
|
|
207
233
|
// ── customExtractors threading (#352) ────────────────────────────────────
|
|
@@ -15,7 +15,7 @@ import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
|
15
15
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
16
16
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
17
17
|
import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
|
|
18
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
18
|
+
import type { PathRuleTokenMatcher, PermissionCheckResult } from "#src/types";
|
|
19
19
|
|
|
20
20
|
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
21
21
|
|
|
@@ -255,6 +255,9 @@ export function makeGateInputs(
|
|
|
255
255
|
getInfrastructureReadDirs?: () => string[];
|
|
256
256
|
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
257
257
|
getPathNormalizer?: () => PathNormalizer;
|
|
258
|
+
getPromotablePathTokenMatcher?: (
|
|
259
|
+
agentName?: string,
|
|
260
|
+
) => PathRuleTokenMatcher;
|
|
258
261
|
} = {},
|
|
259
262
|
): ToolCallGateInputs {
|
|
260
263
|
return {
|
|
@@ -275,6 +278,9 @@ export function makeGateInputs(
|
|
|
275
278
|
vi.fn<() => PathNormalizer>(
|
|
276
279
|
() => new PathNormalizer(process.platform, "/test/cwd"),
|
|
277
280
|
),
|
|
281
|
+
getPromotablePathTokenMatcher:
|
|
282
|
+
overrides.getPromotablePathTokenMatcher ??
|
|
283
|
+
vi.fn<(agentName?: string) => PathRuleTokenMatcher>(() => () => false),
|
|
278
284
|
};
|
|
279
285
|
}
|
|
280
286
|
|
|
@@ -25,7 +25,11 @@ import type { PromptingGatewayLifecycle } from "#src/prompting-gateway";
|
|
|
25
25
|
import type { Ruleset } from "#src/rule";
|
|
26
26
|
import type { SessionLogger } from "#src/session-logger";
|
|
27
27
|
import { SessionRules } from "#src/session-rules";
|
|
28
|
-
import type {
|
|
28
|
+
import type {
|
|
29
|
+
PathRuleTokenMatcher,
|
|
30
|
+
PermissionCheckResult,
|
|
31
|
+
PermissionState,
|
|
32
|
+
} from "#src/types";
|
|
29
33
|
|
|
30
34
|
// ── Per-collaborator fake factories ────────────────────────────────────────
|
|
31
35
|
|
|
@@ -105,6 +109,9 @@ export function makeFakePermissionManager() {
|
|
|
105
109
|
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
106
110
|
.mockReturnValue("allow"),
|
|
107
111
|
getConfigIssues: vi.fn((): string[] => []),
|
|
112
|
+
getPromotablePathTokenMatcher: vi
|
|
113
|
+
.fn<(agentName?: string) => PathRuleTokenMatcher>()
|
|
114
|
+
.mockReturnValue(() => false),
|
|
108
115
|
};
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -1096,6 +1096,87 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
1096
1096
|
).toBe(true);
|
|
1097
1097
|
});
|
|
1098
1098
|
});
|
|
1099
|
+
|
|
1100
|
+
describe("getPromotablePathTokenMatcher (#509)", () => {
|
|
1101
|
+
it("matches a bare token against a specific deny pattern", () => {
|
|
1102
|
+
const manager = makeInMemoryManager({
|
|
1103
|
+
global: { permission: { path: { id_rsa: "deny" } } },
|
|
1104
|
+
});
|
|
1105
|
+
const isPromotable = manager.getPromotablePathTokenMatcher();
|
|
1106
|
+
expect(isPromotable("id_rsa")).toBe(true);
|
|
1107
|
+
expect(isPromotable("other_file")).toBe(false);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it("matches a bare token against a specific ask wildcard pattern", () => {
|
|
1111
|
+
const manager = makeInMemoryManager({
|
|
1112
|
+
global: { permission: { path: { "*.pem": "ask" } } },
|
|
1113
|
+
});
|
|
1114
|
+
const isPromotable = manager.getPromotablePathTokenMatcher();
|
|
1115
|
+
expect(isPromotable("key.pem")).toBe(true);
|
|
1116
|
+
expect(isPromotable("key.txt")).toBe(false);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it("does not match against the universal '*' path pattern", () => {
|
|
1120
|
+
const manager = makeInMemoryManager({
|
|
1121
|
+
global: { permission: { path: { "*": "ask" } } },
|
|
1122
|
+
});
|
|
1123
|
+
const isPromotable = manager.getPromotablePathTokenMatcher();
|
|
1124
|
+
expect(isPromotable("anything")).toBe(false);
|
|
1125
|
+
expect(isPromotable("status")).toBe(false);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it("does not match against an allow-only path pattern", () => {
|
|
1129
|
+
const manager = makeInMemoryManager({
|
|
1130
|
+
global: { permission: { path: { id_rsa: "allow" } } },
|
|
1131
|
+
});
|
|
1132
|
+
const isPromotable = manager.getPromotablePathTokenMatcher();
|
|
1133
|
+
expect(isPromotable("id_rsa")).toBe(false);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it("returns a no-op matcher when no path rules exist", () => {
|
|
1137
|
+
const manager = makeInMemoryManager({
|
|
1138
|
+
global: { permission: { read: "allow" } },
|
|
1139
|
+
});
|
|
1140
|
+
const isPromotable = manager.getPromotablePathTokenMatcher();
|
|
1141
|
+
expect(isPromotable("id_rsa")).toBe(false);
|
|
1142
|
+
expect(isPromotable("anything")).toBe(false);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it("folds case on an injected win32 platform", () => {
|
|
1146
|
+
const manager = new PermissionManager({
|
|
1147
|
+
policyLoader: createInMemoryPolicyLoader({
|
|
1148
|
+
global: { permission: { path: { id_rsa: "deny" } } },
|
|
1149
|
+
}),
|
|
1150
|
+
platform: "win32",
|
|
1151
|
+
});
|
|
1152
|
+
const isPromotable = manager.getPromotablePathTokenMatcher();
|
|
1153
|
+
expect(isPromotable("ID_RSA")).toBe(true);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("stays case-sensitive on a POSIX platform", () => {
|
|
1157
|
+
const manager = new PermissionManager({
|
|
1158
|
+
policyLoader: createInMemoryPolicyLoader({
|
|
1159
|
+
global: { permission: { path: { id_rsa: "deny" } } },
|
|
1160
|
+
}),
|
|
1161
|
+
platform: "linux",
|
|
1162
|
+
});
|
|
1163
|
+
const isPromotable = manager.getPromotablePathTokenMatcher();
|
|
1164
|
+
expect(isPromotable("ID_RSA")).toBe(false);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it("scopes to the requested agent's composed rules", () => {
|
|
1168
|
+
const manager = makeInMemoryManager({
|
|
1169
|
+
global: { permission: { path: { id_rsa: "deny" } } },
|
|
1170
|
+
agent: {
|
|
1171
|
+
coder: { permission: { path: { "secret.key": "deny" } } },
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
const globalMatcher = manager.getPromotablePathTokenMatcher();
|
|
1175
|
+
const agentMatcher = manager.getPromotablePathTokenMatcher("coder");
|
|
1176
|
+
expect(globalMatcher("secret.key")).toBe(false);
|
|
1177
|
+
expect(agentMatcher("secret.key")).toBe(true);
|
|
1178
|
+
});
|
|
1179
|
+
});
|
|
1099
1180
|
});
|
|
1100
1181
|
|
|
1101
1182
|
// ---------------------------------------------------------------------------
|
|
@@ -6,7 +6,11 @@ import { PermissionResolver } from "#src/permission-resolver";
|
|
|
6
6
|
import type { Ruleset } from "#src/rule";
|
|
7
7
|
import { SessionApproval } from "#src/session-approval";
|
|
8
8
|
import { SessionRules } from "#src/session-rules";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
PathRuleTokenMatcher,
|
|
11
|
+
PermissionCheckResult,
|
|
12
|
+
PermissionState,
|
|
13
|
+
} from "#src/types";
|
|
10
14
|
|
|
11
15
|
function makePermissionManager() {
|
|
12
16
|
return {
|
|
@@ -28,6 +32,9 @@ function makePermissionManager() {
|
|
|
28
32
|
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
29
33
|
.mockReturnValue("allow"),
|
|
30
34
|
getConfigIssues: vi.fn((): string[] => []),
|
|
35
|
+
getPromotablePathTokenMatcher: vi
|
|
36
|
+
.fn<(agentName?: string) => PathRuleTokenMatcher>()
|
|
37
|
+
.mockReturnValue(() => false),
|
|
31
38
|
};
|
|
32
39
|
}
|
|
33
40
|
|