@gotgenes/pi-permission-system 19.0.1 → 20.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 +29 -0
- package/docs/configuration.md +3 -0
- package/docs/cross-extension-api.md +17 -126
- package/docs/guides/permission-frontmatter-for-subagent-extensions.md +18 -50
- package/docs/guides/upstream-issue-template.md +2 -2
- package/docs/subagent-integration.md +0 -1
- package/package.json +1 -1
- package/src/access-intent/bash/bash-path-resolver.ts +6 -1
- package/src/access-intent/bash/token-classification.ts +30 -1
- package/src/authority/approval-escalator.ts +37 -89
- package/src/authority/authorizer-selection.ts +87 -0
- package/src/authority/authorizer.ts +72 -0
- package/src/authority/denying-authorizer.ts +12 -0
- package/src/authority/forwarding-io.ts +0 -1
- package/src/authority/local-user-authorizer.ts +46 -0
- package/src/{permission-prompter.ts → authority/permission-prompter.ts} +23 -48
- package/src/authority/subagent-detection.ts +1 -1
- package/src/config-loader.ts +1 -1
- package/src/gate-prompter.ts +1 -1
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/index.ts +19 -34
- package/src/normalize.ts +1 -1
- package/src/path-normalizer.ts +12 -0
- package/src/permission-events.ts +7 -103
- package/src/permission-manager.ts +1 -1
- package/src/permission-session.ts +6 -7
- package/src/permission-ui-prompt.ts +1 -25
- package/src/service.ts +3 -8
- package/src/session-logger.ts +1 -1
- package/src/types.ts +20 -0
- package/src/value-guards.ts +0 -22
- package/src/permission-event-rpc.ts +0 -227
- package/src/prompting-gateway.ts +0 -89
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
3
|
+
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
4
|
+
import {
|
|
5
|
+
type Authorizer,
|
|
6
|
+
type AuthorizerSelectionDeps,
|
|
7
|
+
selectAuthorizer,
|
|
8
|
+
} from "./authorizer";
|
|
9
|
+
import type {
|
|
10
|
+
PermissionPrompterApi,
|
|
11
|
+
PromptPermissionDetails,
|
|
12
|
+
} from "./permission-prompter";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The lifecycle slice of the selection owner that PermissionSession drives.
|
|
16
|
+
*
|
|
17
|
+
* PermissionSession calls activate/deactivate to keep the selection's stored
|
|
18
|
+
* context in sync with its own — the same pattern the former
|
|
19
|
+
* PromptingGatewayLifecycle used.
|
|
20
|
+
*/
|
|
21
|
+
export interface AuthorizerSelectionLifecycle {
|
|
22
|
+
activate(ctx: ExtensionContext): void;
|
|
23
|
+
deactivate(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Context-owning selection root for the Authorizer spine.
|
|
28
|
+
*
|
|
29
|
+
* The rewrite of `PromptingGateway`: owns the stored `ExtensionContext`, runs
|
|
30
|
+
* `selectAuthorizer` once per activation, and implements `GatePrompter` by
|
|
31
|
+
* delegating to the selected `Authorizer` via `PermissionPrompter`.
|
|
32
|
+
*
|
|
33
|
+
* `canConfirm()` survives this step (dissolved in #556): it is recomputed
|
|
34
|
+
* transitionally alongside `selectAuthorizer`'s own branch, rather than
|
|
35
|
+
* derived from the selected authorizer, to keep the ask path byte-identical
|
|
36
|
+
* until #556 derives confirmability from a `DenyingAuthorizer` marker.
|
|
37
|
+
*/
|
|
38
|
+
export class AuthorizerSelection
|
|
39
|
+
implements GatePrompter, AuthorizerSelectionLifecycle
|
|
40
|
+
{
|
|
41
|
+
private selected: Authorizer | null = null;
|
|
42
|
+
private confirmable = false;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private readonly deps: AuthorizerSelectionDeps & {
|
|
46
|
+
prompter: PermissionPrompterApi;
|
|
47
|
+
},
|
|
48
|
+
) {}
|
|
49
|
+
|
|
50
|
+
/** Select the Authorizer for `ctx` and store both it and the confirmable predicate. */
|
|
51
|
+
activate(ctx: ExtensionContext): void {
|
|
52
|
+
this.selected = selectAuthorizer(ctx, this.deps);
|
|
53
|
+
this.confirmable = ctx.hasUI || this.deps.detection.isSubagent(ctx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Clear the stored selection. */
|
|
57
|
+
deactivate(): void {
|
|
58
|
+
this.selected = null;
|
|
59
|
+
this.confirmable = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether an interactive permission prompt can be shown.
|
|
64
|
+
*
|
|
65
|
+
* Returns false when no authorizer has been selected. Otherwise true when
|
|
66
|
+
* the context had UI or was a forwarding subagent at selection time — the
|
|
67
|
+
* two Authorizer-selection predicates, evaluated once at `activate`.
|
|
68
|
+
*/
|
|
69
|
+
canConfirm(): boolean {
|
|
70
|
+
return this.selected !== null && this.confirmable;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Prompt for a permission decision using the selected authorizer.
|
|
75
|
+
*
|
|
76
|
+
* Rejects if no authorizer has been selected — `canConfirm()` guards this
|
|
77
|
+
* in normal use. Implements {@link GatePrompter}.
|
|
78
|
+
*/
|
|
79
|
+
prompt(details: PromptPermissionDetails): Promise<PermissionPromptDecision> {
|
|
80
|
+
if (this.selected === null) {
|
|
81
|
+
return Promise.reject(
|
|
82
|
+
new Error("prompt called before the session was activated"),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return this.deps.prompter.prompt(this.selected, details);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
PermissionPromptDecision,
|
|
4
|
+
requestPermissionDecisionFromUi,
|
|
5
|
+
} from "#src/permission-dialog";
|
|
6
|
+
import type { PermissionEventBus } from "#src/permission-events";
|
|
7
|
+
import type { DebugReviewLogger } from "#src/session-logger";
|
|
8
|
+
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
9
|
+
import { ParentAuthorizer } from "./approval-escalator";
|
|
10
|
+
import { DenyingAuthorizer } from "./denying-authorizer";
|
|
11
|
+
import { LocalUserAuthorizer } from "./local-user-authorizer";
|
|
12
|
+
import type { PromptPermissionDetails } from "./permission-prompter";
|
|
13
|
+
import type { SubagentDetector } from "./subagent-detection";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The live-authority role: on `ask`, an `Authorizer` rules on a single
|
|
17
|
+
* request and is told the decision.
|
|
18
|
+
*
|
|
19
|
+
* One method, one responsibility. `DenyingAuthorizer` ignores `details`;
|
|
20
|
+
* `LocalUserAuthorizer` reads `message`/`sessionLabel` and derives the UI
|
|
21
|
+
* event from it; `ParentAuthorizer` reads `message` and derives the
|
|
22
|
+
* forwarded display from it.
|
|
23
|
+
*/
|
|
24
|
+
export interface Authorizer {
|
|
25
|
+
authorize(
|
|
26
|
+
details: PromptPermissionDetails,
|
|
27
|
+
): Promise<PermissionPromptDecision>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Construction inputs for {@link selectAuthorizer}. */
|
|
31
|
+
export interface AuthorizerSelectionDeps {
|
|
32
|
+
/** Single owner of subagent detection; the ParentAuthorizer-selection predicate. */
|
|
33
|
+
detection: SubagentDetector;
|
|
34
|
+
/** Event bus used by `LocalUserAuthorizer` for the `permissions:ui_prompt` broadcast. */
|
|
35
|
+
events: PermissionEventBus;
|
|
36
|
+
/** Injected for testability; production callers pass the real function. */
|
|
37
|
+
requestPermissionDecisionFromUi: typeof requestPermissionDecisionFromUi;
|
|
38
|
+
/** Forwarding directory `ParentAuthorizer` reads/writes request and response files under. */
|
|
39
|
+
forwardingDir: string;
|
|
40
|
+
/** In-process subagent session registry for forwarding target resolution. */
|
|
41
|
+
registry?: SubagentSessionRegistry;
|
|
42
|
+
logger: DebugReviewLogger;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Select the `Authorizer` for the current context: the single owner of the
|
|
47
|
+
* three-way `hasUI` / `isSubagent` / deny dispatch.
|
|
48
|
+
*
|
|
49
|
+
* Evaluated once per session activation (`AuthorizerSelection.activate`),
|
|
50
|
+
* replacing the re-derivation of the same predicates across
|
|
51
|
+
* `PromptingGateway`, `PermissionPrompter`, and `ApprovalEscalator`.
|
|
52
|
+
*/
|
|
53
|
+
export function selectAuthorizer(
|
|
54
|
+
ctx: ExtensionContext,
|
|
55
|
+
deps: AuthorizerSelectionDeps,
|
|
56
|
+
): Authorizer {
|
|
57
|
+
if (ctx.hasUI) {
|
|
58
|
+
return new LocalUserAuthorizer({
|
|
59
|
+
ui: ctx.ui,
|
|
60
|
+
events: deps.events,
|
|
61
|
+
requestPermissionDecisionFromUi: deps.requestPermissionDecisionFromUi,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (deps.detection.isSubagent(ctx)) {
|
|
65
|
+
return new ParentAuthorizer(ctx, {
|
|
66
|
+
forwardingDir: deps.forwardingDir,
|
|
67
|
+
registry: deps.registry,
|
|
68
|
+
logger: deps.logger,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return new DenyingAuthorizer();
|
|
72
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
2
|
+
import type { Authorizer } from "./authorizer";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Least-privilege Authorizer: no authority is reachable for this session
|
|
6
|
+
* (no UI, not a subagent), so every ask is denied.
|
|
7
|
+
*/
|
|
8
|
+
export class DenyingAuthorizer implements Authorizer {
|
|
9
|
+
authorize(): Promise<PermissionPromptDecision> {
|
|
10
|
+
return Promise.resolve({ approved: false, state: "denied" });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PermissionDecisionUi,
|
|
3
|
+
PermissionPromptDecision,
|
|
4
|
+
requestPermissionDecisionFromUi,
|
|
5
|
+
} from "#src/permission-dialog";
|
|
6
|
+
import {
|
|
7
|
+
emitUiPromptEvent,
|
|
8
|
+
type PermissionEventBus,
|
|
9
|
+
} from "#src/permission-events";
|
|
10
|
+
import { buildDirectUiPrompt } from "#src/permission-ui-prompt";
|
|
11
|
+
import type { Authorizer } from "./authorizer";
|
|
12
|
+
import type { PromptPermissionDetails } from "./permission-prompter";
|
|
13
|
+
|
|
14
|
+
/** Dependencies required by {@link LocalUserAuthorizer}. */
|
|
15
|
+
export interface LocalUserAuthorizerDeps {
|
|
16
|
+
/** The active session's UI surface. */
|
|
17
|
+
ui: PermissionDecisionUi;
|
|
18
|
+
/** Event bus used for the `permissions:ui_prompt` broadcast. */
|
|
19
|
+
events: PermissionEventBus;
|
|
20
|
+
/** Injected for testability; production callers pass the real function. */
|
|
21
|
+
requestPermissionDecisionFromUi: typeof requestPermissionDecisionFromUi;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Authorizer for a session with an active UI: prompt the human here.
|
|
26
|
+
*
|
|
27
|
+
* Emits the `permissions:ui_prompt` broadcast (moved here from
|
|
28
|
+
* `PermissionPrompter`'s `ctx.hasUI` arm) before showing the dialog, so
|
|
29
|
+
* observers know a decision is imminent.
|
|
30
|
+
*/
|
|
31
|
+
export class LocalUserAuthorizer implements Authorizer {
|
|
32
|
+
constructor(private readonly deps: LocalUserAuthorizerDeps) {}
|
|
33
|
+
|
|
34
|
+
authorize(
|
|
35
|
+
details: PromptPermissionDetails,
|
|
36
|
+
): Promise<PermissionPromptDecision> {
|
|
37
|
+
const uiPrompt = buildDirectUiPrompt(details);
|
|
38
|
+
emitUiPromptEvent(this.deps.events, uiPrompt);
|
|
39
|
+
return this.deps.requestPermissionDecisionFromUi(
|
|
40
|
+
this.deps.ui,
|
|
41
|
+
"Permission Required",
|
|
42
|
+
details.message,
|
|
43
|
+
details.sessionLabel ? { sessionLabel: details.sessionLabel } : undefined,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
emitUiPromptEvent,
|
|
6
|
-
type PermissionEventBus,
|
|
7
|
-
} from "./permission-events";
|
|
8
|
-
import { buildDirectUiPrompt } from "./permission-ui-prompt";
|
|
9
|
-
import type { ReviewLogger } from "./session-logger";
|
|
1
|
+
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
2
|
+
import type { ReviewLogger } from "#src/session-logger";
|
|
3
|
+
import type { Authorizer } from "./authorizer";
|
|
10
4
|
|
|
11
5
|
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
12
6
|
|
|
@@ -27,72 +21,53 @@ export interface PromptPermissionDetails {
|
|
|
27
21
|
sessionLabel?: string;
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Narrow seam onto {@link PermissionPrompter}.
|
|
26
|
+
*
|
|
27
|
+
* Kept separate from the concrete class so consumers (e.g. `AuthorizerSelection`)
|
|
28
|
+
* can inject a plain `{ prompt: vi.fn() }` mock in tests — a private field on
|
|
29
|
+
* the concrete class would create a nominal brand that a structural mock
|
|
30
|
+
* cannot satisfy without a cast.
|
|
31
|
+
*/
|
|
31
32
|
export interface PermissionPrompterApi {
|
|
32
33
|
prompt(
|
|
33
|
-
|
|
34
|
+
authorizer: Authorizer,
|
|
34
35
|
details: PromptPermissionDetails,
|
|
35
36
|
): Promise<PermissionPromptDecision>;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/**
|
|
39
|
-
* Dependencies required by PermissionPrompter.
|
|
40
|
-
*
|
|
41
|
-
* Keeps the prompter's external surface narrow: callers provide a review
|
|
42
|
-
* logger, the UI-prompt event bus, and the forwarder that owns the
|
|
43
|
-
* UI/subagent-forwarding branching logic.
|
|
44
|
-
*/
|
|
39
|
+
/** Dependencies required by {@link PermissionPrompter}. */
|
|
45
40
|
export interface PermissionPrompterDeps {
|
|
46
41
|
/** Write structured entries to the permission review log. */
|
|
47
42
|
logger: ReviewLogger;
|
|
48
|
-
/** Event bus used for UI prompt broadcasts. */
|
|
49
|
-
events: PermissionEventBus;
|
|
50
|
-
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
51
|
-
forwarder: ApprovalRequester;
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
/**
|
|
55
|
-
*
|
|
46
|
+
* Brackets the ask-path flow with review-log entries and delegates the
|
|
47
|
+
* live decision to the selected {@link Authorizer}:
|
|
56
48
|
* 1. Review-log "waiting" entry.
|
|
57
|
-
* 2.
|
|
49
|
+
* 2. `authorizer.authorize(details)`.
|
|
58
50
|
* 3. Review-log "approved" / "denied" entry.
|
|
59
51
|
*
|
|
52
|
+
* The UI/forwarding branching this class previously owned now lives on the
|
|
53
|
+
* individual `Authorizer` implementations (`LocalUserAuthorizer`,
|
|
54
|
+
* `ParentAuthorizer`, `DenyingAuthorizer`) — this class no longer threads
|
|
55
|
+
* `ExtensionContext` per call.
|
|
56
|
+
*
|
|
60
57
|
* Yolo-mode auto-approval happens upstream, at the composition stage
|
|
61
58
|
* (`PermissionManager.check`'s `rewriteAsksToYolo`) — an `ask` never reaches
|
|
62
59
|
* this class under yolo, so this class has no yolo-mode knowledge.
|
|
63
|
-
*
|
|
64
|
-
* Injecting a single PermissionPrompter instance means adding a new prompt
|
|
65
|
-
* parameter (e.g. a future sessionLabel variant) only requires changing
|
|
66
|
-
* PromptPermissionDetails and this class — not the full threading chain.
|
|
67
60
|
*/
|
|
68
61
|
export class PermissionPrompter implements PermissionPrompterApi {
|
|
69
62
|
constructor(private readonly deps: PermissionPrompterDeps) {}
|
|
70
63
|
|
|
71
64
|
async prompt(
|
|
72
|
-
|
|
65
|
+
authorizer: Authorizer,
|
|
73
66
|
details: PromptPermissionDetails,
|
|
74
67
|
): Promise<PermissionPromptDecision> {
|
|
75
68
|
this.writeReviewEntry("permission_request.waiting", details);
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
// when it does not (a forwarding subagent), the display fields ride along
|
|
79
|
-
// to the parent so the parent emits a non-degraded event from the
|
|
80
|
-
// forwarded path instead of here.
|
|
81
|
-
const uiPrompt = buildDirectUiPrompt(details);
|
|
82
|
-
if (ctx.hasUI) {
|
|
83
|
-
emitUiPromptEvent(this.deps.events, uiPrompt);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const decision = await this.deps.forwarder.requestApproval(
|
|
87
|
-
ctx,
|
|
88
|
-
details.message,
|
|
89
|
-
details.sessionLabel ? { sessionLabel: details.sessionLabel } : undefined,
|
|
90
|
-
{
|
|
91
|
-
source: uiPrompt.source,
|
|
92
|
-
surface: uiPrompt.surface,
|
|
93
|
-
value: uiPrompt.value,
|
|
94
|
-
},
|
|
95
|
-
);
|
|
70
|
+
const decision = await authorizer.authorize(details);
|
|
96
71
|
|
|
97
72
|
this.writeReviewEntry(
|
|
98
73
|
decision.approved
|
|
@@ -8,7 +8,7 @@ import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
|
8
8
|
/**
|
|
9
9
|
* Narrow seam for the ask-path consumers: "is the current session a subagent?"
|
|
10
10
|
*
|
|
11
|
-
* `
|
|
11
|
+
* `selectAuthorizer`/`AuthorizerSelection` and `ForwardingManager` depend on
|
|
12
12
|
* this single-method view so their unit tests inject a one-field fake without
|
|
13
13
|
* casts. It is the Authorizer-selection predicate the Phase 9 spine consumes.
|
|
14
14
|
*/
|
package/src/config-loader.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./config-schema";
|
|
15
15
|
import { mergeFlatPermissions } from "./permission-merge";
|
|
16
16
|
import type { FlatPermissionConfig, PatternValue } from "./types";
|
|
17
|
-
import { isDenyWithReason, isPermissionState } from "./
|
|
17
|
+
import { isDenyWithReason, isPermissionState } from "./types";
|
|
18
18
|
|
|
19
19
|
// The unified config shape is derived from the zod schema (config-schema.ts,
|
|
20
20
|
// the single source of truth) and re-exported so existing importers keep their
|
package/src/gate-prompter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { PromptPermissionDetails } from "./authority/permission-prompter";
|
|
1
2
|
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
2
|
-
import type { PromptPermissionDetails } from "./permission-prompter";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The prompting role the gate runner needs: a yes/no on whether an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { PromptPermissionDetails } from "#src/authority/permission-prompter";
|
|
1
2
|
import type { DenialContext } from "#src/denial-messages";
|
|
2
3
|
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
3
|
-
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
4
4
|
import type { SessionApproval } from "#src/session-approval";
|
|
5
5
|
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
6
6
|
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getAgentDir, getPackageDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
-
ApprovalEscalator,
|
|
5
|
-
type ApprovalEscalatorDeps,
|
|
6
|
-
} from "./authority/approval-escalator";
|
|
3
|
+
import { AuthorizerSelection } from "./authority/authorizer-selection";
|
|
7
4
|
import {
|
|
8
5
|
ForwardedRequestServer,
|
|
9
6
|
type ForwardedRequestServerDeps,
|
|
10
7
|
} from "./authority/forwarded-request-server";
|
|
8
|
+
import { PermissionPrompter } from "./authority/permission-prompter";
|
|
11
9
|
import { SubagentDetection } from "./authority/subagent-detection";
|
|
12
10
|
import { registerBuiltinToolInputFormatters } from "./builtin-tool-input-formatters";
|
|
13
11
|
import { registerPermissionSystemCommand } from "./config-modal";
|
|
@@ -28,13 +26,10 @@ import { SkillInputGatePipeline } from "./handlers/gates/skill-input-gate-pipeli
|
|
|
28
26
|
import { ToolCallGatePipeline } from "./handlers/gates/tool-call-gate-pipeline";
|
|
29
27
|
import { createFailClosedToolCall } from "./handlers/tool-call-boundary";
|
|
30
28
|
import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
31
|
-
import { registerPermissionRpcHandlers } from "./permission-event-rpc";
|
|
32
29
|
import { PermissionManager } from "./permission-manager";
|
|
33
|
-
import { PermissionPrompter } from "./permission-prompter";
|
|
34
30
|
import { PermissionResolver } from "./permission-resolver";
|
|
35
31
|
import { PermissionSession } from "./permission-session";
|
|
36
32
|
import { LocalPermissionsService } from "./permissions-service";
|
|
37
|
-
import { PromptingGateway } from "./prompting-gateway";
|
|
38
33
|
import { PermissionServiceLifecycle } from "./service-lifecycle";
|
|
39
34
|
import { PermissionSessionLogger } from "./session-logger";
|
|
40
35
|
import { SessionRules } from "./session-rules";
|
|
@@ -95,15 +90,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
95
90
|
logger,
|
|
96
91
|
});
|
|
97
92
|
|
|
98
|
-
const escalatorDeps: ApprovalEscalatorDeps = {
|
|
99
|
-
forwardingDir: paths.forwardingDir,
|
|
100
|
-
detection: subagentDetection,
|
|
101
|
-
registry: subagentRegistry,
|
|
102
|
-
logger,
|
|
103
|
-
requestPermissionDecisionFromUi,
|
|
104
|
-
};
|
|
105
|
-
const escalator = new ApprovalEscalator(escalatorDeps);
|
|
106
|
-
|
|
107
93
|
const requestServerDeps: ForwardedRequestServerDeps = {
|
|
108
94
|
forwardingDir: paths.forwardingDir,
|
|
109
95
|
logger,
|
|
@@ -113,14 +99,15 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
113
99
|
};
|
|
114
100
|
const requestServer = new ForwardedRequestServer(requestServerDeps);
|
|
115
101
|
|
|
116
|
-
const prompter = new PermissionPrompter({
|
|
117
|
-
logger,
|
|
118
|
-
events: pi.events,
|
|
119
|
-
forwarder: escalator,
|
|
120
|
-
});
|
|
102
|
+
const prompter = new PermissionPrompter({ logger });
|
|
121
103
|
|
|
122
|
-
const
|
|
104
|
+
const authorizerSelection = new AuthorizerSelection({
|
|
123
105
|
detection: subagentDetection,
|
|
106
|
+
events: pi.events,
|
|
107
|
+
requestPermissionDecisionFromUi,
|
|
108
|
+
forwardingDir: paths.forwardingDir,
|
|
109
|
+
registry: subagentRegistry,
|
|
110
|
+
logger,
|
|
124
111
|
prompter,
|
|
125
112
|
});
|
|
126
113
|
|
|
@@ -130,7 +117,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
130
117
|
permissionManager,
|
|
131
118
|
sessionRules,
|
|
132
119
|
configStore,
|
|
133
|
-
|
|
120
|
+
authorizerSelection,
|
|
134
121
|
hostPlatform,
|
|
135
122
|
);
|
|
136
123
|
|
|
@@ -150,17 +137,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
150
137
|
});
|
|
151
138
|
|
|
152
139
|
// Resolver composes the manager + session ruleset and owns the
|
|
153
|
-
// access-path → path-values unwrap; the
|
|
154
|
-
// queries through it, so it is constructed before
|
|
140
|
+
// access-path → path-values unwrap; the service routes its policy
|
|
141
|
+
// queries through it, so it is constructed before it.
|
|
155
142
|
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
156
143
|
|
|
157
|
-
const rpcHandles = registerPermissionRpcHandlers(pi.events, {
|
|
158
|
-
resolver,
|
|
159
|
-
session,
|
|
160
|
-
requestPermissionDecisionFromUi,
|
|
161
|
-
logger,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
144
|
const permissionsService = new LocalPermissionsService(
|
|
165
145
|
resolver,
|
|
166
146
|
session,
|
|
@@ -184,7 +164,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
184
164
|
permissionsService,
|
|
185
165
|
subagentDetection,
|
|
186
166
|
pi.events,
|
|
187
|
-
[
|
|
167
|
+
[unsubSubagentLifecycle],
|
|
188
168
|
);
|
|
189
169
|
|
|
190
170
|
const toolRegistry = {
|
|
@@ -204,7 +184,12 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
204
184
|
const agentPrep = new AgentPrepHandler(session, resolver, toolRegistry);
|
|
205
185
|
|
|
206
186
|
const reporter = new GateDecisionReporter(logger, pi.events);
|
|
207
|
-
const gateRunner = new GateRunner(
|
|
187
|
+
const gateRunner = new GateRunner(
|
|
188
|
+
resolver,
|
|
189
|
+
sessionRules,
|
|
190
|
+
authorizerSelection,
|
|
191
|
+
reporter,
|
|
192
|
+
);
|
|
208
193
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
209
194
|
resolver,
|
|
210
195
|
session,
|
package/src/normalize.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Rule, Ruleset } from "./rule";
|
|
2
2
|
import type { FlatPermissionConfig } from "./types";
|
|
3
|
-
import { isDenyWithReason, isPermissionState } from "./
|
|
3
|
+
import { isDenyWithReason, isPermissionState } from "./types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Convert a flat permission config into a Ruleset.
|
package/src/path-normalizer.ts
CHANGED
|
@@ -112,6 +112,18 @@ export class PathNormalizer {
|
|
|
112
112
|
return this.impl.isAbsolute(pathValue);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* True when the host platform treats a backslash as a path separator (win32).
|
|
117
|
+
*
|
|
118
|
+
* The bash rule-candidate classifier reads this to decide whether a
|
|
119
|
+
* backslash-relative token (`dir\file`) is path-shaped: on win32 `\` is a
|
|
120
|
+
* separator, but on POSIX it is a legal filename character (#520). Keeping the
|
|
121
|
+
* decision here means no bash-layer module re-reads `process.platform`.
|
|
122
|
+
*/
|
|
123
|
+
usesWindowsSeparators(): boolean {
|
|
124
|
+
return this.platform === "win32";
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
/**
|
|
116
128
|
* Interpret a literal `cd` target's effect on the effective base.
|
|
117
129
|
*
|
package/src/permission-events.ts
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission event channel — public contract.
|
|
3
3
|
*
|
|
4
|
-
* Exports channel name constants,
|
|
5
|
-
*
|
|
4
|
+
* Exports channel name constants, TypeScript types for all emitted events,
|
|
5
|
+
* and thin emit helpers.
|
|
6
6
|
*
|
|
7
7
|
* Stability guarantee: fields may be added, but existing fields will not be
|
|
8
8
|
* removed or renamed without a semver-major version bump.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
/** Minimal event bus interface required by the emit helpers
|
|
11
|
+
/** Minimal event bus interface required by the emit helpers. */
|
|
12
12
|
export interface PermissionEventBus {
|
|
13
13
|
emit(channel: string, data: unknown): void;
|
|
14
14
|
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// ── Protocol version ───────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* RPC protocol version.
|
|
21
|
-
* Bumped when the envelope shape or method contracts change in a breaking way.
|
|
22
|
-
*/
|
|
23
|
-
export const PERMISSIONS_PROTOCOL_VERSION = 1;
|
|
24
|
-
|
|
25
17
|
// ── Channel name constants ─────────────────────────────────────────────────
|
|
26
18
|
|
|
27
19
|
/** Emitted at `session_start`, after the service is published. */
|
|
@@ -33,43 +25,14 @@ export const PERMISSIONS_UI_PROMPT_CHANNEL = "permissions:ui_prompt";
|
|
|
33
25
|
/** Emitted after every permission gate resolution. */
|
|
34
26
|
export const PERMISSIONS_DECISION_CHANNEL = "permissions:decision";
|
|
35
27
|
|
|
36
|
-
/**
|
|
37
|
-
* RPC request channel — query the permission policy (no prompting).
|
|
38
|
-
*
|
|
39
|
-
* @deprecated Use the `Symbol.for()`-backed service accessor instead:
|
|
40
|
-
* ```typescript
|
|
41
|
-
* const { getPermissionsService } = await import("@gotgenes/pi-permission-system");
|
|
42
|
-
* const service = getPermissionsService();
|
|
43
|
-
* if (service) {
|
|
44
|
-
* const result = service.checkPermission("bash", "git push");
|
|
45
|
-
* }
|
|
46
|
-
* ```
|
|
47
|
-
* The event-bus RPC remains available as a zero-dependency fallback.
|
|
48
|
-
*/
|
|
49
|
-
export const PERMISSIONS_RPC_CHECK_CHANNEL = "permissions:rpc:check";
|
|
50
|
-
|
|
51
|
-
/** RPC request channel — forward a permission prompt to the parent UI. */
|
|
52
|
-
export const PERMISSIONS_RPC_PROMPT_CHANNEL = "permissions:rpc:prompt";
|
|
53
|
-
|
|
54
|
-
// ── Shared RPC envelope ────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Standard RPC reply envelope.
|
|
58
|
-
* Success: `{ success: true, protocolVersion, data? }`.
|
|
59
|
-
* Error: `{ success: false, protocolVersion, error }`.
|
|
60
|
-
*/
|
|
61
|
-
export type PermissionsRpcReply<T = void> =
|
|
62
|
-
| { success: true; protocolVersion: number; data?: T }
|
|
63
|
-
| { success: false; protocolVersion: number; error: string };
|
|
64
|
-
|
|
65
28
|
// ── permissions:ready ──────────────────────────────────────────────────────
|
|
66
29
|
|
|
67
30
|
/**
|
|
68
31
|
* Payload emitted on `permissions:ready`.
|
|
69
32
|
*
|
|
70
|
-
* Intentionally empty: the channel is a readiness signal.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
33
|
+
* Intentionally empty: the channel is a readiness signal. There is no
|
|
34
|
+
* `protocolVersion` — the published types plus package semver define the
|
|
35
|
+
* broadcast contract.
|
|
73
36
|
*/
|
|
74
37
|
export type PermissionsReadyEvent = Record<string, never>;
|
|
75
38
|
|
|
@@ -85,8 +48,7 @@ export type PermissionsReadyEvent = Record<string, never>;
|
|
|
85
48
|
export type PermissionUiPromptSource =
|
|
86
49
|
| "tool_call"
|
|
87
50
|
| "skill_input"
|
|
88
|
-
| "skill_read"
|
|
89
|
-
| "rpc_prompt";
|
|
51
|
+
| "skill_read";
|
|
90
52
|
|
|
91
53
|
/** Forwarding context, present only when a prompt was forwarded from a non-UI subagent. */
|
|
92
54
|
export interface ForwardedPromptContext {
|
|
@@ -155,64 +117,6 @@ export interface PermissionDecisionEvent {
|
|
|
155
117
|
matchedPattern: string | null;
|
|
156
118
|
}
|
|
157
119
|
|
|
158
|
-
// ── permissions:rpc:check ──────────────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Request payload for `permissions:rpc:check`.
|
|
162
|
-
*
|
|
163
|
-
* @deprecated Prefer `getPermissionsService().checkPermission()` from the
|
|
164
|
-
* service accessor module. See `PERMISSIONS_RPC_CHECK_CHANNEL` for details.
|
|
165
|
-
*/
|
|
166
|
-
export interface PermissionsCheckRequest {
|
|
167
|
-
requestId: string;
|
|
168
|
-
/** Permission surface to evaluate. */
|
|
169
|
-
surface: string;
|
|
170
|
-
/** The value to evaluate: command string, tool name, skill name, or path. */
|
|
171
|
-
value?: string;
|
|
172
|
-
/** Optional agent name for per-agent policy resolution. */
|
|
173
|
-
agentName?: string;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Data field in a successful `permissions:rpc:check` reply.
|
|
178
|
-
*
|
|
179
|
-
* @deprecated Prefer `getPermissionsService().checkPermission()` from the
|
|
180
|
-
* service accessor module. See `PERMISSIONS_RPC_CHECK_CHANNEL` for details.
|
|
181
|
-
*/
|
|
182
|
-
export interface PermissionsCheckReplyData {
|
|
183
|
-
result: "allow" | "deny" | "ask";
|
|
184
|
-
matchedPattern: string | null;
|
|
185
|
-
origin: string | null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── permissions:rpc:prompt ─────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
/** Request payload for `permissions:rpc:prompt`. */
|
|
191
|
-
export interface PermissionsPromptRequest {
|
|
192
|
-
requestId: string;
|
|
193
|
-
/** Permission surface being evaluated. */
|
|
194
|
-
surface: string;
|
|
195
|
-
/** Value being evaluated (shown in the dialog). */
|
|
196
|
-
value: string;
|
|
197
|
-
/** Optional agent name for display. */
|
|
198
|
-
agentName?: string;
|
|
199
|
-
/** Message to display in the permission dialog. */
|
|
200
|
-
message: string;
|
|
201
|
-
/** Optional label for the "for this session" option. */
|
|
202
|
-
sessionLabel?: string;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Data field in a successful `permissions:rpc:prompt` reply. */
|
|
206
|
-
export interface PermissionsPromptReplyData {
|
|
207
|
-
approved: boolean;
|
|
208
|
-
/**
|
|
209
|
-
* Detailed state: "approved", "approved_for_session",
|
|
210
|
-
* "denied", or "denied_with_reason".
|
|
211
|
-
*/
|
|
212
|
-
state: string;
|
|
213
|
-
denialReason?: string;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
120
|
// ── Emit helpers ───────────────────────────────────────────────────────────
|
|
217
121
|
|
|
218
122
|
/**
|