@gotgenes/pi-permission-system 40.0.1 → 40.0.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
CHANGED
|
@@ -5,6 +5,13 @@ 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
|
+
## [40.0.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v40.0.1...pi-permission-system-v40.0.2) (2026-10-06)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** a session grant on one bash unit no longer approves the chain's other asking units ([3fffff8](https://github.com/gotgenes/pi-packages/commit/3fffff8158da81094d43a62673c8388ccb1ddf61)), closes [#1033](https://github.com/gotgenes/pi-packages/issues/1033)
|
|
14
|
+
|
|
8
15
|
## [40.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v40.0.0...pi-permission-system-v40.0.1) (2026-10-06)
|
|
9
16
|
|
|
10
17
|
|
package/package.json
CHANGED
|
@@ -123,9 +123,8 @@ export function resolveBashCommandCheck(
|
|
|
123
123
|
*
|
|
124
124
|
* The winner names one command, but a forwarded ask's answer approves the
|
|
125
125
|
* whole tool call, so the serving node has to judge every unit the child could
|
|
126
|
-
* not resolve, each with its own floor (#1030). A unit the child's rules
|
|
127
|
-
*
|
|
128
|
-
* approved exactly that command.
|
|
126
|
+
* not resolve, each with its own floor (#1030). A unit the child's rules or
|
|
127
|
+
* the session allowed stays home.
|
|
129
128
|
*/
|
|
130
129
|
function withAskingUnits(
|
|
131
130
|
winner: PermissionCheckResult,
|
|
@@ -133,7 +132,7 @@ function withAskingUnits(
|
|
|
133
132
|
): PermissionCheckResult {
|
|
134
133
|
if (winner.state !== "ask") return winner;
|
|
135
134
|
const askingUnits = results
|
|
136
|
-
.filter((result) => result.state === "ask"
|
|
135
|
+
.filter((result) => result.state === "ask")
|
|
137
136
|
.map((result) => ({
|
|
138
137
|
command: result.command ?? "",
|
|
139
138
|
...(result.floor === undefined ? {} : { floor: result.floor }),
|
|
@@ -149,17 +148,14 @@ function withAskingUnits(
|
|
|
149
148
|
* `git push x && sudo rm y` a `git push *: ask` rule wins the tie and the
|
|
150
149
|
* wrapper's floor would otherwise go unrecorded. A forwarded ask is judged on
|
|
151
150
|
* the winner's value alone, so the floor has to ride the result for the
|
|
152
|
-
* serving node to keep it (#1029).
|
|
153
|
-
* out: the user approved exactly that command.
|
|
151
|
+
* serving node to keep it (#1029).
|
|
154
152
|
*/
|
|
155
153
|
function withChainFloor(
|
|
156
154
|
winner: PermissionCheckResult,
|
|
157
155
|
results: readonly PermissionCheckResult[],
|
|
158
156
|
): PermissionCheckResult {
|
|
159
157
|
if (winner.state !== "ask" || winner.floor !== undefined) return winner;
|
|
160
|
-
const floored = results.find(
|
|
161
|
-
(result) => result.floor !== undefined && result.source !== "session",
|
|
162
|
-
);
|
|
158
|
+
const floored = results.find((result) => result.floor !== undefined);
|
|
163
159
|
return floored ? { ...winner, floor: floored.floor } : winner;
|
|
164
160
|
}
|
|
165
161
|
|
|
@@ -181,7 +177,7 @@ function resolveCommandUnit(
|
|
|
181
177
|
resolver,
|
|
182
178
|
);
|
|
183
179
|
const floored =
|
|
184
|
-
cmd.wrapperKind && base.state === "allow"
|
|
180
|
+
cmd.wrapperKind && base.state === "allow" && !isSessionGrant(base)
|
|
185
181
|
? resolveWrapperUnit(cmd, cmd.wrapperKind, base, agentName, resolver)
|
|
186
182
|
: base;
|
|
187
183
|
const unparsed = floorUnparsedUnit(cmd, command, floored);
|
|
@@ -210,26 +206,40 @@ function resolveCommandUnit(
|
|
|
210
206
|
* decides instead — and a wrapper unit already floored to `ask` keeps its own,
|
|
211
207
|
* more specific sentinel.
|
|
212
208
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
209
|
+
* A session grant is never floored: the user approved this exact command, and
|
|
210
|
+
* an `ask` carrying it would tie with a sibling unit's real ask and could
|
|
211
|
+
* approve the whole chain through `GateRunner`'s session fast path (#1033).
|
|
216
212
|
*/
|
|
217
213
|
function floorUnparsedUnit(
|
|
218
214
|
cmd: BashCommand,
|
|
219
215
|
command: string,
|
|
220
216
|
resolved: PermissionCheckResult,
|
|
221
217
|
): PermissionCheckResult {
|
|
222
|
-
if (
|
|
218
|
+
if (
|
|
219
|
+
!cmd.parseUnresolved ||
|
|
220
|
+
resolved.state !== "allow" ||
|
|
221
|
+
isSessionGrant(resolved)
|
|
222
|
+
) {
|
|
223
|
+
return resolved;
|
|
224
|
+
}
|
|
223
225
|
return { ...floorToAsk(resolved, UNPARSED_SUBTREE_SENTINEL), command };
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
/**
|
|
229
|
+
* True when the session layer decided the check. `SessionRules` records only
|
|
230
|
+
* `allow`s, so this is a grant the user gave for the command it matched.
|
|
231
|
+
*/
|
|
232
|
+
function isSessionGrant(check: PermissionCheckResult): boolean {
|
|
233
|
+
return check.source === "session";
|
|
234
|
+
}
|
|
235
|
+
|
|
226
236
|
/**
|
|
227
237
|
* Clamp a resolved check up to a synthetic `ask` naming the floor that raised
|
|
228
238
|
* it.
|
|
229
239
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
240
|
+
* Never handed a session grant: both floors leave one unfloored. Drops
|
|
241
|
+
* `matchedSpelling`: it names what the replaced rule matched, and beside the
|
|
242
|
+
* sentinel it would name a match that did not decide.
|
|
233
243
|
*/
|
|
234
244
|
function floorToAsk(
|
|
235
245
|
resolved: PermissionCheckResult,
|
|
@@ -138,13 +138,10 @@ export function preResolvedCheckOf(
|
|
|
138
138
|
* a semantic one, and it is why the same treatment is *not* extended to `ask`
|
|
139
139
|
* (#915): two asking gates ask two different questions.
|
|
140
140
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* today; the clause is kept so the predicate is correct on its own terms
|
|
146
|
-
* rather than by way of a distant invariant, and it errs toward today's
|
|
147
|
-
* behavior by declining to pre-empt.
|
|
141
|
+
* A session-sourced `deny` counts too: {@link GateRunner.runDescriptor}'s
|
|
142
|
+
* session fast path takes only a session `allow`, so any other session-sourced
|
|
143
|
+
* check reaches the deny/ask/allow gate and a `deny` blocks there. `SessionRules`
|
|
144
|
+
* records only allows, so that combination is unreachable today.
|
|
148
145
|
*
|
|
149
146
|
* Yolo needs no clause: `resolveYoloGrant` matches an `allow` of origin
|
|
150
147
|
* `yolo` and an `ask`, never a `deny`.
|
|
@@ -154,7 +151,7 @@ export function isUnconditionalDeny(gate: GateResult): boolean {
|
|
|
154
151
|
return false;
|
|
155
152
|
}
|
|
156
153
|
const check = preResolvedCheckOf(gate);
|
|
157
|
-
return check !== null && check.state === "deny"
|
|
154
|
+
return check !== null && check.state === "deny";
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/**
|
|
@@ -116,8 +116,10 @@ export class GateRunner {
|
|
|
116
116
|
// context holds what every resolution of this gate shares, and who decided
|
|
117
117
|
// is by definition not shared (#726).
|
|
118
118
|
|
|
119
|
-
// 2. Session-hit fast path
|
|
120
|
-
|
|
119
|
+
// 2. Session-hit fast path: a session grant is an allow. `SessionRules`
|
|
120
|
+
// records nothing else, so a session-sourced ask or deny means something
|
|
121
|
+
// clamped a grant, and it takes the gate below instead of riding the grant.
|
|
122
|
+
if (check.source === "session" && check.state === "allow") {
|
|
121
123
|
this.reporter.writeReviewLog("permission_request.session_approved", {
|
|
122
124
|
...logContext,
|
|
123
125
|
resolution: "session_approved",
|