@gotgenes/pi-permission-system 17.1.0 → 18.0.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 +33 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +6 -3
- package/src/access-intent/bash/bash-path-resolver.ts +16 -12
- package/src/access-intent/bash/program.ts +0 -4
- package/src/access-intent/bash/token-classification.ts +23 -0
- package/src/handlers/before-agent-start.ts +1 -2
- package/src/handlers/gates/external-directory.ts +2 -12
- package/src/handlers/gates/skill-read.ts +4 -8
- package/src/handlers/gates/tool-call-gate-pipeline.ts +62 -24
- package/src/handlers/gates/tool.ts +10 -14
- package/src/index.ts +8 -6
- package/src/input-normalizer.ts +54 -56
- package/src/path-normalizer.ts +30 -0
- package/src/permission-event-rpc.ts +21 -15
- package/src/permission-manager.ts +3 -6
- package/src/permission-session.ts +0 -5
- package/src/permissions-service.ts +33 -12
- package/src/skill-prompt-sanitizer.ts +8 -21
- package/test/access-intent/bash/program.test.ts +1 -1
- package/test/access-intent/bash/token-classification.test.ts +75 -0
- package/test/bash-external-directory.test.ts +38 -0
- package/test/composition-root.test.ts +22 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -3
- package/test/handlers/gates/external-directory.test.ts +0 -1
- package/test/handlers/gates/skill-read.test.ts +16 -12
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +73 -0
- package/test/handlers/gates/tool.test.ts +25 -16
- package/test/helpers/gate-fixtures.ts +0 -3
- package/test/input-normalizer.test.ts +163 -270
- package/test/path-normalizer.test.ts +43 -0
- package/test/path-utils.test.ts +1 -1
- package/test/permission-event-rpc.test.ts +80 -65
- package/test/permission-manager-unified.test.ts +134 -145
- package/test/permissions-service.test.ts +84 -72
- package/test/service.test.ts +56 -103
- package/test/skill-prompt-sanitizer.test.ts +31 -65
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v17.1.1...pi-permission-system-v18.0.0) (2026-06-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** A permissions:rpc:check query for a `path` / `external_directory` / path-bearing surface now matches the canonical (symlink-resolved) alias, and a `path` / path-bearing query now evaluates the supplied path instead of collapsing to `*`.
|
|
14
|
+
* **pi-permission-system:** A service (`getPermissionsService().checkPermission`) query for a `path` / `external_directory` / path-bearing surface now matches the canonical (symlink-resolved) alias, and a `path` / path-bearing query now evaluates the supplied path instead of collapsing to `*`. A symlinked path can now match a rule on its canonical target.
|
|
15
|
+
* **pi-permission-system:** a per-tool path rule (e.g. `read: deny *.env`) now also fires when a symlink's resolved target matches the pattern, where previously only the lexically-referenced spelling matched. A symlink alias can no longer evade a per-tool deny/allow.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **pi-permission-system:** match the canonical form on service path queries ([be4a3e7](https://github.com/gotgenes/pi-packages/commit/be4a3e7f48e700db4c667ce8176459a1e89820b4))
|
|
20
|
+
* **pi-permission-system:** match the canonical form on the per-tool path gate ([ad36e78](https://github.com/gotgenes/pi-packages/commit/ad36e7860084be7692cb142f50c8818bd013ec38))
|
|
21
|
+
* **pi-permission-system:** match the canonical form on the RPC check query ([bb04ca5](https://github.com/gotgenes/pi-packages/commit/bb04ca5d0bae265570144f7d76dee2bad9269f94))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* **pi-permission-system:** remove unused join import; annotate closed findings ([#504](https://github.com/gotgenes/pi-packages/issues/504)) ([eb7c7b2](https://github.com/gotgenes/pi-packages/commit/eb7c7b298e21358d831892545b5dd8d3e9fb340a))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Documentation
|
|
30
|
+
|
|
31
|
+
* **pi-permission-system:** document canonical per-tool path matching ([bafa492](https://github.com/gotgenes/pi-packages/commit/bafa492cf8517902aea5a44d6ea59d4b33e7f754))
|
|
32
|
+
* **pi-permission-system:** document canonical service/RPC path matching ([35c36fa](https://github.com/gotgenes/pi-packages/commit/35c36fa9f7954aa84eb468954660b5857e067be3))
|
|
33
|
+
|
|
34
|
+
## [17.1.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v17.1.0...pi-permission-system-v17.1.1) (2026-06-29)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Bug Fixes
|
|
38
|
+
|
|
39
|
+
* **pi-permission-system:** gate Windows drive-letter paths in bash external_directory ([#508](https://github.com/gotgenes/pi-packages/issues/508)) ([2d33183](https://github.com/gotgenes/pi-packages/commit/2d331834254f58bcb0782b3da353132821536296))
|
|
40
|
+
|
|
8
41
|
## [17.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v17.0.0...pi-permission-system-v17.1.0) (2026-06-28)
|
|
9
42
|
|
|
10
43
|
|
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ A `path` pattern matches both the path as the agent references it and its canoni
|
|
|
73
73
|
|
|
74
74
|
For per-tool path patterns (`read`, `write`, `edit`, `find`, `grep`, `ls`), patterns are matched against the file path from `input.path`.
|
|
75
75
|
This lets you express rules like "allow reads but deny `.env` files" at the individual tool level.
|
|
76
|
+
Like the cross-cutting `path` surface, per-tool patterns match both the referenced path and its canonical (symlink-resolved) form, so a per-tool deny resists symlink-alias evasion.
|
|
76
77
|
When Pi's current working directory is known, relative path inputs also match their cwd-normalized absolute form, so `src/App.jsx` can match both `src/*` and `/workspace/project/*`.
|
|
77
78
|
|
|
78
79
|
The `external_directory` surface is the CWD-boundary gate: it decides whether reaching **outside** the working tree is allowed, and accepts a pattern map so you can allow specific outside-CWD directories without opening up all external access.
|
package/package.json
CHANGED
|
@@ -32,9 +32,12 @@ export interface PathValuesAccessIntent {
|
|
|
32
32
|
/**
|
|
33
33
|
* An `AccessPath` value object for a path-shaped surface.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Built for every path-shaped surface: the cross-cutting `path` and
|
|
36
|
+
* `external_directory` gates, the per-tool path-bearing surfaces
|
|
37
|
+
* (`read`/`write`/`edit`/`grep`/`find`/`ls`, #502), and the service/RPC policy
|
|
38
|
+
* queries for those surfaces (#503). Lets `AccessPath` flow into the resolver
|
|
39
|
+
* as a first-class variant so the resolver — not the producer — asks it for
|
|
40
|
+
* `matchValues()` (Tell-Don't-Ask).
|
|
38
41
|
*/
|
|
39
42
|
export interface AccessPathAccessIntent {
|
|
40
43
|
kind: "access-path";
|
|
@@ -347,7 +347,7 @@ export class BashPathResolver {
|
|
|
347
347
|
// anywhere, so flag it conservatively (resolved against the baked cwd
|
|
348
348
|
// only for a display path). Absolute / `~` candidates are base-independent
|
|
349
349
|
// below.
|
|
350
|
-
if (base.kind === "unknown" && isRelativeCandidate(candidate)) {
|
|
350
|
+
if (base.kind === "unknown" && this.isRelativeCandidate(candidate)) {
|
|
351
351
|
const accessPath = this.normalizer.forPath(candidate);
|
|
352
352
|
const canonical = accessPath.boundaryValue();
|
|
353
353
|
if (canonical && !isSafeSystemPath(canonical) && !seen.has(canonical)) {
|
|
@@ -421,7 +421,7 @@ export class BashPathResolver {
|
|
|
421
421
|
// An unknown base + relative candidate stays literal-only: a resolved
|
|
422
422
|
// absolute or canonical alias would resolve against the wrong directory and
|
|
423
423
|
// could spuriously match a rule (#393).
|
|
424
|
-
if (base.kind === "unknown" && isRelativeCandidate(candidate)) {
|
|
424
|
+
if (base.kind === "unknown" && this.isRelativeCandidate(candidate)) {
|
|
425
425
|
return this.normalizer.forLiteral(normalizePathPolicyLiteral(candidate));
|
|
426
426
|
}
|
|
427
427
|
|
|
@@ -431,6 +431,20 @@ export class BashPathResolver {
|
|
|
431
431
|
: undefined;
|
|
432
432
|
return this.normalizer.forPath(candidate, { resolveBase });
|
|
433
433
|
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* True when a path candidate is relative (resolved against the effective
|
|
437
|
+
* directory) rather than absolute or home-relative (`~…`), which are
|
|
438
|
+
* base-independent.
|
|
439
|
+
*
|
|
440
|
+
* Delegates the absoluteness decision to the platform-aware `PathNormalizer`
|
|
441
|
+
* rather than a POSIX-only `startsWith("/")` check, so Windows drive-letter
|
|
442
|
+
* paths (`C:/…`, `C:\…`) are correctly treated as absolute on win32 and as
|
|
443
|
+
* relative on POSIX (where they denote an in-CWD path).
|
|
444
|
+
*/
|
|
445
|
+
private isRelativeCandidate(candidate: string): boolean {
|
|
446
|
+
return !this.normalizer.isAbsolute(candidate) && !candidate.startsWith("~");
|
|
447
|
+
}
|
|
434
448
|
}
|
|
435
449
|
|
|
436
450
|
// ── Pure AST/string helpers ──────────────────────────────────────────────────
|
|
@@ -519,13 +533,3 @@ function literalTextOf(node: TSNode): string | null {
|
|
|
519
533
|
return null;
|
|
520
534
|
}
|
|
521
535
|
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* True when a path candidate is relative (resolved against the effective
|
|
525
|
-
* directory) rather than absolute (`/…`) or home-relative (`~…`), which are
|
|
526
|
-
* base-independent.
|
|
527
|
-
* Used to decide which candidates an unknown base affects.
|
|
528
|
-
*/
|
|
529
|
-
function isRelativeCandidate(candidate: string): boolean {
|
|
530
|
-
return !candidate.startsWith("/") && !candidate.startsWith("~");
|
|
531
|
-
}
|
|
@@ -73,10 +73,6 @@ export class BashProgram {
|
|
|
73
73
|
* back to the whole command so the surface is never evaluated weaker than
|
|
74
74
|
* before.
|
|
75
75
|
*/
|
|
76
|
-
// Used by resolveBashCommandCheck (bash-command.ts) and tests. Fallow's
|
|
77
|
-
// syntactic analysis cannot resolve the static-factory return type (private
|
|
78
|
-
// ctor), so it reports a false positive here.
|
|
79
|
-
// fallow-ignore-next-line unused-class-member
|
|
80
76
|
commands(): BashCommand[] {
|
|
81
77
|
return [...this.commandUnits];
|
|
82
78
|
}
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* Both classifiers share the private `rejectNonPathToken` predicate that captures
|
|
9
9
|
* the seven rejection cases common to both (the production clone this module was
|
|
10
10
|
* extracted to eliminate).
|
|
11
|
+
*
|
|
12
|
+
* Both classifiers recognize Windows drive-letter absolute paths (`C:/…`, `C:\…`)
|
|
13
|
+
* unconditionally on all platforms. On POSIX the token resolves as a real in-CWD
|
|
14
|
+
* relative path and is gated by the `path` surface; on Windows the `PathNormalizer`
|
|
15
|
+
* routes it through the absolute-path branch. Shape recognition is platform-independent
|
|
16
|
+
* string matching; the platform-sensitive absoluteness decision belongs to `PathNormalizer`.
|
|
11
17
|
*/
|
|
12
18
|
|
|
13
19
|
// ── Public classifiers ─────────────────────────────────────────────────────
|
|
@@ -19,6 +25,7 @@
|
|
|
19
25
|
* - Absolute paths (starting with `/`)
|
|
20
26
|
* - Home-relative paths (starting with `~/`)
|
|
21
27
|
* - Parent-traversal paths (containing `..`)
|
|
28
|
+
* - Windows drive-letter absolute paths (`C:/…` or `C:\…`)
|
|
22
29
|
*
|
|
23
30
|
* Returns the raw token string if it qualifies, or `null` to skip.
|
|
24
31
|
*/
|
|
@@ -28,6 +35,7 @@ export function classifyTokenAsPathCandidate(token: string): string | null {
|
|
|
28
35
|
if (token.startsWith("/")) return token;
|
|
29
36
|
if (token.startsWith("~/")) return token;
|
|
30
37
|
if (token.includes("..")) return token;
|
|
38
|
+
if (WINDOWS_DRIVE_PATH_PATTERN.test(token)) return token;
|
|
31
39
|
|
|
32
40
|
return null;
|
|
33
41
|
}
|
|
@@ -38,8 +46,13 @@ export function classifyTokenAsPathCandidate(token: string): string | null {
|
|
|
38
46
|
* Accepts the same shapes as `classifyTokenAsPathCandidate`, plus:
|
|
39
47
|
* - Dot-files and `./`-relative paths (starting with `.`)
|
|
40
48
|
* - Any relative path containing `/` (e.g. `src/foo.ts`)
|
|
49
|
+
* - Windows drive-letter absolute paths (`C:/…` or `C:\…`)
|
|
41
50
|
*
|
|
42
51
|
* The `~/foo` case is covered by `includes("/")` — no separate `~/` branch needed.
|
|
52
|
+
* The forward-slash drive form (`C:/…`) is also caught by `includes("/")`, but the
|
|
53
|
+
* explicit `WINDOWS_DRIVE_PATH_PATTERN` branch makes both separator forms first-class
|
|
54
|
+
* and order-independent, and covers the backslash-only form (`D:\…`) which `includes("/")`
|
|
55
|
+
* cannot reach.
|
|
43
56
|
*
|
|
44
57
|
* Does NOT require the strict "must start with `/` or `~/` or contain `..`"
|
|
45
58
|
* gate that the external-directory classifier uses.
|
|
@@ -52,12 +65,22 @@ export function classifyTokenAsRuleCandidate(token: string): string | null {
|
|
|
52
65
|
if (token.startsWith(".")) return token;
|
|
53
66
|
if (token.includes("/")) return token; // covers ~/ paths and all relative paths with /
|
|
54
67
|
if (token.includes("..")) return token; // bare ".." (no slash)
|
|
68
|
+
if (WINDOWS_DRIVE_PATH_PATTERN.test(token)) return token; // backslash-only drive form
|
|
55
69
|
|
|
56
70
|
return null;
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
// ── Private rejection predicate ────────────────────────────────────────────
|
|
60
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Windows drive-letter absolute path: a single ASCII letter, a colon, then a
|
|
77
|
+
* separator (`/` or `\`). Matches `C:/…` and `C:\…` but not drive-relative
|
|
78
|
+
* `C:foo` (no separator) or multi-letter schemes (`https:`, `mailto:`).
|
|
79
|
+
* Single-letter schemes with `//` (e.g. `c://x`) are already rejected by
|
|
80
|
+
* `URL_PATTERN` before this pattern is tested.
|
|
81
|
+
*/
|
|
82
|
+
const WINDOWS_DRIVE_PATH_PATTERN = /^[a-zA-Z]:[/\\]/;
|
|
83
|
+
|
|
61
84
|
/**
|
|
62
85
|
* URL pattern to skip tokens that look like URLs rather than paths.
|
|
63
86
|
*/
|
|
@@ -84,8 +84,7 @@ export class AgentPrepHandler {
|
|
|
84
84
|
toolPromptResult.prompt,
|
|
85
85
|
this.resolver,
|
|
86
86
|
agentName,
|
|
87
|
-
|
|
88
|
-
this.session.getPlatform(),
|
|
87
|
+
this.session.getPathNormalizer(),
|
|
89
88
|
);
|
|
90
89
|
this.session.setActiveSkillEntries(skillPromptResult.entries);
|
|
91
90
|
return skillPromptResult.prompt !== event.systemPrompt
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
2
|
-
import { getToolInputPath
|
|
2
|
+
import { getToolInputPath } from "#src/path-utils";
|
|
3
3
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
4
|
import { SessionApproval } from "#src/session-approval";
|
|
5
5
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
@@ -22,7 +22,6 @@ export function describeExternalDirectoryGate(
|
|
|
22
22
|
infraDirs: string[],
|
|
23
23
|
resolver: ScopedPermissionResolver,
|
|
24
24
|
normalizer: PathNormalizer,
|
|
25
|
-
platform: NodeJS.Platform,
|
|
26
25
|
extractors?: ToolAccessExtractorLookup,
|
|
27
26
|
): GateResult {
|
|
28
27
|
const externalDirectoryPath = getToolInputPath(
|
|
@@ -40,18 +39,9 @@ export function describeExternalDirectoryGate(
|
|
|
40
39
|
// check (below) use the canonical, symlink-resolved path; pattern matching
|
|
41
40
|
// uses the typed and resolved aliases (#418).
|
|
42
41
|
const accessPath = normalizer.forPath(externalDirectoryPath);
|
|
43
|
-
const canonicalExtPath = accessPath.boundaryValue();
|
|
44
42
|
|
|
45
43
|
// ── Pi infrastructure read bypass ──────────────────────────────────────
|
|
46
|
-
if (
|
|
47
|
-
isPiInfrastructureRead(
|
|
48
|
-
tcc.toolName,
|
|
49
|
-
canonicalExtPath,
|
|
50
|
-
infraDirs,
|
|
51
|
-
tcc.cwd,
|
|
52
|
-
platform,
|
|
53
|
-
)
|
|
54
|
-
) {
|
|
44
|
+
if (normalizer.isInfrastructureRead(tcc.toolName, accessPath, infraDirs)) {
|
|
55
45
|
return {
|
|
56
46
|
action: "allow",
|
|
57
47
|
log: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PathNormalizer } from "#src/path-normalizer";
|
|
2
2
|
import { formatSkillPathAskPrompt } from "#src/permission-prompts";
|
|
3
3
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
4
4
|
import { findSkillPathMatch } from "#src/skill-prompt-sanitizer";
|
|
@@ -15,7 +15,7 @@ import type { ToolCallContext } from "./types";
|
|
|
15
15
|
*/
|
|
16
16
|
export function describeSkillReadGate(
|
|
17
17
|
tcc: ToolCallContext,
|
|
18
|
-
|
|
18
|
+
normalizer: PathNormalizer,
|
|
19
19
|
getActiveSkillEntries: () => SkillPromptEntry[],
|
|
20
20
|
): GateDescriptor | null {
|
|
21
21
|
const activeSkillEntries = getActiveSkillEntries();
|
|
@@ -30,15 +30,11 @@ export function describeSkillReadGate(
|
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const normalizedReadPath =
|
|
34
|
-
path,
|
|
35
|
-
tcc.cwd,
|
|
36
|
-
platform,
|
|
37
|
-
);
|
|
33
|
+
const normalizedReadPath = normalizer.comparableValue(path);
|
|
38
34
|
const matchedSkill = findSkillPathMatch(
|
|
39
35
|
normalizedReadPath,
|
|
40
36
|
activeSkillEntries,
|
|
41
|
-
|
|
37
|
+
normalizer,
|
|
42
38
|
);
|
|
43
39
|
|
|
44
40
|
if (!matchedSkill) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { AccessPath } from "#src/access-intent/access-path";
|
|
1
2
|
import { BashProgram } from "#src/access-intent/bash/program";
|
|
2
3
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
4
|
+
import { getPathBearingToolPath } from "#src/path-utils";
|
|
3
5
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
6
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
5
7
|
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
ToolPreviewFormatter,
|
|
9
11
|
type ToolPreviewFormatterOptions,
|
|
10
12
|
} from "#src/tool-preview-formatter";
|
|
13
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
11
14
|
import { getNonEmptyString, toRecord } from "#src/value-guards";
|
|
12
15
|
import { resolveBashCommandCheck } from "./bash-command";
|
|
13
16
|
import { describeBashExternalDirectoryGate } from "./bash-external-directory";
|
|
@@ -39,8 +42,6 @@ export interface ToolCallGateInputs {
|
|
|
39
42
|
getToolPreviewLimits(): ToolPreviewFormatterOptions;
|
|
40
43
|
/** The session's path normalizer (platform + cwd baked in). */
|
|
41
44
|
getPathNormalizer(): PathNormalizer;
|
|
42
|
-
/** The host platform injected at the composition root. */
|
|
43
|
-
getPlatform(): NodeJS.Platform;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -81,11 +82,10 @@ export class ToolCallGatePipeline {
|
|
|
81
82
|
);
|
|
82
83
|
|
|
83
84
|
const infraDirs = this.inputs.getInfrastructureReadDirs();
|
|
84
|
-
const platform = this.inputs.getPlatform();
|
|
85
85
|
|
|
86
86
|
const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
|
|
87
87
|
() =>
|
|
88
|
-
describeSkillReadGate(tcc,
|
|
88
|
+
describeSkillReadGate(tcc, normalizer, () =>
|
|
89
89
|
this.inputs.getActiveSkillEntries(),
|
|
90
90
|
),
|
|
91
91
|
() =>
|
|
@@ -96,35 +96,22 @@ export class ToolCallGatePipeline {
|
|
|
96
96
|
infraDirs,
|
|
97
97
|
this.resolver,
|
|
98
98
|
normalizer,
|
|
99
|
-
platform,
|
|
100
99
|
this.customExtractors,
|
|
101
100
|
),
|
|
102
101
|
() => describeBashExternalDirectoryGate(tcc, bashProgram, this.resolver),
|
|
103
102
|
() => describeBashPathGate(tcc, bashProgram, this.resolver),
|
|
104
103
|
() => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
? resolveBashCommandCheck(
|
|
112
|
-
command ?? "",
|
|
113
|
-
bashProgram.commands(),
|
|
114
|
-
tcc.agentName ?? undefined,
|
|
115
|
-
this.resolver,
|
|
116
|
-
)
|
|
117
|
-
: this.resolver.resolve({
|
|
118
|
-
kind: "tool",
|
|
119
|
-
surface: tcc.toolName,
|
|
120
|
-
input: tcc.input,
|
|
121
|
-
agentName: tcc.agentName ?? undefined,
|
|
122
|
-
});
|
|
104
|
+
const { toolCheck, accessPath } = this.resolvePerToolCheck(
|
|
105
|
+
tcc,
|
|
106
|
+
bashProgram,
|
|
107
|
+
command,
|
|
108
|
+
normalizer,
|
|
109
|
+
);
|
|
123
110
|
const toolDescriptor = describeToolGate(
|
|
124
111
|
tcc,
|
|
125
112
|
toolCheck,
|
|
126
113
|
formatter,
|
|
127
|
-
|
|
114
|
+
accessPath,
|
|
128
115
|
);
|
|
129
116
|
toolDescriptor.preCheck = toolCheck;
|
|
130
117
|
return toolDescriptor;
|
|
@@ -144,4 +131,55 @@ export class ToolCallGatePipeline {
|
|
|
144
131
|
|
|
145
132
|
return { action: "allow" };
|
|
146
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the per-tool gate's check, choosing the intent by tool shape:
|
|
137
|
+
* bash chains its sub-commands; a path-bearing tool with a path emits an
|
|
138
|
+
* `access-path` intent (so the per-tool surface matches lexical ∪ canonical,
|
|
139
|
+
* #502); every other tool (and a path-bearing tool with no path) keeps the
|
|
140
|
+
* raw `tool` intent the manager normalizes.
|
|
141
|
+
*
|
|
142
|
+
* Returns the `AccessPath` alongside the check so `describeToolGate` derives
|
|
143
|
+
* the session-approval value from `accessPath.value()`.
|
|
144
|
+
*/
|
|
145
|
+
private resolvePerToolCheck(
|
|
146
|
+
tcc: ToolCallContext,
|
|
147
|
+
bashProgram: BashProgram | null,
|
|
148
|
+
command: string | null,
|
|
149
|
+
normalizer: PathNormalizer,
|
|
150
|
+
): { toolCheck: PermissionCheckResult; accessPath?: AccessPath } {
|
|
151
|
+
if (tcc.toolName === "bash" && bashProgram) {
|
|
152
|
+
return {
|
|
153
|
+
toolCheck: resolveBashCommandCheck(
|
|
154
|
+
command ?? "",
|
|
155
|
+
bashProgram.commands(),
|
|
156
|
+
tcc.agentName ?? undefined,
|
|
157
|
+
this.resolver,
|
|
158
|
+
),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const filePath = getPathBearingToolPath(tcc.toolName, tcc.input);
|
|
163
|
+
if (filePath !== null) {
|
|
164
|
+
const accessPath = normalizer.forPath(filePath);
|
|
165
|
+
return {
|
|
166
|
+
accessPath,
|
|
167
|
+
toolCheck: this.resolver.resolve({
|
|
168
|
+
kind: "access-path",
|
|
169
|
+
surface: tcc.toolName,
|
|
170
|
+
path: accessPath,
|
|
171
|
+
agentName: tcc.agentName ?? undefined,
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
toolCheck: this.resolver.resolve({
|
|
178
|
+
kind: "tool",
|
|
179
|
+
surface: tcc.toolName,
|
|
180
|
+
input: tcc.input,
|
|
181
|
+
agentName: tcc.agentName ?? undefined,
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
147
185
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
normalizePathForComparison,
|
|
4
|
-
PATH_BEARING_TOOLS,
|
|
5
|
-
} from "#src/path-utils";
|
|
1
|
+
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
|
+
import { getPathBearingToolPath, PATH_BEARING_TOOLS } from "#src/path-utils";
|
|
6
3
|
import { suggestSessionPattern } from "#src/pattern-suggest";
|
|
7
4
|
import { formatAskPrompt } from "#src/permission-prompts";
|
|
8
5
|
import { SessionApproval } from "#src/session-approval";
|
|
@@ -16,20 +13,19 @@ import type { ToolCallContext } from "./types";
|
|
|
16
13
|
* Derive the value used for session-approval pattern suggestions.
|
|
17
14
|
*
|
|
18
15
|
* Bash → command string; MCP → qualified target;
|
|
19
|
-
* path-bearing tools → the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* path-bearing tools → the `AccessPath`'s lexical absolute form (`value()`),
|
|
17
|
+
* so the suggested pattern matches the policy values a later call produces;
|
|
18
|
+
* others (or a path-bearing tool with no path) → catch-all wildcard.
|
|
22
19
|
*/
|
|
23
20
|
function deriveSuggestionValue(
|
|
24
21
|
tcc: ToolCallContext,
|
|
25
22
|
check: PermissionCheckResult,
|
|
26
|
-
|
|
23
|
+
accessPath?: AccessPath,
|
|
27
24
|
): string {
|
|
28
25
|
if (tcc.toolName === "bash") return check.command ?? "";
|
|
29
26
|
if (tcc.toolName === "mcp") return check.target ?? "mcp";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return normalizePathForComparison(path, tcc.cwd, platform);
|
|
27
|
+
if (accessPath) return accessPath.value();
|
|
28
|
+
return "*";
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
/**
|
|
@@ -42,7 +38,7 @@ export function describeToolGate(
|
|
|
42
38
|
tcc: ToolCallContext,
|
|
43
39
|
check: PermissionCheckResult,
|
|
44
40
|
formatter: ToolPreviewFormatter,
|
|
45
|
-
|
|
41
|
+
accessPath?: AccessPath,
|
|
46
42
|
): GateDescriptor {
|
|
47
43
|
const permissionLogContext = formatter.getPermissionLogContext(
|
|
48
44
|
check,
|
|
@@ -53,7 +49,7 @@ export function describeToolGate(
|
|
|
53
49
|
// Compute session approval suggestion for the "for this session" option.
|
|
54
50
|
const suggestion = suggestSessionPattern(
|
|
55
51
|
tcc.toolName,
|
|
56
|
-
deriveSuggestionValue(tcc, check,
|
|
52
|
+
deriveSuggestionValue(tcc, check, accessPath),
|
|
57
53
|
);
|
|
58
54
|
|
|
59
55
|
const askMessage = formatAskPrompt(
|
package/src/index.ts
CHANGED
|
@@ -134,17 +134,21 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
134
134
|
),
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
// Resolver composes the manager + session ruleset and owns the
|
|
138
|
+
// access-path → path-values unwrap; the RPC and service route their policy
|
|
139
|
+
// queries through it, so it is constructed before both.
|
|
140
|
+
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
141
|
+
|
|
137
142
|
const rpcHandles = registerPermissionRpcHandlers(pi.events, {
|
|
138
|
-
|
|
139
|
-
sessionRules,
|
|
143
|
+
resolver,
|
|
140
144
|
session,
|
|
141
145
|
requestPermissionDecisionFromUi,
|
|
142
146
|
logger,
|
|
143
147
|
});
|
|
144
148
|
|
|
145
149
|
const permissionsService = new LocalPermissionsService(
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
resolver,
|
|
151
|
+
session,
|
|
148
152
|
formatterRegistry,
|
|
149
153
|
accessExtractorRegistry,
|
|
150
154
|
);
|
|
@@ -174,8 +178,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
174
178
|
setActive: (names: string[]) => pi.setActiveTools(names),
|
|
175
179
|
};
|
|
176
180
|
|
|
177
|
-
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
178
|
-
|
|
179
181
|
const audit = new DecisionAudit();
|
|
180
182
|
const lifecycle = new SessionLifecycleHandler(
|
|
181
183
|
session,
|