@gotgenes/pi-permission-system 5.17.0 → 5.18.1
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 +27 -0
- package/README.md +2 -1
- package/package.json +4 -1
- package/src/index.ts +21 -0
- package/src/input-normalizer.ts +28 -0
- package/src/pattern-suggest.ts +5 -0
- package/src/permission-event-rpc.ts +1 -20
- package/src/permission-events.ts +25 -3
- package/src/service.ts +75 -0
- package/tests/pattern-suggest.test.ts +15 -0
- package/tests/service.test.ts +144 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ 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
|
+
## [5.18.1](https://github.com/gotgenes/pi-permission-system/compare/v5.18.0...v5.18.1) (2026-05-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan Pi GitHub Tools extension ([#153](https://github.com/gotgenes/pi-permission-system/issues/153)) ([6f8566f](https://github.com/gotgenes/pi-permission-system/commit/6f8566feba22981e3f726aae8544bab53dce8a8a))
|
|
14
|
+
* **retro:** add retro notes for issue [#145](https://github.com/gotgenes/pi-permission-system/issues/145) ([70ff363](https://github.com/gotgenes/pi-permission-system/commit/70ff36369a902f82d78e277ba9fa7948bb62d82c))
|
|
15
|
+
* update /ship-issue to use pi-github-tools ([#153](https://github.com/gotgenes/pi-permission-system/issues/153)) ([7a4de21](https://github.com/gotgenes/pi-permission-system/commit/7a4de21ee16f7a1fff3ef8cf6e2b3a0516183ab5))
|
|
16
|
+
* update docs and pattern-suggest for path surface ([6defcdb](https://github.com/gotgenes/pi-permission-system/commit/6defcdb5430296c82da6eefc1980ab109dedc202))
|
|
17
|
+
|
|
18
|
+
## [5.18.0](https://github.com/gotgenes/pi-permission-system/compare/v5.17.0...v5.18.0) (2026-05-14)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* add package.json exports field for cross-extension import ([#145](https://github.com/gotgenes/pi-permission-system/issues/145)) ([1091de5](https://github.com/gotgenes/pi-permission-system/commit/1091de5eb673050c3b83448ee69cffb876c407d9))
|
|
24
|
+
* add Symbol.for()-backed service accessor module ([#145](https://github.com/gotgenes/pi-permission-system/issues/145)) ([6a7ddab](https://github.com/gotgenes/pi-permission-system/commit/6a7ddab6e3e58ca0f93807255e9e48716d96ca24))
|
|
25
|
+
* publish permissions service on startup, clear on shutdown ([#145](https://github.com/gotgenes/pi-permission-system/issues/145)) ([97bea7b](https://github.com/gotgenes/pi-permission-system/commit/97bea7bec043de4f6abb823846cef5c04a069517))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
|
|
30
|
+
* deprecate permissions:rpc:check types in favor of service accessor ([#145](https://github.com/gotgenes/pi-permission-system/issues/145)) ([a64b1b9](https://github.com/gotgenes/pi-permission-system/commit/a64b1b91f141e1a98b576433280ad09d95ec3011))
|
|
31
|
+
* document service accessor and deprecate RPC check ([#145](https://github.com/gotgenes/pi-permission-system/issues/145)) ([931a14e](https://github.com/gotgenes/pi-permission-system/commit/931a14efec1ef9bc53075f8974bfd1eeff7e0749))
|
|
32
|
+
* plan Symbol.for()-backed service accessor ([#145](https://github.com/gotgenes/pi-permission-system/issues/145)) ([d9448bc](https://github.com/gotgenes/pi-permission-system/commit/d9448bc8c9ee71714599a18f03cf516a5ffca2cb))
|
|
33
|
+
* **retro:** add retro notes for issue [#148](https://github.com/gotgenes/pi-permission-system/issues/148) ([84e0262](https://github.com/gotgenes/pi-permission-system/commit/84e026264e292357c18c0333b1d1bd561f70149b))
|
|
34
|
+
|
|
8
35
|
## [5.17.0](https://github.com/gotgenes/pi-permission-system/compare/v5.16.0...v5.17.0) (2026-05-14)
|
|
9
36
|
|
|
10
37
|
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) codin
|
|
|
17
17
|
- **Enforces allow / ask / deny** at tool-call time with UI confirmation dialogs
|
|
18
18
|
- **Controls bash commands** with wildcard pattern matching (`git *: ask`, `rm -rf *: deny`)
|
|
19
19
|
- **Gates MCP and skill access** at server, tool, and skill-name granularity
|
|
20
|
+
- **Protects sensitive file patterns** — cross-cutting `path` rules deny `.env`, `~/.ssh/*`, etc. across all tools and bash at once
|
|
20
21
|
- **Guards external paths** — prompts before file tools or bash commands reach outside `cwd`
|
|
21
22
|
- **Forwards prompts from subagents** — `ask` policies work even in non-UI execution contexts
|
|
22
23
|
|
|
@@ -91,7 +92,7 @@ For the full reference — all surfaces, runtime knobs, per-agent overrides, mer
|
|
|
91
92
|
|---|---|
|
|
92
93
|
|[docs/configuration.md](docs/configuration.md)|Full policy reference, runtime knobs, per-agent overrides, recipes|
|
|
93
94
|
|[docs/session-approvals.md](docs/session-approvals.md)|Session-scoped rules, pattern suggestions, bash arity table|
|
|
94
|
-
|[docs/
|
|
95
|
+
|[docs/cross-extension-api.md](docs/cross-extension-api.md)|Cross-extension service accessor, event bus integration, decision broadcasts|
|
|
95
96
|
|[docs/subagent-integration.md](docs/subagent-integration.md)|Permission forwarding, coexistence with subagent extensions|
|
|
96
97
|
|[docs/guides/permission-frontmatter-for-subagent-extensions.md](docs/guides/permission-frontmatter-for-subagent-extensions.md)|Convention guide for subagent extension authors|
|
|
97
98
|
|[docs/opencode-compatibility.md](docs/opencode-compatibility.md)|OpenCode compatibility — shared concepts, divergences, porting guide|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-permission-system",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.18.1",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/service.ts"
|
|
8
|
+
},
|
|
6
9
|
"files": [
|
|
7
10
|
"src",
|
|
8
11
|
"tests",
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PermissionGateHandler,
|
|
9
9
|
SessionLifecycleHandler,
|
|
10
10
|
} from "./handlers";
|
|
11
|
+
import { buildInputForSurface } from "./input-normalizer";
|
|
11
12
|
import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
12
13
|
import { registerPermissionRpcHandlers } from "./permission-event-rpc";
|
|
13
14
|
import { emitReadyEvent } from "./permission-events";
|
|
@@ -19,6 +20,11 @@ import {
|
|
|
19
20
|
refreshExtensionConfig,
|
|
20
21
|
saveExtensionConfig,
|
|
21
22
|
} from "./runtime";
|
|
23
|
+
import type { PermissionsService } from "./service";
|
|
24
|
+
import {
|
|
25
|
+
publishPermissionsService,
|
|
26
|
+
unpublishPermissionsService,
|
|
27
|
+
} from "./service";
|
|
22
28
|
import { createSessionLogger } from "./session-logger";
|
|
23
29
|
import { isSubagentExecutionContext } from "./subagent-context";
|
|
24
30
|
import {
|
|
@@ -91,6 +97,20 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
91
97
|
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
92
98
|
});
|
|
93
99
|
|
|
100
|
+
const permissionsService: PermissionsService = {
|
|
101
|
+
checkPermission(surface, value, agentName) {
|
|
102
|
+
const input = buildInputForSurface(surface, value);
|
|
103
|
+
const sessionRules = runtime.sessionRules.getRuleset();
|
|
104
|
+
return runtime.permissionManager.checkPermission(
|
|
105
|
+
surface,
|
|
106
|
+
input,
|
|
107
|
+
agentName,
|
|
108
|
+
sessionRules,
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
publishPermissionsService(permissionsService);
|
|
113
|
+
|
|
94
114
|
emitReadyEvent(pi.events);
|
|
95
115
|
|
|
96
116
|
const toolRegistry = {
|
|
@@ -101,6 +121,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
101
121
|
const lifecycle = new SessionLifecycleHandler(session, () => {
|
|
102
122
|
rpcHandles.unsubCheck();
|
|
103
123
|
rpcHandles.unsubPrompt();
|
|
124
|
+
unpublishPermissionsService();
|
|
104
125
|
});
|
|
105
126
|
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
106
127
|
const gates = new PermissionGateHandler(session, pi.events, toolRegistry);
|
package/src/input-normalizer.ts
CHANGED
|
@@ -2,6 +2,34 @@ import { toRecord } from "./common";
|
|
|
2
2
|
import { createMcpPermissionTargets } from "./mcp-targets";
|
|
3
3
|
import { getPathBearingToolPath, PATH_BEARING_TOOLS } from "./path-utils";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Construct a surface-appropriate input object from a raw value string.
|
|
7
|
+
*
|
|
8
|
+
* This is the inverse of `normalizeInput()` — it builds the minimal input
|
|
9
|
+
* object that `PermissionManager.checkPermission()` expects for a given
|
|
10
|
+
* surface, from a single string value.
|
|
11
|
+
*
|
|
12
|
+
* Used by the event-bus RPC handler and the `Symbol.for()` service accessor
|
|
13
|
+
* so external callers can query policy with `(surface, value)` instead of
|
|
14
|
+
* constructing a full tool-call input payload.
|
|
15
|
+
*
|
|
16
|
+
* Note: MCP inputs are complex (server name + tool name derivation). Callers
|
|
17
|
+
* providing an MCP surface receive a best-effort policy evaluation using the
|
|
18
|
+
* value as a pre-qualified target string. Pass the fully-qualified target
|
|
19
|
+
* (e.g. "exa:search" or "exa") directly.
|
|
20
|
+
*/
|
|
21
|
+
export function buildInputForSurface(
|
|
22
|
+
surface: string,
|
|
23
|
+
value: string | undefined,
|
|
24
|
+
): unknown {
|
|
25
|
+
const v = value ?? "";
|
|
26
|
+
if (surface === "bash") return { command: v };
|
|
27
|
+
if (surface === "skill") return { name: v };
|
|
28
|
+
if (surface === "external_directory") return { path: v };
|
|
29
|
+
// MCP and tool surfaces: normalizeInput handles them from the surface alone.
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
/**
|
|
6
34
|
* Surface-normalized representation of a tool invocation used by
|
|
7
35
|
* `checkPermission()` to feed a single `evaluateFirst()` call.
|
package/src/pattern-suggest.ts
CHANGED
|
@@ -69,6 +69,8 @@ function buildLabel(pattern: string, surface: string): string {
|
|
|
69
69
|
return `Yes, allow skill "${pattern}" for this session`;
|
|
70
70
|
case "external_directory":
|
|
71
71
|
return `Yes, allow access to external directory "${pattern}" for this session`;
|
|
72
|
+
case "path":
|
|
73
|
+
return `Yes, allow path "${pattern}" for this session`;
|
|
72
74
|
default:
|
|
73
75
|
// Path-bearing tools with a specific path pattern show the pattern.
|
|
74
76
|
if (PATH_BEARING_TOOLS.has(surface) && pattern !== "*") {
|
|
@@ -104,6 +106,9 @@ export function suggestSessionPattern(
|
|
|
104
106
|
case "external_directory":
|
|
105
107
|
pattern = deriveApprovalPattern(value);
|
|
106
108
|
break;
|
|
109
|
+
case "path":
|
|
110
|
+
pattern = deriveApprovalPattern(value);
|
|
111
|
+
break;
|
|
107
112
|
default:
|
|
108
113
|
// Path-bearing tools: derive a directory-scoped pattern from the path.
|
|
109
114
|
if (PATH_BEARING_TOOLS.has(surface) && value !== "*") {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* permission prompts without importing this package.
|
|
7
7
|
*/
|
|
8
8
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { buildInputForSurface } from "./input-normalizer";
|
|
9
10
|
import type {
|
|
10
11
|
PermissionPromptDecision,
|
|
11
12
|
RequestPermissionOptions,
|
|
@@ -79,26 +80,6 @@ function errorReply(error: string): PermissionsRpcReply {
|
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
/**
|
|
83
|
-
* Construct a surface-appropriate input object from a raw value string.
|
|
84
|
-
*
|
|
85
|
-
* Note: MCP inputs are complex (server name + tool name derivation). Callers
|
|
86
|
-
* providing an MCP surface receive a best-effort policy evaluation using the
|
|
87
|
-
* value as a pre-qualified target string. Pass the fully-qualified target
|
|
88
|
-
* (e.g. "exa:search" or "exa") directly.
|
|
89
|
-
*/
|
|
90
|
-
function buildInputForSurface(
|
|
91
|
-
surface: string,
|
|
92
|
-
value: string | undefined,
|
|
93
|
-
): unknown {
|
|
94
|
-
const v = value ?? "";
|
|
95
|
-
if (surface === "bash") return { command: v };
|
|
96
|
-
if (surface === "skill") return { name: v };
|
|
97
|
-
if (surface === "external_directory") return { path: v };
|
|
98
|
-
// MCP and tool surfaces: normalizeInput handles them from the surface alone.
|
|
99
|
-
return {};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
83
|
// ── RPC handler: permissions:rpc:check ────────────────────────────────────
|
|
103
84
|
|
|
104
85
|
function handleCheckRpc(
|
package/src/permission-events.ts
CHANGED
|
@@ -30,7 +30,19 @@ export const PERMISSIONS_READY_CHANNEL = "permissions:ready";
|
|
|
30
30
|
/** Emitted after every permission gate resolution. */
|
|
31
31
|
export const PERMISSIONS_DECISION_CHANNEL = "permissions:decision";
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* RPC request channel — query the permission policy (no prompting).
|
|
35
|
+
*
|
|
36
|
+
* @deprecated Use the `Symbol.for()`-backed service accessor instead:
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const { getPermissionsService } = await import("@gotgenes/pi-permission-system");
|
|
39
|
+
* const service = getPermissionsService();
|
|
40
|
+
* if (service) {
|
|
41
|
+
* const result = service.checkPermission("bash", "git push");
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
* The event-bus RPC remains available as a zero-dependency fallback.
|
|
45
|
+
*/
|
|
34
46
|
export const PERMISSIONS_RPC_CHECK_CHANNEL = "permissions:rpc:check";
|
|
35
47
|
|
|
36
48
|
/** RPC request channel — forward a permission prompt to the parent UI. */
|
|
@@ -88,7 +100,12 @@ export interface PermissionDecisionEvent {
|
|
|
88
100
|
|
|
89
101
|
// ── permissions:rpc:check ──────────────────────────────────────────────────
|
|
90
102
|
|
|
91
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Request payload for `permissions:rpc:check`.
|
|
105
|
+
*
|
|
106
|
+
* @deprecated Prefer `getPermissionsService().checkPermission()` from the
|
|
107
|
+
* service accessor module. See `PERMISSIONS_RPC_CHECK_CHANNEL` for details.
|
|
108
|
+
*/
|
|
92
109
|
export interface PermissionsCheckRequest {
|
|
93
110
|
requestId: string;
|
|
94
111
|
/** Permission surface to evaluate. */
|
|
@@ -99,7 +116,12 @@ export interface PermissionsCheckRequest {
|
|
|
99
116
|
agentName?: string;
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Data field in a successful `permissions:rpc:check` reply.
|
|
121
|
+
*
|
|
122
|
+
* @deprecated Prefer `getPermissionsService().checkPermission()` from the
|
|
123
|
+
* service accessor module. See `PERMISSIONS_RPC_CHECK_CHANNEL` for details.
|
|
124
|
+
*/
|
|
103
125
|
export interface PermissionsCheckReplyData {
|
|
104
126
|
result: "allow" | "deny" | "ask";
|
|
105
127
|
matchedPattern: string | null;
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-extension service accessor backed by `Symbol.for()` on `globalThis`.
|
|
3
|
+
*
|
|
4
|
+
* `Symbol.for()` is process-global by spec, so it survives jiti's per-extension
|
|
5
|
+
* module isolation (`moduleCache: false`). A consumer doing
|
|
6
|
+
* `import("@gotgenes/pi-permission-system")` gets a fresh module copy, but
|
|
7
|
+
* `getPermissionsService()` reads from the same `globalThis` slot the provider
|
|
8
|
+
* wrote to — enabling direct, synchronous, type-safe function calls.
|
|
9
|
+
*
|
|
10
|
+
* Best practice: call `getPermissionsService()` per use rather than caching the
|
|
11
|
+
* reference — this ensures resilience across `/reload` and load-order edge cases.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
15
|
+
|
|
16
|
+
export type { PermissionCheckResult, PermissionState };
|
|
17
|
+
|
|
18
|
+
/** Process-global key for the service slot. */
|
|
19
|
+
const SERVICE_KEY = Symbol.for("@gotgenes/pi-permission-system:service");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Public interface exposed to other extensions via `getPermissionsService()`.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors the simplified RPC signature — surface + optional value + optional
|
|
25
|
+
* agent name — and delegates to `PermissionManager.checkPermission()` with
|
|
26
|
+
* current session rules internally.
|
|
27
|
+
*/
|
|
28
|
+
export interface PermissionsService {
|
|
29
|
+
/**
|
|
30
|
+
* Query the permission policy for a surface and value.
|
|
31
|
+
*
|
|
32
|
+
* @param surface - Permission surface: "bash", "read", "mcp", "skill",
|
|
33
|
+
* "external_directory", etc.
|
|
34
|
+
* @param value - The value to evaluate: command string, tool name, skill
|
|
35
|
+
* name, or path. Omit or pass `undefined` for a
|
|
36
|
+
* surface-level query.
|
|
37
|
+
* @param agentName - Optional agent name for per-agent policy resolution.
|
|
38
|
+
* @returns Full check result including state, matched pattern, and origin.
|
|
39
|
+
*/
|
|
40
|
+
checkPermission(
|
|
41
|
+
surface: string,
|
|
42
|
+
value?: string,
|
|
43
|
+
agentName?: string,
|
|
44
|
+
): PermissionCheckResult;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Store a `PermissionsService` on `globalThis` so other extensions can
|
|
49
|
+
* retrieve it via `getPermissionsService()`.
|
|
50
|
+
*
|
|
51
|
+
* Overwrites any previously published service — safe for `/reload`.
|
|
52
|
+
*/
|
|
53
|
+
export function publishPermissionsService(service: PermissionsService): void {
|
|
54
|
+
(globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Retrieve the published `PermissionsService`, or `undefined` if the
|
|
59
|
+
* permission-system extension has not loaded (or has been unloaded).
|
|
60
|
+
*/
|
|
61
|
+
export function getPermissionsService(): PermissionsService | undefined {
|
|
62
|
+
return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
|
|
63
|
+
| PermissionsService
|
|
64
|
+
| undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remove the service from `globalThis`.
|
|
69
|
+
*
|
|
70
|
+
* Called during `session_shutdown` to avoid stale references after the
|
|
71
|
+
* extension is torn down.
|
|
72
|
+
*/
|
|
73
|
+
export function unpublishPermissionsService(): void {
|
|
74
|
+
delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
|
|
75
|
+
}
|
|
@@ -121,6 +121,21 @@ describe("suggestSessionPattern", () => {
|
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
describe("path surface", () => {
|
|
125
|
+
it("returns directory-scoped pattern for a file path", () => {
|
|
126
|
+
const result = suggestSessionPattern("path", "src/.env");
|
|
127
|
+
expect(result).toMatchObject({
|
|
128
|
+
surface: "path",
|
|
129
|
+
pattern: "src/*",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("label includes path pattern", () => {
|
|
134
|
+
const result = suggestSessionPattern("path", "src/.env");
|
|
135
|
+
expect(result.label).toBe('Yes, allow path "src/*" for this session');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
124
139
|
describe("path-bearing tool surfaces", () => {
|
|
125
140
|
it("returns directory-scoped pattern for read with a file path", () => {
|
|
126
141
|
const result = suggestSessionPattern("read", "/outside/project/file.ts");
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { buildInputForSurface } from "../src/input-normalizer";
|
|
3
|
+
import type { PermissionsService } from "../src/service";
|
|
4
|
+
import {
|
|
5
|
+
getPermissionsService,
|
|
6
|
+
publishPermissionsService,
|
|
7
|
+
unpublishPermissionsService,
|
|
8
|
+
} from "../src/service";
|
|
9
|
+
import type { PermissionCheckResult } from "../src/types";
|
|
10
|
+
|
|
11
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function makeService(
|
|
14
|
+
overrides: Partial<PermissionsService> = {},
|
|
15
|
+
): PermissionsService {
|
|
16
|
+
return {
|
|
17
|
+
checkPermission: vi.fn(),
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── globalThis accessor ────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe("globalThis accessor", () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
unpublishPermissionsService();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns undefined when nothing has been published", () => {
|
|
30
|
+
expect(getPermissionsService()).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns the published service", () => {
|
|
34
|
+
const service = makeService();
|
|
35
|
+
publishPermissionsService(service);
|
|
36
|
+
expect(getPermissionsService()).toBe(service);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("overwrites a previously published service", () => {
|
|
40
|
+
const first = makeService();
|
|
41
|
+
const second = makeService();
|
|
42
|
+
publishPermissionsService(first);
|
|
43
|
+
publishPermissionsService(second);
|
|
44
|
+
expect(getPermissionsService()).toBe(second);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns undefined after unpublish", () => {
|
|
48
|
+
const service = makeService();
|
|
49
|
+
publishPermissionsService(service);
|
|
50
|
+
unpublishPermissionsService();
|
|
51
|
+
expect(getPermissionsService()).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("unpublish is safe to call when nothing was published", () => {
|
|
55
|
+
expect(() => unpublishPermissionsService()).not.toThrow();
|
|
56
|
+
expect(getPermissionsService()).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ── service adapter delegation ─────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe("service adapter delegation", () => {
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
unpublishPermissionsService();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const fakeResult: PermissionCheckResult = {
|
|
68
|
+
toolName: "bash",
|
|
69
|
+
state: "allow",
|
|
70
|
+
matchedPattern: "git *",
|
|
71
|
+
source: "bash",
|
|
72
|
+
origin: "global",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
it("checkPermission delegates surface and value through buildInputForSurface", () => {
|
|
76
|
+
const checkPermission = vi.fn().mockReturnValue(fakeResult);
|
|
77
|
+
const sessionRules = [
|
|
78
|
+
{
|
|
79
|
+
surface: "bash",
|
|
80
|
+
pattern: "*",
|
|
81
|
+
action: "allow" as const,
|
|
82
|
+
layer: "session" as const,
|
|
83
|
+
origin: "session" as const,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// Build the adapter the same way index.ts will
|
|
88
|
+
const service: PermissionsService = {
|
|
89
|
+
checkPermission(surface, value, agentName) {
|
|
90
|
+
const input = buildInputForSurface(surface, value);
|
|
91
|
+
return checkPermission(surface, input, agentName, sessionRules);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
publishPermissionsService(service);
|
|
96
|
+
const retrieved = getPermissionsService()!;
|
|
97
|
+
const result = retrieved.checkPermission("bash", "git push");
|
|
98
|
+
|
|
99
|
+
expect(result).toBe(fakeResult);
|
|
100
|
+
expect(checkPermission).toHaveBeenCalledWith(
|
|
101
|
+
"bash",
|
|
102
|
+
{ command: "git push" },
|
|
103
|
+
undefined,
|
|
104
|
+
sessionRules,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("checkPermission passes agentName through", () => {
|
|
109
|
+
const checkPermission = vi.fn().mockReturnValue(fakeResult);
|
|
110
|
+
|
|
111
|
+
const service: PermissionsService = {
|
|
112
|
+
checkPermission(surface, value, agentName) {
|
|
113
|
+
const input = buildInputForSurface(surface, value);
|
|
114
|
+
return checkPermission(surface, input, agentName, []);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
publishPermissionsService(service);
|
|
119
|
+
getPermissionsService()!.checkPermission("skill", "my-skill", "Explore");
|
|
120
|
+
|
|
121
|
+
expect(checkPermission).toHaveBeenCalledWith(
|
|
122
|
+
"skill",
|
|
123
|
+
{ name: "my-skill" },
|
|
124
|
+
"Explore",
|
|
125
|
+
[],
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("checkPermission uses empty object for unknown surfaces", () => {
|
|
130
|
+
const checkPermission = vi.fn().mockReturnValue(fakeResult);
|
|
131
|
+
|
|
132
|
+
const service: PermissionsService = {
|
|
133
|
+
checkPermission(surface, value, agentName) {
|
|
134
|
+
const input = buildInputForSurface(surface, value);
|
|
135
|
+
return checkPermission(surface, input, agentName, []);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
publishPermissionsService(service);
|
|
140
|
+
getPermissionsService()!.checkPermission("read", "/tmp/file");
|
|
141
|
+
|
|
142
|
+
expect(checkPermission).toHaveBeenCalledWith("read", {}, undefined, []);
|
|
143
|
+
});
|
|
144
|
+
});
|