@gotgenes/pi-permission-system 30.0.0 → 30.2.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/src/rule.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { PathFlavor } from "#src/path/path-flavor";
2
2
 
3
3
  import { PATH_SURFACES } from "./access-intent/path-surfaces";
4
+ import { expandHomePath } from "./expand-home";
4
5
  import type { PermissionState } from "./types";
5
6
  import { type WildcardMatchOptions, wildcardMatch } from "./wildcard-matcher";
6
7
 
@@ -139,6 +140,54 @@ function ruleMatches(
139
140
  );
140
141
  }
141
142
 
143
+ /**
144
+ * Whether every value on `surface` resolves to `deny`.
145
+ *
146
+ * This is a different question from "what does the surface's catch-all say?",
147
+ * and it carries a different burden of proof: the catch-all answers *what this
148
+ * surface is*, while this answers *whether anything at all could get through* —
149
+ * the question tool exposure has to ask before withholding a tool from the
150
+ * agent entirely.
151
+ *
152
+ * Reachability is decided by probing each pattern configured on the surface as
153
+ * a representative value through {@link evaluate}, so last-match-wins shadowing
154
+ * is honored: an exception written *after* a `deny` catch-all is reachable,
155
+ * while one written *before* it is not.
156
+ *
157
+ * The probe is an approximation of "does any string resolve non-deny", not a
158
+ * decision procedure for it. Being wrong in either direction only changes
159
+ * whether the agent sees the tool — the invocation gate re-evaluates the real
160
+ * value against the same ruleset either way.
161
+ */
162
+ export function isSurfaceFullyDenied(
163
+ surface: string,
164
+ rules: Ruleset,
165
+ flavor: PathFlavor,
166
+ ): boolean {
167
+ for (const value of probeValuesForSurface(surface, rules)) {
168
+ if (evaluate(surface, value, rules, flavor).action !== "deny") return false;
169
+ }
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * The representative values {@link isSurfaceFullyDenied} probes: the catch-all,
175
+ * plus every pattern configured on a rule whose surface reaches `surface`.
176
+ *
177
+ * Each pattern is home-expanded because {@link compileWildcardPattern} expands
178
+ * the *pattern* side only; an unexpanded `~/notes/*` probe would fail to match
179
+ * its own rule and report the surface denied.
180
+ */
181
+ function probeValuesForSurface(surface: string, rules: Ruleset): Set<string> {
182
+ const values = new Set<string>(["*"]);
183
+ for (const rule of rules) {
184
+ if (wildcardMatch(rule.surface, surface)) {
185
+ values.add(expandHomePath(rule.pattern));
186
+ }
187
+ }
188
+ return values;
189
+ }
190
+
142
191
  /**
143
192
  * Evaluate a surface against multiple values, returning the most restrictive
144
193
  * non-allow result (deny > ask > allow).
package/src/service.ts CHANGED
@@ -99,13 +99,17 @@ export interface PermissionQuery {
99
99
  ): PermissionCheckResult;
100
100
 
101
101
  /**
102
- * Query the tool-level permission state for pre-filtering tools before
103
- * creating a child session.
102
+ * Query a surface's catch-all permission state its blanket policy.
104
103
  *
105
104
  * Returns `"deny"` | `"allow"` | `"ask"` based on the composed policy.
106
105
  * Does not consider command-level rules (e.g. per-bash-command patterns) —
107
106
  * use `checkPermission` for runtime invocation gates.
108
107
  *
108
+ * This is **not** the question to ask when pre-filtering a tool list: a
109
+ * partially permissive surface such as `bash: {"*": "deny", "git *": "ask"}`
110
+ * answers `"deny"` here while `git status` would still be asked about. Use
111
+ * {@link PermissionsService.isToolFullyDenied} for that.
112
+ *
109
113
  * @param toolName - Tool name (e.g. `"bash"`, `"read"`, `"my-extension:tool"`).
110
114
  * @param agentName - Optional agent name for per-agent policy resolution.
111
115
  */
@@ -126,6 +130,24 @@ export interface PermissionQuery {
126
130
  * rules internally.
127
131
  */
128
132
  export interface PermissionsService extends PermissionQuery {
133
+ /**
134
+ * Whether every value under a tool's surface resolves to `deny`.
135
+ *
136
+ * This is the question to ask before withholding a tool from a child
137
+ * session's tool list, and it is not `getToolPermission`: that reports the
138
+ * surface's own catch-all, so a partially permissive surface such as
139
+ * `bash: {"*": "deny", "git *": "ask"}` reads as `"deny"` while `git status`
140
+ * would still be asked about.
141
+ *
142
+ * Rule ordering is honored (last-match-wins), so an exception written *after*
143
+ * a `deny` catch-all keeps the tool reachable and one written *before* it
144
+ * does not.
145
+ *
146
+ * @param toolName - Tool name (e.g. `"bash"`, `"read"`, `"my-extension:tool"`).
147
+ * @param agentName - Optional agent name for per-agent policy resolution.
148
+ */
149
+ isToolFullyDenied(toolName: string, agentName?: string): boolean;
150
+
129
151
  /**
130
152
  * Register a custom preview formatter for a specific tool name.
131
153
  *
@@ -1,4 +1,8 @@
1
- import type { ApprovalGrant } from "#src/approval-grant";
1
+ import {
2
+ type ApprovalGrant,
3
+ type SessionGrantWidth,
4
+ widenGrant,
5
+ } from "#src/approval-grant";
2
6
  import type { ForwardedSessionApproval } from "#src/authority/permission-forwarding";
3
7
 
4
8
  /**
@@ -33,6 +37,20 @@ export class SessionApproval {
33
37
  return this.grants.length > 0;
34
38
  }
35
39
 
40
+ /**
41
+ * This approval as recorded at `width`.
42
+ *
43
+ * The runner holds a width and an approval and tells the approval to produce
44
+ * itself — it never inspects a grant's surface. Each grant is folded
45
+ * individually, so an approval whose patterns proved different directions
46
+ * keeps one grant per pattern (#810) at either width.
47
+ */
48
+ atWidth(width: SessionGrantWidth): SessionApproval {
49
+ return width === "proven"
50
+ ? this
51
+ : new SessionApproval(this.grants.map(widenGrant));
52
+ }
53
+
36
54
  /**
37
55
  * Plain data shape for relaying this approval on a forwarded request, so the
38
56
  * serving node can record the same grants as a whole-session grant.