@gotgenes/pi-permission-system 33.0.3 → 33.0.4
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 +12 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/config/config-issue-reporter.ts +60 -0
- package/src/config/config-loader.ts +3 -5
- package/src/config/config-store.ts +36 -22
- package/src/handlers/lifecycle.ts +13 -1
- package/src/handlers/session-turn-prep.ts +9 -0
- package/src/index.ts +13 -2
- package/src/session/permission-session.ts +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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
|
+
## [33.0.4](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.3...pi-permission-system-v33.0.4) (2026-09-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** show a config warning present at session start ([65a59f7](https://github.com/gotgenes/pi-packages/commit/65a59f717fba9823cde171c9fbfdcd322dcc6ec7)), closes [#933](https://github.com/gotgenes/pi-packages/issues/933)
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* **pi-permission-system:** declare non-Pi hosts a conditional non-goal ([b9d645c](https://github.com/gotgenes/pi-packages/commit/b9d645cdf39df10afe5493ca1817df3a1c99c527)), closes [#922](https://github.com/gotgenes/pi-packages/issues/922)
|
|
18
|
+
* **pi-permission-system:** describe the config warning's delivery ([6a9024f](https://github.com/gotgenes/pi-packages/commit/6a9024f4db193f592a321a32c45979dedc2985e8)), closes [#933](https://github.com/gotgenes/pi-packages/issues/933)
|
|
19
|
+
|
|
8
20
|
## [33.0.3](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.2...pi-permission-system-v33.0.3) (2026-09-19)
|
|
9
21
|
|
|
10
22
|
|
package/README.md
CHANGED
|
@@ -183,6 +183,11 @@ Hardening the gates against bypass, fail-closed corrections (breaking ones inclu
|
|
|
183
183
|
- _Model judgment in the core._
|
|
184
184
|
This package makes no LLM call and holds no model config; model-assisted judging attaches as a chain link over the authorizer seam instead.
|
|
185
185
|
A link decides nothing until you name it in `authorizerChain`, and its `allow` on an excluded surface is downgraded to `defer`.
|
|
186
|
+
- _Supporting a non-Pi host._
|
|
187
|
+
Built and validated against Pi's extension API and no other: a fork that loads Pi extensions — [Oh My Pi](https://github.com/can1357/oh-my-pi) among them — diverges in payload shape, tool vocabulary, and approval authority, on its own release schedule.
|
|
188
|
+
Input that violates a contract this package already reads is hardened against anyway, because defensive normalization is correct whoever sent it.
|
|
189
|
+
Modeling a foreign host's semantics is not, because a guarantee that cannot be executed against is worse than a declined one.
|
|
190
|
+
This one is conditional rather than permanent — the architecture doc names the five conditions that would make a second host a goal.
|
|
186
191
|
|
|
187
192
|
The [architecture doc](https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/architecture/architecture.md#scope-and-non-goals) carries the full inventory, with the decision record behind each entry.
|
|
188
193
|
|
|
@@ -194,6 +199,7 @@ The companion question — whether a capability model replaces the actor-keyed s
|
|
|
194
199
|
**Where adjacent requests belong.**
|
|
195
200
|
True isolation of a permitted action → an agent sandbox, which this package's scope decisions are exported to rather than duplicated in.
|
|
196
201
|
Model-assisted judging of an `ask` → a chain link over the authorizer seam; [@gotgenes/pi-permission-model-judge](https://www.npmjs.com/package/@gotgenes/pi-permission-model-judge) is the first-party one, and judges mistyped paths.
|
|
202
|
+
A non-Pi host's own payload shapes and tool formats → that host's Pi-compatibility layer, where one fix reaches every Pi extension at once instead of one.
|
|
197
203
|
Approve-and-steer, edit diffs, and risk explanations → a downstream package over the `permissions:decision` event and the presentation seams.
|
|
198
204
|
|
|
199
205
|
## Documentation
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-issue-reporter.ts — Tell the operator what is wrong with their config,
|
|
3
|
+
* once per issue, for as long as it is wrong.
|
|
4
|
+
*
|
|
5
|
+
* `ConfigStore` loads and answers; this decides whether the operator has
|
|
6
|
+
* already heard it. Splitting the two is what fixes #933: the store was primed
|
|
7
|
+
* at factory time, with no context to notify through, and recorded the warning
|
|
8
|
+
* as delivered anyway — so the identical warning at `session_start` was deduped
|
|
9
|
+
* away and every config issue reached the debug log alone.
|
|
10
|
+
*
|
|
11
|
+
* Delivery goes through the injected `warn` seam rather than a ctx parameter.
|
|
12
|
+
* That seam is `SessionLogger.warn`, which reaches the operator through
|
|
13
|
+
* `PermissionSession.notify` and the context the session owns — the same path
|
|
14
|
+
* every other session-lifecycle warning in this package takes. A reporter
|
|
15
|
+
* cannot be handed a context that does not exist, which is the whole point.
|
|
16
|
+
*
|
|
17
|
+
* The latch is per issue and the delivery is per report: a config with three
|
|
18
|
+
* detector hits produces one notification, and a later report announces only
|
|
19
|
+
* what is new. `reported` is *replaced* on every report rather than added to,
|
|
20
|
+
* so an issue the operator fixed and then reintroduced is announced again —
|
|
21
|
+
* which preserves the clearing behavior the store's single-string dedupe had,
|
|
22
|
+
* at per-issue granularity.
|
|
23
|
+
*
|
|
24
|
+
* Driven at both moments the operator is reachable: `session_start`, and every
|
|
25
|
+
* `before_agent_start` (the config is re-read there, so an issue created
|
|
26
|
+
* mid-session is caught on the next turn).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The config seam this reads (ISP): the issues current as of the last load. */
|
|
30
|
+
export interface ConfigIssueSource {
|
|
31
|
+
getConfigIssues(): readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The log seam this writes (ISP): one operator-facing warning. */
|
|
35
|
+
export interface ConfigIssueWarner {
|
|
36
|
+
warn(message: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The seam the session-start and turn-prep handlers drive. */
|
|
40
|
+
export interface ConfigIssueReporting {
|
|
41
|
+
report(): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ConfigIssueReporter implements ConfigIssueReporting {
|
|
45
|
+
private reported: ReadonlySet<string> = new Set();
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly source: ConfigIssueSource,
|
|
49
|
+
private readonly log: ConfigIssueWarner,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
report(): void {
|
|
53
|
+
const current = this.source.getConfigIssues();
|
|
54
|
+
const unreported = current.filter((issue) => !this.reported.has(issue));
|
|
55
|
+
if (unreported.length > 0) {
|
|
56
|
+
this.log.warn(unreported.join("\n"));
|
|
57
|
+
}
|
|
58
|
+
this.reported = new Set(current);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -468,11 +468,9 @@ export function detectDeprecatedPreviewCaps(
|
|
|
468
468
|
* make a display preference a policy event. The decision keeps its default
|
|
469
469
|
* letter, and the message names the entry, the reason, and the letter kept.
|
|
470
470
|
*
|
|
471
|
-
* Where that message surfaces is the caller's problem
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
* log alone. That predates this detector and swallows its two siblings the same
|
|
475
|
-
* way (#933).
|
|
471
|
+
* Where that message surfaces is the caller's problem: `ConfigIssueReporter`
|
|
472
|
+
* warns it at session start and on any turn whose refresh first finds it,
|
|
473
|
+
* latched per issue so a persisting one is not repeated (#933).
|
|
476
474
|
*
|
|
477
475
|
* Pure, following {@link detectPermissiveBashFallback}: it takes the merged
|
|
478
476
|
* config and returns a message; the caller owns pushing it onto the issue list.
|
|
@@ -6,11 +6,9 @@ import {
|
|
|
6
6
|
writeFileSync,
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { dirname, normalize } from "node:path";
|
|
9
|
-
import type {
|
|
10
|
-
ExtensionCommandContext,
|
|
11
|
-
ExtensionContext,
|
|
12
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
10
|
import type { DebugReviewLogger } from "#src/logging/session-logger";
|
|
11
|
+
import type { ConfigIssueSource } from "./config-issue-reporter";
|
|
14
12
|
import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
|
|
15
13
|
import {
|
|
16
14
|
getGlobalConfigPath,
|
|
@@ -40,7 +38,7 @@ export interface ConfigReader {
|
|
|
40
38
|
* coupling between the class and test doubles.
|
|
41
39
|
*/
|
|
42
40
|
export interface SessionConfigStore extends ConfigReader {
|
|
43
|
-
refresh(
|
|
41
|
+
refresh(cwd: string | undefined, projectTrusted: boolean): void;
|
|
44
42
|
logResolvedPaths(cwd?: string): void;
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -73,14 +71,17 @@ export interface ConfigStoreDeps {
|
|
|
73
71
|
*
|
|
74
72
|
* Replaces the three `(runtime, …)` config free functions
|
|
75
73
|
* (`refreshExtensionConfig`, `saveExtensionConfig`, `logResolvedConfigPaths`)
|
|
76
|
-
* with methods that privately own `config` and
|
|
74
|
+
* with methods that privately own `config` and the issue list the last load
|
|
75
|
+
* produced.
|
|
77
76
|
*
|
|
78
77
|
* Implements {@link ConfigReader} so consumers that only read the current config
|
|
79
78
|
* can depend on the narrow interface rather than the full class.
|
|
80
79
|
*/
|
|
81
|
-
export class ConfigStore
|
|
80
|
+
export class ConfigStore
|
|
81
|
+
implements SessionConfigStore, CommandConfigStore, ConfigIssueSource
|
|
82
|
+
{
|
|
82
83
|
private config: PermissionSystemExtensionConfig;
|
|
83
|
-
private
|
|
84
|
+
private configIssues: readonly string[] = [];
|
|
84
85
|
|
|
85
86
|
constructor(private readonly deps: ConfigStoreDeps) {
|
|
86
87
|
this.config = { ...DEFAULT_EXTENSION_CONFIG };
|
|
@@ -91,16 +92,39 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
|
|
|
91
92
|
return this.config;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* What is wrong with the config as of the last {@link refresh}.
|
|
97
|
+
*
|
|
98
|
+
* Every issue `loadAndMergeConfigs` collects: a legacy-file notice, a zod
|
|
99
|
+
* field violation, and the cross-cutting detectors (a permissive bash
|
|
100
|
+
* fallback, a deprecated preview cap, a refused dialog-key binding).
|
|
101
|
+
*
|
|
102
|
+
* This store answers; `ConfigIssueReporter` decides whether the operator has
|
|
103
|
+
* heard it yet (#933). Not to be confused with
|
|
104
|
+
* `PermissionResolver.getConfigIssues(agentName?)`, which answers for the
|
|
105
|
+
* *policy* files rather than the extension config.
|
|
106
|
+
*/
|
|
107
|
+
getConfigIssues(): readonly string[] {
|
|
108
|
+
return this.configIssues;
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
/**
|
|
95
112
|
* Reload merged config from disk.
|
|
96
113
|
*
|
|
97
|
-
*
|
|
114
|
+
* `cwd` scopes the project-level lookup; omit it when no session cwd is
|
|
115
|
+
* known yet (the factory-time priming load).
|
|
98
116
|
* When `projectTrusted` is `false`, the project scope is withheld so an
|
|
99
117
|
* untrusted repository's runtime config (`yoloMode`, `permissionReviewLog`,
|
|
100
118
|
* …) cannot loosen the operator's global config (#644).
|
|
119
|
+
*
|
|
120
|
+
* Takes no `ExtensionContext` on purpose: a load that holds one acquires UI
|
|
121
|
+
* side effects it cannot honor when there is no session yet, which is how a
|
|
122
|
+
* config warning came to be recorded as delivered without being shown
|
|
123
|
+
* (#933). `PermissionSession.refreshConfig` syncs the status bar and
|
|
124
|
+
* `ConfigIssueReporter` tells the operator; this reads files and answers
|
|
125
|
+
* questions about them.
|
|
101
126
|
*/
|
|
102
|
-
refresh(
|
|
103
|
-
const cwd = ctx?.cwd ?? null;
|
|
127
|
+
refresh(cwd: string | undefined, projectTrusted: boolean): void {
|
|
104
128
|
const mergeResult = loadAndMergeConfigs(
|
|
105
129
|
this.deps.agentDir,
|
|
106
130
|
cwd ?? "",
|
|
@@ -110,20 +134,11 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
|
|
|
110
134
|
const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
|
|
111
135
|
this.config = runtimeConfig;
|
|
112
136
|
|
|
113
|
-
|
|
114
|
-
syncPermissionSystemStatus(ctx, runtimeConfig);
|
|
115
|
-
}
|
|
137
|
+
this.configIssues = mergeResult.issues;
|
|
116
138
|
|
|
117
139
|
const warning =
|
|
118
140
|
mergeResult.issues.length > 0 ? mergeResult.issues.join("\n") : undefined;
|
|
119
141
|
|
|
120
|
-
if (warning && warning !== this.lastConfigWarning) {
|
|
121
|
-
this.lastConfigWarning = warning;
|
|
122
|
-
ctx?.ui.notify(warning, "warning");
|
|
123
|
-
} else if (!warning) {
|
|
124
|
-
this.lastConfigWarning = null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
142
|
this.deps.logger.debug("config.loaded", {
|
|
128
143
|
warning: warning ?? null,
|
|
129
144
|
debugLog: runtimeConfig.debugLog,
|
|
@@ -177,7 +192,6 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
|
|
|
177
192
|
|
|
178
193
|
this.config = normalized;
|
|
179
194
|
syncPermissionSystemStatus(ctx, normalized);
|
|
180
|
-
this.lastConfigWarning = null;
|
|
181
195
|
|
|
182
196
|
this.deps.logger.debug("config.saved", {
|
|
183
197
|
debugLog: normalized.debugLog,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ConfigIssueReporting } from "#src/config/config-issue-reporter";
|
|
2
3
|
import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/config/status";
|
|
3
4
|
import type { DecisionSummaryWriter } from "#src/logging/decision-audit";
|
|
4
5
|
import type { SessionLogger } from "#src/logging/session-logger";
|
|
@@ -36,6 +37,9 @@ export const UNTRUSTED_PROJECT_MESSAGE =
|
|
|
36
37
|
* the ready event; `teardown` unsubscribes all session listeners and unpublishes
|
|
37
38
|
* - `logger` — injected directly; replaces the former `session.logger` reach-through
|
|
38
39
|
* - `audit` — per-session decision counters; its summary is written on shutdown
|
|
40
|
+
* - `configIssues` — reports what is wrong with the extension config, latched
|
|
41
|
+
* per issue; driven here and on every turn, so an issue already on disk when
|
|
42
|
+
* the session opens is shown rather than swallowed (#933)
|
|
39
43
|
*/
|
|
40
44
|
export class SessionLifecycleHandler {
|
|
41
45
|
constructor(
|
|
@@ -44,6 +48,7 @@ export class SessionLifecycleHandler {
|
|
|
44
48
|
private readonly serviceLifecycle: ServiceLifecycle,
|
|
45
49
|
private readonly logger: SessionLogger,
|
|
46
50
|
private readonly audit: DecisionSummaryWriter,
|
|
51
|
+
private readonly configIssues: ConfigIssueReporting,
|
|
47
52
|
) {}
|
|
48
53
|
|
|
49
54
|
handleSessionStart(
|
|
@@ -51,9 +56,16 @@ export class SessionLifecycleHandler {
|
|
|
51
56
|
ctx: ExtensionContext,
|
|
52
57
|
): Promise<void> {
|
|
53
58
|
const projectTrusted = ctx.isProjectTrusted();
|
|
54
|
-
|
|
59
|
+
// Reset first: it activates the session, binding the context that
|
|
60
|
+
// `PermissionSession.notify` — and therefore `logger.warn` — delivers
|
|
61
|
+
// through. A refresh before activation has no UI to report into, which is
|
|
62
|
+
// how a config warning present at session start went unseen (#933).
|
|
55
63
|
this.session.resetForNewSession(ctx, projectTrusted);
|
|
64
|
+
this.session.refreshConfig(ctx, projectTrusted);
|
|
56
65
|
this.session.logResolvedConfigPaths();
|
|
66
|
+
// The config was just re-read above, and the session is activated, so a
|
|
67
|
+
// warning has a UI to reach.
|
|
68
|
+
this.configIssues.report();
|
|
57
69
|
if (!projectTrusted) {
|
|
58
70
|
this.warnProjectUntrusted(ctx, "session_start");
|
|
59
71
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ConfigIssueReporting } from "#src/config/config-issue-reporter";
|
|
2
3
|
import type { ReadyAnnouncer } from "#src/service/service-lifecycle";
|
|
3
4
|
|
|
4
5
|
/** The session surface the turn-prep routine drives. */
|
|
@@ -27,6 +28,9 @@ export interface TurnPreparation {
|
|
|
27
28
|
* - `warmParser` — warms the tree-sitter parser so the synchronous advisory
|
|
28
29
|
* bash path can decompose at gate parity; `before_agent_start` precedes any
|
|
29
30
|
* tool call, so triggering it here closes the pre-warm window (#309)
|
|
31
|
+
* - `configIssues` — reports what the config refresh found, latched per issue,
|
|
32
|
+
* so an issue the operator created mid-session is shown on the next turn
|
|
33
|
+
* without an unchanged one being repeated every turn (#933)
|
|
30
34
|
* - `readyAnnouncer` — re-announces `permissions:ready` once per session
|
|
31
35
|
* (ADR 0012 decision 3); `before_agent_start` runs after every extension's
|
|
32
36
|
* `session_start` and before any ask, so a consumer that registers from the
|
|
@@ -37,6 +41,7 @@ export class SessionTurnPrep implements TurnPreparation {
|
|
|
37
41
|
private readonly session: TurnPrepSession,
|
|
38
42
|
private readonly warmParser: () => void,
|
|
39
43
|
private readonly readyAnnouncer: ReadyAnnouncer,
|
|
44
|
+
private readonly configIssues: ConfigIssueReporting,
|
|
40
45
|
) {}
|
|
41
46
|
|
|
42
47
|
prepare(ctx: ExtensionContext): void {
|
|
@@ -50,6 +55,10 @@ export class SessionTurnPrep implements TurnPreparation {
|
|
|
50
55
|
// right before agent start after session_start withheld it (#644). The
|
|
51
56
|
// session_start handler already warned; do not re-warn on every start.
|
|
52
57
|
this.session.refreshConfig(ctx, ctx.isProjectTrusted());
|
|
58
|
+
// Say what that refresh found, before the announce below hands this node
|
|
59
|
+
// to a consumer: the operator hears about a config they just broke on the
|
|
60
|
+
// next turn, and hears an unchanged issue only once.
|
|
61
|
+
this.configIssues.report();
|
|
53
62
|
// Announce last: the node is up to date for the turn, so a consumer that
|
|
54
63
|
// resolves the service in its ready handler queries current policy. The
|
|
55
64
|
// once-per-session guard lives in the announcer, not here.
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { SubagentDetection } from "#src/authority/subagent-detection";
|
|
34
34
|
import { subscribeSubagentLifecycle } from "#src/authority/subagent-lifecycle-events";
|
|
35
35
|
import { getSubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
36
|
+
import { ConfigIssueReporter } from "#src/config/config-issue-reporter";
|
|
36
37
|
import { registerPermissionSystemCommand } from "#src/config/config-modal";
|
|
37
38
|
import { getGlobalConfigPath } from "#src/config/config-paths";
|
|
38
39
|
import { ConfigStore } from "#src/config/config-store";
|
|
@@ -236,8 +237,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
236
237
|
// refresh() must run after `session` is assigned: a debug-write IO failure
|
|
237
238
|
// triggers the logger's notify sink — `session.notify(m)` — which no-ops
|
|
238
239
|
// on the null context but requires `session` to be bound.
|
|
239
|
-
// No
|
|
240
|
-
// scope (fail closed); session_start reloads with the real
|
|
240
|
+
// No cwd or trust decision exists at factory init, so there is no project
|
|
241
|
+
// scope to withhold from (fail closed); session_start reloads with the real
|
|
242
|
+
// cwd and trust decision. This load reports nothing to the operator: it
|
|
243
|
+
// cannot, and it used to consume the warning by pretending it had (#933).
|
|
241
244
|
configStore.refresh(undefined, false);
|
|
242
245
|
|
|
243
246
|
const configPath = getGlobalConfigPath(agentDir);
|
|
@@ -305,12 +308,19 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
305
308
|
};
|
|
306
309
|
|
|
307
310
|
const audit = new DecisionAudit();
|
|
311
|
+
// Reads the store's issue list and tells the operator what is new, through
|
|
312
|
+
// the logger's warn sink rather than a ctx parameter — which is what the
|
|
313
|
+
// factory-time priming refresh lacked, so every config issue already on disk
|
|
314
|
+
// was recorded as delivered and never shown (#933). Driven at session_start
|
|
315
|
+
// and on every turn; the latch keeps an unchanged issue quiet.
|
|
316
|
+
const configIssueReporter = new ConfigIssueReporter(configStore, logger);
|
|
308
317
|
const lifecycle = new SessionLifecycleHandler(
|
|
309
318
|
session,
|
|
310
319
|
resolver,
|
|
311
320
|
serviceLifecycle,
|
|
312
321
|
logger,
|
|
313
322
|
audit,
|
|
323
|
+
configIssueReporter,
|
|
314
324
|
);
|
|
315
325
|
const turnPrep = new SessionTurnPrep(
|
|
316
326
|
session,
|
|
@@ -318,6 +328,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
318
328
|
void warmBashParser();
|
|
319
329
|
},
|
|
320
330
|
serviceLifecycle,
|
|
331
|
+
configIssueReporter,
|
|
321
332
|
);
|
|
322
333
|
const agentPrep = new AgentPrepHandler(
|
|
323
334
|
turnPrep,
|
|
@@ -5,6 +5,7 @@ import type { ShellToolsConfig } from "#src/config/config-schema";
|
|
|
5
5
|
import type { SessionConfigStore } from "#src/config/config-store";
|
|
6
6
|
import type { PermissionSystemExtensionConfig } from "#src/config/extension-config";
|
|
7
7
|
import type { ExtensionPaths } from "#src/config/extension-paths";
|
|
8
|
+
import { syncPermissionSystemStatus } from "#src/config/status";
|
|
8
9
|
import type { SkillPromptEntry } from "#src/exposure/skill-prompt-sanitizer";
|
|
9
10
|
import {
|
|
10
11
|
ToolSurfaceBaseline,
|
|
@@ -202,15 +203,25 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
202
203
|
// ── Config ─────────────────────────────────────────────────────────────
|
|
203
204
|
|
|
204
205
|
/**
|
|
205
|
-
* Reload merged config from disk
|
|
206
|
-
*
|
|
207
|
-
*
|
|
206
|
+
* Reload merged config from disk, then bring the status bar in step with it.
|
|
207
|
+
*
|
|
208
|
+
* When `projectTrusted` is `false`, the project scope is withheld so an
|
|
209
|
+
* untrusted project's runtime config is not merged (#644).
|
|
210
|
+
*
|
|
211
|
+
* The status sync lives here rather than in `ConfigStore` because it is a UI
|
|
212
|
+
* side effect of the session, keyed on the session's context: the store
|
|
213
|
+
* loads and answers, and needs no ctx to do it (#933). Both drivers
|
|
214
|
+
* (`session_start` and every `before_agent_start`) call this one method, so
|
|
215
|
+
* the sync has a single home rather than one copy per handler.
|
|
208
216
|
*/
|
|
209
217
|
refreshConfig(
|
|
210
218
|
ctx: ExtensionContext | undefined,
|
|
211
219
|
projectTrusted: boolean,
|
|
212
220
|
): void {
|
|
213
|
-
this.configStore.refresh(ctx, projectTrusted);
|
|
221
|
+
this.configStore.refresh(ctx?.cwd, projectTrusted);
|
|
222
|
+
if (ctx?.hasUI) {
|
|
223
|
+
syncPermissionSystemStatus(ctx, this.configStore.current());
|
|
224
|
+
}
|
|
214
225
|
}
|
|
215
226
|
|
|
216
227
|
/** Write the resolved config path set to the review and debug logs. */
|