@gotgenes/pi-permission-system 10.5.0 → 10.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +11 -6
- package/src/handlers/lifecycle.ts +7 -4
- package/src/handlers/permission-gate-handler.ts +3 -3
- package/src/index.ts +8 -3
- package/src/input-normalizer.ts +20 -8
- package/src/path-utils.ts +1 -10
- package/src/permission-resolver.ts +0 -3
- package/src/permission-session.ts +8 -52
- package/src/session-rules.ts +3 -2
- package/src/skill-prompt-sanitizer.ts +1 -1
- package/test/before-agent-start-cache.test.ts +89 -0
- package/test/handlers/before-agent-start.test.ts +56 -86
- package/test/handlers/external-directory-session-dedup.test.ts +175 -159
- package/test/handlers/gates/bash-path.test.ts +57 -0
- package/test/handlers/gates/path.test.ts +58 -0
- package/test/handlers/input.test.ts +5 -4
- package/test/handlers/lifecycle.test.ts +79 -85
- package/test/handlers/tool-call.test.ts +106 -2
- package/test/helpers/handler-fixtures.ts +99 -102
- package/test/helpers/manager-harness.ts +61 -0
- package/test/helpers/session-fixtures.ts +192 -0
- package/test/input-normalizer.test.ts +77 -1
- package/test/logging.test.ts +51 -0
- package/test/path-utils.test.ts +10 -0
- package/test/permission-forwarding.test.ts +73 -0
- package/test/permission-manager-unified.test.ts +1577 -3
- package/test/permission-resolver.test.ts +3 -1
- package/test/permission-session.test.ts +14 -198
- package/test/session-rules.test.ts +13 -5
- package/test/skill-prompt-sanitizer.test.ts +130 -0
- package/test/status.test.ts +10 -0
- package/test/system-prompt-sanitizer.test.ts +68 -0
- package/test/tool-registry.test.ts +42 -0
- package/test/yolo-mode.test.ts +78 -0
- package/src/agent-prep-session.ts +0 -28
- package/src/gate-handler-session.ts +0 -13
- package/src/session-lifecycle-session.ts +0 -24
- package/test/permission-system.test.ts +0 -2785
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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
|
+
## [10.5.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.5.1...pi-permission-system-v10.5.2) (2026-06-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** expand $HOME in normalizePathForComparison ([#350](https://github.com/gotgenes/pi-packages/issues/350)) ([1b92ed3](https://github.com/gotgenes/pi-packages/commit/1b92ed3d2364174d3287171c58ce8452239b3e8d))
|
|
14
|
+
* **pi-permission-system:** home-expand path values before matching ([#350](https://github.com/gotgenes/pi-packages/issues/350)) ([48a7b37](https://github.com/gotgenes/pi-packages/commit/48a7b3783857b449442d30edefe04f8255e5f4f8))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* **pi-permission-system:** note path values are home-expanded for matching ([#350](https://github.com/gotgenes/pi-packages/issues/350)) ([e9c264d](https://github.com/gotgenes/pi-packages/commit/e9c264de85d327a0bfbcd84401a259cb509a5dfa))
|
|
20
|
+
|
|
21
|
+
## [10.5.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.5.0...pi-permission-system-v10.5.1) (2026-06-07)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
* correct SkillPermissionChecker comment after resolver rewire ([#341](https://github.com/gotgenes/pi-packages/issues/341)) ([1528382](https://github.com/gotgenes/pi-packages/commit/15283820a920fead92b348410828332b69f0a0d9))
|
|
27
|
+
|
|
8
28
|
## [10.5.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.4.0...pi-permission-system-v10.5.0) (2026-06-07)
|
|
9
29
|
|
|
10
30
|
|
package/package.json
CHANGED
|
@@ -2,11 +2,12 @@ import type {
|
|
|
2
2
|
BeforeAgentStartEventResult,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { AgentPrepSession } from "#src/agent-prep-session";
|
|
6
5
|
import {
|
|
7
6
|
createActiveToolsCacheKey,
|
|
8
7
|
createBeforeAgentStartPromptStateKey,
|
|
9
8
|
} from "#src/before-agent-start-cache";
|
|
9
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
10
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
10
11
|
import { resolveSkillPromptEntries } from "#src/skill-prompt-sanitizer";
|
|
11
12
|
import { sanitizeAvailableToolsSection } from "#src/system-prompt-sanitizer";
|
|
12
13
|
import { getToolNameFromValue, type ToolRegistry } from "#src/tool-registry";
|
|
@@ -35,12 +36,14 @@ export function shouldExposeTool(
|
|
|
35
36
|
* Handles the `before_agent_start` event: tool filtering + prompt sanitization.
|
|
36
37
|
*
|
|
37
38
|
* Constructor deps:
|
|
38
|
-
* - `session` — encapsulates all mutable session state
|
|
39
|
+
* - `session` — encapsulates all mutable session state and lifecycle operations
|
|
40
|
+
* - `resolver` — owns permission-query surface: `getToolPermission`, `getPolicyCacheStamp`, skill check
|
|
39
41
|
* - `toolRegistry` — Pi tool API subset (getAll + setActive)
|
|
40
42
|
*/
|
|
41
43
|
export class AgentPrepHandler {
|
|
42
44
|
constructor(
|
|
43
|
-
private readonly session:
|
|
45
|
+
private readonly session: PermissionSession,
|
|
46
|
+
private readonly resolver: PermissionResolver,
|
|
44
47
|
private readonly toolRegistry: ToolRegistry,
|
|
45
48
|
) {}
|
|
46
49
|
|
|
@@ -63,7 +66,7 @@ export class AgentPrepHandler {
|
|
|
63
66
|
}
|
|
64
67
|
if (
|
|
65
68
|
shouldExposeTool(toolName, agentName, (t, a) =>
|
|
66
|
-
this.
|
|
69
|
+
this.resolver.getToolPermission(t, a),
|
|
67
70
|
)
|
|
68
71
|
) {
|
|
69
72
|
allowedTools.push(toolName);
|
|
@@ -79,7 +82,9 @@ export class AgentPrepHandler {
|
|
|
79
82
|
const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
|
|
80
83
|
agentName,
|
|
81
84
|
cwd: ctx.cwd,
|
|
82
|
-
permissionStamp: this.
|
|
85
|
+
permissionStamp: this.resolver.getPolicyCacheStamp(
|
|
86
|
+
agentName ?? undefined,
|
|
87
|
+
),
|
|
83
88
|
systemPrompt: event.systemPrompt,
|
|
84
89
|
allowedToolNames: allowedTools,
|
|
85
90
|
});
|
|
@@ -96,7 +101,7 @@ export class AgentPrepHandler {
|
|
|
96
101
|
);
|
|
97
102
|
const skillPromptResult = resolveSkillPromptEntries(
|
|
98
103
|
toolPromptResult.prompt,
|
|
99
|
-
this.
|
|
104
|
+
this.resolver,
|
|
100
105
|
agentName,
|
|
101
106
|
ctx.cwd,
|
|
102
107
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
3
5
|
import type { ServiceLifecycle } from "#src/service-lifecycle";
|
|
4
|
-
import type { SessionLifecycleSession } from "#src/session-lifecycle-session";
|
|
5
6
|
import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/status";
|
|
6
7
|
|
|
7
8
|
/** Minimal subset of SessionStartEvent used by this handler. */
|
|
@@ -18,14 +19,16 @@ interface ResourcesDiscoverPayload {
|
|
|
18
19
|
* Handles session lifecycle events: start, reload, and shutdown.
|
|
19
20
|
*
|
|
20
21
|
* Constructor deps:
|
|
21
|
-
* - `session` — encapsulates all mutable session state
|
|
22
|
+
* - `session` — encapsulates all mutable session state and lifecycle operations
|
|
23
|
+
* - `resolver` — owns permission-query surface: `getConfigIssues`
|
|
22
24
|
* - `serviceLifecycle` — owns the process-global service publication;
|
|
23
25
|
* `activate` publishes (skipped for registered subagent children) and emits
|
|
24
26
|
* the ready event; `teardown` unsubscribes all session listeners and unpublishes
|
|
25
27
|
*/
|
|
26
28
|
export class SessionLifecycleHandler {
|
|
27
29
|
constructor(
|
|
28
|
-
private readonly session:
|
|
30
|
+
private readonly session: PermissionSession,
|
|
31
|
+
private readonly resolver: PermissionResolver,
|
|
29
32
|
private readonly serviceLifecycle: ServiceLifecycle,
|
|
30
33
|
) {}
|
|
31
34
|
|
|
@@ -38,7 +41,7 @@ export class SessionLifecycleHandler {
|
|
|
38
41
|
this.session.logResolvedConfigPaths();
|
|
39
42
|
|
|
40
43
|
const agentName = this.session.resolveAgentName(ctx);
|
|
41
|
-
const policyIssues = this.
|
|
44
|
+
const policyIssues = this.resolver.getConfigIssues(agentName ?? undefined);
|
|
42
45
|
for (const issue of policyIssues) {
|
|
43
46
|
this.session.logger.warn(issue);
|
|
44
47
|
}
|
|
@@ -4,11 +4,11 @@ import type {
|
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
import { toRecord } from "#src/common";
|
|
7
|
-
import type { GateHandlerSession } from "#src/gate-handler-session";
|
|
8
7
|
import {
|
|
9
8
|
formatMissingToolNameReason,
|
|
10
9
|
formatUnknownToolReason,
|
|
11
10
|
} from "#src/permission-prompts";
|
|
11
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
12
12
|
import {
|
|
13
13
|
checkRequestedToolRegistration,
|
|
14
14
|
getToolNameFromValue,
|
|
@@ -31,7 +31,7 @@ interface InputPayload {
|
|
|
31
31
|
* Handles permission gate events: tool_call and input.
|
|
32
32
|
*
|
|
33
33
|
* Constructor deps:
|
|
34
|
-
* - `session` —
|
|
34
|
+
* - `session` — state/lifecycle owner: bind per-event context, resolve agent name
|
|
35
35
|
* - `toolRegistry` — Pi tool API subset (getAll + setActive)
|
|
36
36
|
* - `pipeline` — owns tool-call gate-producer assembly and the run loop
|
|
37
37
|
* - `skillInputPipeline` — owns skill-input gate assembly (pre-check, notify, run)
|
|
@@ -39,7 +39,7 @@ interface InputPayload {
|
|
|
39
39
|
*/
|
|
40
40
|
export class PermissionGateHandler {
|
|
41
41
|
constructor(
|
|
42
|
-
private readonly session:
|
|
42
|
+
private readonly session: PermissionSession,
|
|
43
43
|
private readonly toolRegistry: ToolRegistry,
|
|
44
44
|
private readonly pipeline: ToolCallGatePipeline,
|
|
45
45
|
private readonly skillInputPipeline: SkillInputGatePipeline,
|
package/src/index.ts
CHANGED
|
@@ -157,12 +157,17 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
157
157
|
setActive: (names: string[]) => pi.setActiveTools(names),
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
|
|
161
|
-
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
162
160
|
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
163
161
|
|
|
162
|
+
const lifecycle = new SessionLifecycleHandler(
|
|
163
|
+
session,
|
|
164
|
+
resolver,
|
|
165
|
+
serviceLifecycle,
|
|
166
|
+
);
|
|
167
|
+
const agentPrep = new AgentPrepHandler(session, resolver, toolRegistry);
|
|
168
|
+
|
|
164
169
|
const reporter = new GateDecisionReporter(session.logger, pi.events);
|
|
165
|
-
const gateRunner = new GateRunner(resolver,
|
|
170
|
+
const gateRunner = new GateRunner(resolver, sessionRules, gateway, reporter);
|
|
166
171
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
167
172
|
resolver,
|
|
168
173
|
session,
|
package/src/input-normalizer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { toRecord } from "./common";
|
|
1
|
+
import { getNonEmptyString, toRecord } from "./common";
|
|
2
|
+
import { expandHomePath } from "./expand-home";
|
|
2
3
|
import { createMcpPermissionTargets } from "./mcp-targets";
|
|
3
|
-
import {
|
|
4
|
+
import { PATH_BEARING_TOOLS } from "./path-utils";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Construct a surface-appropriate input object from a raw value string.
|
|
@@ -66,13 +67,11 @@ export function normalizeInput(
|
|
|
66
67
|
input: unknown,
|
|
67
68
|
configuredMcpServerNames: readonly string[],
|
|
68
69
|
): NormalizedInput {
|
|
69
|
-
// --- Special surfaces (external_directory) ---
|
|
70
|
+
// --- Special surfaces (path, external_directory) ---
|
|
70
71
|
if (SPECIAL_PERMISSION_KEYS.has(toolName)) {
|
|
71
|
-
const record = toRecord(input);
|
|
72
|
-
const pathValue = typeof record.path === "string" ? record.path : null;
|
|
73
72
|
return {
|
|
74
73
|
surface: toolName,
|
|
75
|
-
values: [
|
|
74
|
+
values: [normalizePathSurfaceValue(input)],
|
|
76
75
|
resultExtras: {},
|
|
77
76
|
};
|
|
78
77
|
}
|
|
@@ -116,10 +115,9 @@ export function normalizeInput(
|
|
|
116
115
|
|
|
117
116
|
// --- Path-bearing tools (read, write, edit, grep, find, ls) ---
|
|
118
117
|
if (PATH_BEARING_TOOLS.has(toolName)) {
|
|
119
|
-
const path = getPathBearingToolPath(toolName, input);
|
|
120
118
|
return {
|
|
121
119
|
surface: toolName,
|
|
122
|
-
values: [
|
|
120
|
+
values: [normalizePathSurfaceValue(input)],
|
|
123
121
|
resultExtras: {},
|
|
124
122
|
};
|
|
125
123
|
}
|
|
@@ -131,3 +129,17 @@ export function normalizeInput(
|
|
|
131
129
|
resultExtras: {},
|
|
132
130
|
};
|
|
133
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract and home-expand the `input.path` lookup value shared by every path
|
|
135
|
+
* surface (`path`, `external_directory`, and the path-bearing tools).
|
|
136
|
+
*
|
|
137
|
+
* Missing, empty, or whitespace-only paths collapse to the surface catch-all
|
|
138
|
+
* `"*"`; otherwise `~/…` and `$HOME/…` prefixes are expanded to the OS home
|
|
139
|
+
* directory so values match home-anchored patterns symmetrically with how
|
|
140
|
+
* `compileWildcardPattern` expands the patterns themselves (#350).
|
|
141
|
+
*/
|
|
142
|
+
function normalizePathSurfaceValue(input: unknown): string {
|
|
143
|
+
const path = getNonEmptyString(toRecord(input).path);
|
|
144
|
+
return path === null ? "*" : expandHomePath(path);
|
|
145
|
+
}
|
package/src/path-utils.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
1
|
import { join, normalize, resolve, sep } from "node:path";
|
|
3
2
|
|
|
4
3
|
import { getNonEmptyString, toRecord } from "./common";
|
|
@@ -15,15 +14,7 @@ export function normalizePathForComparison(
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
let normalizedPath = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
18
|
-
|
|
19
|
-
if (normalizedPath === "~") {
|
|
20
|
-
normalizedPath = homedir();
|
|
21
|
-
} else if (
|
|
22
|
-
normalizedPath.startsWith("~/") ||
|
|
23
|
-
normalizedPath.startsWith("~\\")
|
|
24
|
-
) {
|
|
25
|
-
normalizedPath = join(homedir(), normalizedPath.slice(2));
|
|
26
|
-
}
|
|
17
|
+
normalizedPath = expandHomePath(normalizedPath);
|
|
27
18
|
|
|
28
19
|
const absolutePath = resolve(cwd, normalizedPath);
|
|
29
20
|
const normalizedAbsolutePath = normalize(absolutePath);
|
|
@@ -67,17 +67,14 @@ export class PermissionResolver implements ScopedPermissionResolver {
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// fallow-ignore-next-line unused-class-member
|
|
71
70
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
72
71
|
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
// fallow-ignore-next-line unused-class-member
|
|
76
74
|
getConfigIssues(agentName?: string): string[] {
|
|
77
75
|
return this.permissionManager.getConfigIssues(agentName);
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
// fallow-ignore-next-line unused-class-member
|
|
81
78
|
getPolicyCacheStamp(agentName?: string): string {
|
|
82
79
|
return this.permissionManager.getPolicyCacheStamp(agentName);
|
|
83
80
|
}
|
|
@@ -4,18 +4,15 @@ import {
|
|
|
4
4
|
getActiveAgentName,
|
|
5
5
|
getActiveAgentNameFromSystemPrompt,
|
|
6
6
|
} from "./active-agent";
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import type { SessionConfigStore } from "./config-store";
|
|
9
9
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
10
10
|
import type { ExtensionPaths } from "./extension-paths";
|
|
11
11
|
import type { ForwardingController } from "./forwarding-manager";
|
|
12
|
-
import type {
|
|
12
|
+
import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipeline";
|
|
13
13
|
import type { ScopedPermissionManager } from "./permission-manager";
|
|
14
14
|
import type { PromptingGatewayLifecycle } from "./prompting-gateway";
|
|
15
|
-
|
|
16
|
-
import type { SessionApproval } from "./session-approval";
|
|
17
|
-
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
18
|
-
import type { SessionLifecycleSession } from "./session-lifecycle-session";
|
|
15
|
+
|
|
19
16
|
import type { SessionLogger } from "./session-logger";
|
|
20
17
|
import type { SessionRules } from "./session-rules";
|
|
21
18
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
@@ -23,7 +20,6 @@ import {
|
|
|
23
20
|
resolveToolPreviewLimits,
|
|
24
21
|
type ToolPreviewFormatterOptions,
|
|
25
22
|
} from "./tool-preview-formatter";
|
|
26
|
-
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
27
23
|
|
|
28
24
|
/**
|
|
29
25
|
* Encapsulates all mutable session state and exposes operations instead of
|
|
@@ -40,13 +36,7 @@ import type { PermissionCheckResult, PermissionState } from "./types";
|
|
|
40
36
|
* - `SessionConfigStore` — owns extension config; provides refresh, log, read
|
|
41
37
|
* - `PromptingGatewayLifecycle` — prompting lifecycle forwarded via activate/deactivate
|
|
42
38
|
*/
|
|
43
|
-
export class PermissionSession
|
|
44
|
-
implements
|
|
45
|
-
SessionApprovalRecorder,
|
|
46
|
-
GateHandlerSession,
|
|
47
|
-
AgentPrepSession,
|
|
48
|
-
SessionLifecycleSession
|
|
49
|
-
{
|
|
39
|
+
export class PermissionSession implements ToolCallGateInputs {
|
|
50
40
|
private context: ExtensionContext | null = null;
|
|
51
41
|
private skillEntries: SkillPromptEntry[] = [];
|
|
52
42
|
private knownAgentName: string | null = null;
|
|
@@ -84,44 +74,6 @@ export class PermissionSession
|
|
|
84
74
|
return this.context;
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
// ── Permission checking (delegates to PermissionManager) ───────────────
|
|
88
|
-
|
|
89
|
-
checkPermission(
|
|
90
|
-
surface: string,
|
|
91
|
-
input: unknown,
|
|
92
|
-
agentName?: string,
|
|
93
|
-
sessionRules?: Rule[],
|
|
94
|
-
): PermissionCheckResult {
|
|
95
|
-
return this.permissionManager.checkPermission(
|
|
96
|
-
surface,
|
|
97
|
-
input,
|
|
98
|
-
agentName,
|
|
99
|
-
sessionRules,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
104
|
-
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
getConfigIssues(agentName?: string): string[] {
|
|
108
|
-
return this.permissionManager.getConfigIssues(agentName);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
getPolicyCacheStamp(agentName?: string): string {
|
|
112
|
-
return this.permissionManager.getPolicyCacheStamp(agentName);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Session rules (delegates to SessionRules) ──────────────────────────
|
|
116
|
-
|
|
117
|
-
getSessionRuleset(): Rule[] {
|
|
118
|
-
return this.sessionRules.getRuleset();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
recordSessionApproval(approval: SessionApproval): void {
|
|
122
|
-
this.sessionRules.record(approval);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
77
|
// ── Session lifecycle ────────────────────────────────────────────────────
|
|
126
78
|
|
|
127
79
|
/**
|
|
@@ -212,6 +164,10 @@ export class PermissionSession
|
|
|
212
164
|
return this.knownAgentName;
|
|
213
165
|
}
|
|
214
166
|
|
|
167
|
+
// Read by config-modal (`controller.session.lastKnownActiveAgentName`).
|
|
168
|
+
// fallow cannot trace the getter through the command's object-literal
|
|
169
|
+
// wiring, so it reports a false positive here.
|
|
170
|
+
// fallow-ignore-next-line unused-class-member
|
|
215
171
|
get lastKnownActiveAgentName(): string | null {
|
|
216
172
|
return this.knownAgentName;
|
|
217
173
|
}
|
package/src/session-rules.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { dirname, sep } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import type { Ruleset } from "./rule";
|
|
4
4
|
import type { SessionApproval } from "./session-approval";
|
|
5
|
+
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Ephemeral in-memory store of session-scoped permission approvals.
|
|
@@ -11,7 +12,7 @@ import type { SessionApproval } from "./session-approval";
|
|
|
11
12
|
*
|
|
12
13
|
* Cleared on session_shutdown — never persisted to disk.
|
|
13
14
|
*/
|
|
14
|
-
export class SessionRules {
|
|
15
|
+
export class SessionRules implements SessionApprovalRecorder {
|
|
15
16
|
private rules: Ruleset = [];
|
|
16
17
|
|
|
17
18
|
/** Record a wildcard pattern as approved for the given surface. */
|
|
@@ -36,7 +37,7 @@ export class SessionRules {
|
|
|
36
37
|
* The loop lives here so callers never need to know whether an approval
|
|
37
38
|
* carries one pattern or many — they just tell the store to record it.
|
|
38
39
|
*/
|
|
39
|
-
|
|
40
|
+
recordSessionApproval(approval: SessionApproval): void {
|
|
40
41
|
for (const pattern of approval.patterns) {
|
|
41
42
|
this.approve(approval.surface, pattern);
|
|
42
43
|
}
|
|
@@ -8,7 +8,7 @@ import type { PermissionCheckResult, PermissionState } from "./types";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Narrow interface for the permission checker used by skill prompt resolution.
|
|
11
|
-
* Both `PermissionManager` and `
|
|
11
|
+
* Both `PermissionManager` and `PermissionResolver` satisfy this structurally.
|
|
12
12
|
*/
|
|
13
13
|
export interface SkillPermissionChecker {
|
|
14
14
|
checkPermission(
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createActiveToolsCacheKey,
|
|
5
|
+
createBeforeAgentStartPromptStateKey,
|
|
6
|
+
shouldApplyCachedAgentStartState,
|
|
7
|
+
} from "#src/before-agent-start-cache";
|
|
8
|
+
import { createManager } from "#test/helpers/manager-harness";
|
|
9
|
+
|
|
10
|
+
test("Before-agent-start cache dedupes unchanged active-tool exposure and prompt state", () => {
|
|
11
|
+
const allowedTools = ["read", "mcp"];
|
|
12
|
+
const activeToolsKey = createActiveToolsCacheKey(allowedTools);
|
|
13
|
+
const promptStateKey = createBeforeAgentStartPromptStateKey({
|
|
14
|
+
agentName: "code",
|
|
15
|
+
cwd: "C:/workspace/project",
|
|
16
|
+
permissionStamp: "permissions-v1",
|
|
17
|
+
systemPrompt: "Available tools:\n- read\n- mcp",
|
|
18
|
+
allowedToolNames: allowedTools,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(shouldApplyCachedAgentStartState(null, activeToolsKey)).toBe(true);
|
|
22
|
+
expect(shouldApplyCachedAgentStartState(activeToolsKey, activeToolsKey)).toBe(
|
|
23
|
+
false,
|
|
24
|
+
);
|
|
25
|
+
expect(shouldApplyCachedAgentStartState(null, promptStateKey)).toBe(true);
|
|
26
|
+
expect(shouldApplyCachedAgentStartState(promptStateKey, promptStateKey)).toBe(
|
|
27
|
+
false,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("Before-agent-start prompt cache invalidates on permission changes while runtime enforcement stays authoritative", () => {
|
|
32
|
+
const { manager, globalConfigPath, cleanup } = createManager({
|
|
33
|
+
permission: { "*": "allow", write: "deny" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const baselineStamp = manager.getPolicyCacheStamp();
|
|
38
|
+
const baselineKey = createBeforeAgentStartPromptStateKey({
|
|
39
|
+
agentName: null,
|
|
40
|
+
cwd: "C:/workspace/project",
|
|
41
|
+
permissionStamp: baselineStamp,
|
|
42
|
+
systemPrompt: "Available tools:\n- read\n- write",
|
|
43
|
+
allowedToolNames: ["read"],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(shouldApplyCachedAgentStartState(baselineKey, baselineKey)).toBe(
|
|
47
|
+
false,
|
|
48
|
+
);
|
|
49
|
+
expect(manager.checkPermission("write", {}, undefined).state).toBe("deny");
|
|
50
|
+
|
|
51
|
+
const updatedConfig = `${JSON.stringify(
|
|
52
|
+
{ permission: { "*": "allow", write: "allow" } },
|
|
53
|
+
null,
|
|
54
|
+
2,
|
|
55
|
+
)}\n`;
|
|
56
|
+
|
|
57
|
+
let updatedStamp = baselineStamp;
|
|
58
|
+
for (
|
|
59
|
+
let attempt = 0;
|
|
60
|
+
attempt < 10 && updatedStamp === baselineStamp;
|
|
61
|
+
attempt += 1
|
|
62
|
+
) {
|
|
63
|
+
const waitUntil = Date.now() + 2;
|
|
64
|
+
while (Date.now() < waitUntil) {
|
|
65
|
+
// Wait for the filesystem timestamp granularity to advance.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
writeFileSync(globalConfigPath, updatedConfig, "utf8");
|
|
69
|
+
updatedStamp = manager.getPolicyCacheStamp();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
expect(updatedStamp).not.toBe(baselineStamp);
|
|
73
|
+
|
|
74
|
+
const invalidatedKey = createBeforeAgentStartPromptStateKey({
|
|
75
|
+
agentName: null,
|
|
76
|
+
cwd: "C:/workspace/project",
|
|
77
|
+
permissionStamp: updatedStamp,
|
|
78
|
+
systemPrompt: "Available tools:\n- read\n- write",
|
|
79
|
+
allowedToolNames: ["read", "write"],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(shouldApplyCachedAgentStartState(baselineKey, invalidatedKey)).toBe(
|
|
83
|
+
true,
|
|
84
|
+
);
|
|
85
|
+
expect(manager.checkPermission("write", {}, undefined).state).toBe("allow");
|
|
86
|
+
} finally {
|
|
87
|
+
cleanup();
|
|
88
|
+
}
|
|
89
|
+
});
|