pi-auto-permissions 0.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/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +304 -0
- package/THIRD_PARTY_NOTICES.md +70 -0
- package/docs/implementation-plan.md +311 -0
- package/docs/invariants.md +555 -0
- package/package.json +59 -0
- package/src/canonical.ts +314 -0
- package/src/commands/index.ts +362 -0
- package/src/domain.ts +198 -0
- package/src/extension.ts +430 -0
- package/src/guardian/circuit-breaker.ts +102 -0
- package/src/guardian/index.ts +74 -0
- package/src/guardian/policy.ts +135 -0
- package/src/guardian/prompt.ts +379 -0
- package/src/guardian/reviewer.ts +599 -0
- package/src/guardian/types.ts +149 -0
- package/src/guardian/verdict.ts +211 -0
- package/src/pi/index.ts +13 -0
- package/src/pi/model-reviewer.ts +235 -0
- package/src/pi/transcript.ts +524 -0
- package/src/policy/dangerous-command.ts +312 -0
- package/src/policy/path-policy.ts +501 -0
- package/src/runtime/index.ts +1 -0
- package/src/runtime/permission-engine.ts +474 -0
- package/src/sandbox/config.ts +188 -0
- package/src/sandbox/controller.ts +354 -0
- package/src/sandbox/index.ts +26 -0
- package/src/sandbox/runtime.ts +28 -0
- package/src/sandbox/types.ts +125 -0
- package/src/state/config-store.ts +580 -0
- package/src/state/index.ts +2 -0
- package/src/tools/bash.ts +101 -0
- package/src/tools/gate.ts +74 -0
- package/src/tools/index.ts +2 -0
- package/vendor/openai-codex/NOTICE +6 -0
- package/vendor/openai-codex/README.md +17 -0
- package/vendor/openai-codex/guardian/policy.md +42 -0
- package/vendor/openai-codex/guardian/policy_template.md +58 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
# Permission-system invariants
|
|
2
|
+
|
|
3
|
+
Status: normative design specification.
|
|
4
|
+
|
|
5
|
+
Upstream reference points:
|
|
6
|
+
|
|
7
|
+
- Pi `0.80.10`, source revision
|
|
8
|
+
[`3da591ab74ab9ab407e72ed882600b2c851fae21`](https://github.com/earendil-works/pi/tree/3da591ab74ab9ab407e72ed882600b2c851fae21).
|
|
9
|
+
- Codex Guardian and approval behavior, source revision
|
|
10
|
+
[`0fb559f0f6e231a88ac02ea002d3ecd248e2b515`](https://github.com/openai/codex/tree/0fb559f0f6e231a88ac02ea002d3ecd248e2b515).
|
|
11
|
+
|
|
12
|
+
This document defines what the extension guarantees, the assumptions under which
|
|
13
|
+
those guarantees hold, and why the state machine has no path to the pathological
|
|
14
|
+
behaviors called out in the product requirements.
|
|
15
|
+
|
|
16
|
+
## 1. Scope and trust boundary
|
|
17
|
+
|
|
18
|
+
The extension is a permission guardrail for model-originated Pi tool calls. It is
|
|
19
|
+
not a security boundary against the user, the operating-system kernel, Pi itself,
|
|
20
|
+
or arbitrary extension code already trusted and loaded into Pi's process.
|
|
21
|
+
|
|
22
|
+
The proof assumes:
|
|
23
|
+
|
|
24
|
+
1. Pi invokes the extension's `tool_call` handler before executing each
|
|
25
|
+
model-originated tool call and honors `{ block: true }`.
|
|
26
|
+
2. Other loaded extensions are trusted: they do not mutate a reviewed request
|
|
27
|
+
after this extension's handler returns, replace this extension's guarded
|
|
28
|
+
built-ins later, or perform effects unrelated to their declared tool call.
|
|
29
|
+
3. User-entered `!` and `!!` commands are user-authorized actions and are outside
|
|
30
|
+
the model-action guardrail.
|
|
31
|
+
4. The selected review model, model provider, Pi runtime, sandbox runtime, OS,
|
|
32
|
+
filesystem, and kernel may fail, but are not maliciously subverted.
|
|
33
|
+
5. Semantic correctness of an LLM verdict is not provable. The extension proves
|
|
34
|
+
routing, binding, failure, and execution properties around that verdict.
|
|
35
|
+
6. A hostile local process racing path checks is outside the threat model. Static
|
|
36
|
+
file policy still resolves existing symlinks and the nearest existing ancestor
|
|
37
|
+
so ordinary path indirection cannot accidentally broaden a request.
|
|
38
|
+
7. Durable-state proofs cover store-authored atomic-write failures and corruption
|
|
39
|
+
of the config together with at most one of the two revision-metadata
|
|
40
|
+
artifacts. Simultaneous destruction or malicious replacement of both private
|
|
41
|
+
revision watermarks is a hostile-local-process case and is outside scope; the
|
|
42
|
+
store refuses to guess a revision in that state.
|
|
43
|
+
|
|
44
|
+
Third-party tools are policy-reviewed when Pi exposes them through `tool_call`,
|
|
45
|
+
but their implementation is the installing user's responsibility and is not
|
|
46
|
+
claimed to be OS-sandboxed.
|
|
47
|
+
|
|
48
|
+
## 2. State
|
|
49
|
+
|
|
50
|
+
### 2.1 Durable global state
|
|
51
|
+
|
|
52
|
+
Let the only durable permission state be
|
|
53
|
+
|
|
54
|
+
\[
|
|
55
|
+
G = (v, enabled, reviewer, revision)
|
|
56
|
+
\]
|
|
57
|
+
|
|
58
|
+
where:
|
|
59
|
+
|
|
60
|
+
- \(v = 1\) is the configuration schema version;
|
|
61
|
+
- \(enabled \in \{true,false\}\);
|
|
62
|
+
- \(reviewer \in \{None, (provider, modelId, thinkingLevel)\}\), where
|
|
63
|
+
`thinkingLevel` is one of the levels Pi reports as supported by that exact
|
|
64
|
+
model;
|
|
65
|
+
- \(revision \in \mathbb{N}\) increases on every committed global mutation.
|
|
66
|
+
|
|
67
|
+
For store-authored state, the store also maintains two private durable copies of
|
|
68
|
+
one allocation watermark, \(counter = recovery \ge revision\). They are not
|
|
69
|
+
permission configuration and are never consulted to choose a mode; they exist
|
|
70
|
+
so a repair cannot reuse an old revision after malformed JSON erased the
|
|
71
|
+
embedded value. A publication durably advances the recovery watermark, the
|
|
72
|
+
primary counter, and then the config while holding the same cross-process lock
|
|
73
|
+
used by readers. Failure may skip a revision but can never reuse one on a later
|
|
74
|
+
store-authored commit.
|
|
75
|
+
|
|
76
|
+
The configuration also has a derived health value:
|
|
77
|
+
|
|
78
|
+
\[
|
|
79
|
+
health(G) \in \{missing, valid, fault\}.
|
|
80
|
+
\]
|
|
81
|
+
|
|
82
|
+
`missing` is reserved for the all-three-absent first-run state and means
|
|
83
|
+
`(enabled=true, reviewer=None, revision=0)`. Invalid JSON, an unknown schema,
|
|
84
|
+
invalid field types, malformed revision metadata, a missing half of the
|
|
85
|
+
watermark pair, unequal watermarks, a watermark below the config revision, or a
|
|
86
|
+
missing config accompanied by either watermark produce `fault`; they never
|
|
87
|
+
produce permissive defaults for a guarded action. A metadata fault remains Fault
|
|
88
|
+
until an explicit management mutation safely repairs and publishes both
|
|
89
|
+
watermarks plus the complete config.
|
|
90
|
+
|
|
91
|
+
Reads and writes are serialized across processes. Each watermark and then the
|
|
92
|
+
complete config is written to a private temporary file, file-synced, and
|
|
93
|
+
atomically renamed; the containing directory is synced where the filesystem
|
|
94
|
+
supports it. A live reader therefore never observes the staged intermediate
|
|
95
|
+
files of a concurrent writer. If a crash leaves unequal or malformed metadata,
|
|
96
|
+
the composite read is Fault. Explicit repair takes the maximum valid config
|
|
97
|
+
hint and surviving watermark, advances it, and restores both metadata artifacts
|
|
98
|
+
before publishing a healthy config. If neither watermark is valid,
|
|
99
|
+
repair refuses because monotonicity cannot be proved. Equal watermarks may be
|
|
100
|
+
ahead after a failed config publication, which only creates a harmless revision
|
|
101
|
+
gap. A process failure after config rename but before acknowledgement has an
|
|
102
|
+
intentionally ambiguous outcome: a later read of the monotonic revision is
|
|
103
|
+
authoritative.
|
|
104
|
+
|
|
105
|
+
### 2.2 Per-session state
|
|
106
|
+
|
|
107
|
+
For each Pi extension runtime \(i\):
|
|
108
|
+
|
|
109
|
+
\[
|
|
110
|
+
S_i = (requestedMode_i, sessionRevision_i, backend_i, alive_i)
|
|
111
|
+
\]
|
|
112
|
+
|
|
113
|
+
where:
|
|
114
|
+
|
|
115
|
+
- \(requestedMode_i \in \{Auto, Unrestricted\}\);
|
|
116
|
+
- \(sessionRevision_i \in \mathbb{N}\) increases on every session-local policy
|
|
117
|
+
change;
|
|
118
|
+
- \(backend_i \in \{\bot, Sandboxed, ReviewOnly, Unavailable\}\), where
|
|
119
|
+
\(\bot\) is the transient, not-yet-probed value used while the runtime is
|
|
120
|
+
initializing;
|
|
121
|
+
- \(alive_i \in \{true,false\}\).
|
|
122
|
+
|
|
123
|
+
`backend` is an enforcement implementation detail, not a third user-visible
|
|
124
|
+
permission mode. The complete reviewer tuple is one atomic setting: a thinking
|
|
125
|
+
level is never inferred at review time from the main session, and changing only
|
|
126
|
+
that level increments the global revision just like changing the model.
|
|
127
|
+
|
|
128
|
+
The production extension does not publish an active session until probing has
|
|
129
|
+
replaced \(\bot\) with a concrete backend. A shell gate or reviewed-action gate
|
|
130
|
+
that nevertheless encounters \(\bot\) denies because no review binding can be
|
|
131
|
+
formed. A fresh state starts at revision zero; reload reconstruction advances the
|
|
132
|
+
stored checkpoint revision, and each actual backend, mode, or lifecycle change
|
|
133
|
+
advances it again.
|
|
134
|
+
|
|
135
|
+
Every new, resumed, forked, cloned, or independently spawned Pi runtime starts
|
|
136
|
+
with `requestedMode=Auto`; it never copies a parent's mode. A Pi `/reload`
|
|
137
|
+
reconstructs the prior requested mode for that same logical runtime. If no
|
|
138
|
+
reviewer is configured, Auto is unavailable and its effective behavior is
|
|
139
|
+
Unrestricted until a reviewer is selected. Selecting the first reviewer sets
|
|
140
|
+
the invoking session's requested mode to Auto.
|
|
141
|
+
|
|
142
|
+
Define the effective mode:
|
|
143
|
+
|
|
144
|
+
\[
|
|
145
|
+
effective(G,S_i) =
|
|
146
|
+
\begin{cases}
|
|
147
|
+
Disabled & health(G)\neq fault \land enabled=false \\
|
|
148
|
+
Unrestricted & requestedMode_i=Unrestricted \\
|
|
149
|
+
Fault & health(G)=fault \land requestedMode_i=Auto \\
|
|
150
|
+
UnrestrictedUnavailable & health(G)\neq fault \land enabled=true \land reviewer=None \\
|
|
151
|
+
Auto & health(G)\neq fault \land enabled=true \land reviewer\neq None.
|
|
152
|
+
\end{cases}
|
|
153
|
+
\]
|
|
154
|
+
|
|
155
|
+
`UnrestrictedUnavailable` is displayed simply as Unrestricted with Auto marked
|
|
156
|
+
unavailable. It remembers the default Auto intent so already-running default
|
|
157
|
+
sessions become Auto when the first reviewer is selected. An explicitly selected
|
|
158
|
+
Unrestricted session remains Unrestricted.
|
|
159
|
+
|
|
160
|
+
### 2.3 Enforcement backend
|
|
161
|
+
|
|
162
|
+
At startup the extension probes the shell sandbox without executing a
|
|
163
|
+
model-originated command:
|
|
164
|
+
|
|
165
|
+
- supported macOS/Linux plus successful dependency checks, initialization, and
|
|
166
|
+
a real contained-process probe yields `Sandboxed`;
|
|
167
|
+
- an unsupported operating system yields `ReviewOnly` and a non-blocking
|
|
168
|
+
warning;
|
|
169
|
+
- a macOS/Linux dependency, initialization, or probe failure yields
|
|
170
|
+
`Unavailable` and denies Auto shell calls rather than silently weakening the
|
|
171
|
+
expected platform boundary;
|
|
172
|
+
- an indeterminate state denies shell execution until it resolves.
|
|
173
|
+
|
|
174
|
+
Sandbox Runtime is process-global. On a supported host, exactly one controller
|
|
175
|
+
in a Node process owns it at a time. Another same-process supported-host session
|
|
176
|
+
cannot install a different policy while that owner is active: sandbox creation
|
|
177
|
+
is converted to `Unavailable`, so that session's Auto shell calls fail closed.
|
|
178
|
+
Positively unsupported hosts do not claim the singleton and may independently
|
|
179
|
+
use ReviewOnly. Shutdown releases ownership only after active command leases
|
|
180
|
+
drain and Sandbox Runtime resets successfully. If that final reset fails, the
|
|
181
|
+
owner slot is poisoned for the rest of the Node process; later supported-host
|
|
182
|
+
sessions remain `Unavailable` until process restart rather than reusing possibly
|
|
183
|
+
stale process-global policy.
|
|
184
|
+
|
|
185
|
+
If sandbox infrastructure fails before a future command begins, subsequent Auto
|
|
186
|
+
shell calls are denied. If a sandboxed process may have begun, that call is never
|
|
187
|
+
automatically retried. This prevents both duplicated side effects and an
|
|
188
|
+
unannounced downgrade on an operating system where containment is expected.
|
|
189
|
+
|
|
190
|
+
## 3. Canonical actions and lifecycle
|
|
191
|
+
|
|
192
|
+
A model action is canonically represented as
|
|
193
|
+
|
|
194
|
+
\[
|
|
195
|
+
A = Canon(toolName, arguments, cwd, toolMetadata).
|
|
196
|
+
\]
|
|
197
|
+
|
|
198
|
+
Canonicalization is deterministic: object keys are sorted, unsupported values are
|
|
199
|
+
rejected, strings are preserved byte-for-byte as UTF-8, and the result is bounded
|
|
200
|
+
in size. Review captures
|
|
201
|
+
|
|
202
|
+
\[
|
|
203
|
+
C = (A, reviewer, globalRevision, sessionRevision, backend, sessionId).
|
|
204
|
+
\]
|
|
205
|
+
|
|
206
|
+
Every action has exactly one lifecycle:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
Created -> Classified -> Admitted -> Executed
|
|
210
|
+
\-> Reviewing -> Admitted
|
|
211
|
+
\-> Denied
|
|
212
|
+
\-> Denied
|
|
213
|
+
\-> Cancelled
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`Executed`, `Denied`, and `Cancelled` are terminal. No transition leaves a
|
|
217
|
+
terminal state. `Executed` has only `Admitted` as a predecessor.
|
|
218
|
+
|
|
219
|
+
Admission is the linearization boundary between permission policy and tool
|
|
220
|
+
execution. For a reviewed action, it is the final successful binding read plus
|
|
221
|
+
the immediately following synchronous lifecycle/breaker check. For an
|
|
222
|
+
unreviewed route, its enabled-state ordering is determined by the global-state
|
|
223
|
+
read used by that gate, followed by the synchronous final lifecycle check. An
|
|
224
|
+
action linearized before a later policy commit may enter its executor once; the
|
|
225
|
+
extension does not claim to revoke or kill an action already admitted. An action
|
|
226
|
+
whose controlling read is after that commit must use the new state.
|
|
227
|
+
|
|
228
|
+
An Auto review admits an action only if all of the following hold:
|
|
229
|
+
|
|
230
|
+
1. the selected model returned an exactly valid `allow` verdict;
|
|
231
|
+
2. the request was not aborted and its session is alive;
|
|
232
|
+
3. the current reviewer model and thinking level equal the captured tuple;
|
|
233
|
+
4. current global and session revisions equal the captured revisions;
|
|
234
|
+
5. the current backend equals the captured backend;
|
|
235
|
+
6. the current canonical request equals the reviewed canonical request.
|
|
236
|
+
|
|
237
|
+
All other outcomes deny: explicit deny, `ask`, malformed output, missing model,
|
|
238
|
+
missing credentials, timeout, cancellation, queue exhaustion, oversized input,
|
|
239
|
+
provider failure, internal exception, revision change, backend change, or request
|
|
240
|
+
change. There is no fallback model and no conversion to Unrestricted.
|
|
241
|
+
|
|
242
|
+
## 4. Admission function
|
|
243
|
+
|
|
244
|
+
For a healthy enabled Auto session, classify built-ins as follows.
|
|
245
|
+
|
|
246
|
+
### 4.1 Read-only file tools
|
|
247
|
+
|
|
248
|
+
`read`, `grep`, `find`, and `ls` are admitted without model review. This matches
|
|
249
|
+
the broad-read property of Codex workspace-write and cannot directly create a
|
|
250
|
+
filesystem, process, or network side effect.
|
|
251
|
+
|
|
252
|
+
### 4.2 Direct mutation tools
|
|
253
|
+
|
|
254
|
+
For `write` and `edit`, let `target` be the canonical target resolved against
|
|
255
|
+
`cwd`, including existing symlinks or the nearest existing ancestor for a path
|
|
256
|
+
that does not yet exist.
|
|
257
|
+
|
|
258
|
+
`target` is statically admitted exactly when it is inside a writable root and is
|
|
259
|
+
not inside a protected metadata path. Writable roots are the session workspace,
|
|
260
|
+
the OS temporary directory, and `/tmp` where present. Protected metadata includes
|
|
261
|
+
top-level `.git`, resolved Git directories, `.agents`, `.codex`, and `.pi`.
|
|
262
|
+
|
|
263
|
+
Every other direct mutation is reviewed before the built-in executes. A denial
|
|
264
|
+
therefore causes zero effects from that tool call.
|
|
265
|
+
|
|
266
|
+
If the path policy cannot be constructed at session startup, the installed
|
|
267
|
+
fallback admits only the known read-only built-ins and statically denies direct
|
|
268
|
+
mutations. It does not guess a writable path or ask the reviewer to compensate
|
|
269
|
+
for missing path classification.
|
|
270
|
+
|
|
271
|
+
One narrower control-plane rule precedes this classification: Pi direct-file
|
|
272
|
+
tools may never mutate the extension's durable state or lock paths in Auto.
|
|
273
|
+
Those targets are statically denied without consulting the reviewer. This
|
|
274
|
+
prevents an ordinary `write`/`edit` call from turning an action review into a
|
|
275
|
+
permission-setting mutation.
|
|
276
|
+
|
|
277
|
+
### 4.3 Shell tool with a healthy sandbox
|
|
278
|
+
|
|
279
|
+
For `bash` under `Sandboxed`:
|
|
280
|
+
|
|
281
|
+
- an explicit `require_escalated` request is reviewed before execution;
|
|
282
|
+
- a command matching the pinned Codex dangerous-command rules is reviewed before
|
|
283
|
+
execution, and an exact allow still runs inside the fixed sandbox;
|
|
284
|
+
- every other command executes once inside the fixed workspace-write sandbox.
|
|
285
|
+
|
|
286
|
+
The fixed sandbox grants broad reads, workspace and temporary-directory writes,
|
|
287
|
+
keeps protected metadata read-only, denies network access, and applies to the
|
|
288
|
+
spawned process tree.
|
|
289
|
+
|
|
290
|
+
A sandbox rejection is not a Guardian denial. It is returned as an ordinary tool
|
|
291
|
+
error. The model may choose a materially safer command or submit a new exact
|
|
292
|
+
`require_escalated` call for Auto review. The extension never retries the rejected
|
|
293
|
+
call itself.
|
|
294
|
+
|
|
295
|
+
### 4.4 Shell tool without a sandbox
|
|
296
|
+
|
|
297
|
+
For `bash` under `ReviewOnly`, every call is reviewed before any process starts.
|
|
298
|
+
An allow executes that exact command once with normal host permissions. Every
|
|
299
|
+
non-allow produces no process.
|
|
300
|
+
|
|
301
|
+
Thus unsupported platforms remain functional without ever executing an
|
|
302
|
+
unreviewed unsandboxed model command in Auto.
|
|
303
|
+
|
|
304
|
+
### 4.5 Failed supported sandbox
|
|
305
|
+
|
|
306
|
+
For `bash` under `Unavailable`, every call is denied before model review or
|
|
307
|
+
process creation. ReviewOnly is intentionally not used here: on macOS/Linux the
|
|
308
|
+
user is entitled to rely on containment being present, so an infrastructure
|
|
309
|
+
fault may reduce availability but cannot silently reduce enforcement.
|
|
310
|
+
|
|
311
|
+
### 4.6 Third-party tools
|
|
312
|
+
|
|
313
|
+
Unknown/custom tools are reviewed using their Pi-exposed name, description,
|
|
314
|
+
schema/source metadata, arguments, cwd, and conversation context. An allow lets
|
|
315
|
+
Pi call the trusted implementation; every non-allow blocks it. OS containment of
|
|
316
|
+
the implementation is explicitly outside scope.
|
|
317
|
+
|
|
318
|
+
### 4.7 Bypass states
|
|
319
|
+
|
|
320
|
+
`Disabled` and explicit `Unrestricted` admit model tool calls without this
|
|
321
|
+
extension's review or sandbox. They are deliberate user-authorized bypasses.
|
|
322
|
+
|
|
323
|
+
`Fault` denies guarded calls. User-only management commands remain available to
|
|
324
|
+
attempt a safe repair or deliberately choose Unrestricted. Repair refuses when
|
|
325
|
+
neither watermark is valid enough to prove a monotonic next revision.
|
|
326
|
+
|
|
327
|
+
`Unavailable` affects shell routing only: Auto direct-file and third-party tool
|
|
328
|
+
policy still operates, while every model-originated `bash` call is denied with an
|
|
329
|
+
infrastructure-specific safer-action result.
|
|
330
|
+
|
|
331
|
+
If construction of the permission engine itself fails after subsystem startup,
|
|
332
|
+
the extension publishes no active runtime, reports `Auto (unavailable)`, and its
|
|
333
|
+
already-registered bash and non-bash gates return the fixed denial. Because the
|
|
334
|
+
command host also requires an active runtime, the three extension commands
|
|
335
|
+
report failure rather than mutating state in this condition. A later successful
|
|
336
|
+
Pi session initialization (normally reload or process restart) is required to
|
|
337
|
+
recover. This catastrophic state is distinct from `Unavailable`, where the
|
|
338
|
+
engine is active and only Auto shell routing is unavailable. With no engine,
|
|
339
|
+
even a previously durable Off setting cannot be consulted by the outer gates;
|
|
340
|
+
they choose denial rather than an unverified bypass.
|
|
341
|
+
|
|
342
|
+
## 5. Normative invariants
|
|
343
|
+
|
|
344
|
+
### I1. Two-mode invariant
|
|
345
|
+
|
|
346
|
+
The only user-selectable modes are Auto and Unrestricted. Sandboxed, ReviewOnly,
|
|
347
|
+
and Unavailable never appear as selectable modes.
|
|
348
|
+
|
|
349
|
+
### I2. Auto-availability invariant
|
|
350
|
+
|
|
351
|
+
No session can effectively operate in Auto without a configured reviewer model
|
|
352
|
+
and an explicitly selected supported thinking level.
|
|
353
|
+
|
|
354
|
+
### I3. Fresh-session invariant
|
|
355
|
+
|
|
356
|
+
Every new session computes its requested mode independently as Auto. No parent or
|
|
357
|
+
previous session can propagate Unrestricted.
|
|
358
|
+
|
|
359
|
+
### I4. Global-disable invariant
|
|
360
|
+
|
|
361
|
+
For every active permission runtime, the durable `enabled=false` publication is
|
|
362
|
+
the global-switch linearization point. Every successful gate read of that state
|
|
363
|
+
after publication observes Disabled and bypasses this extension; a failed read
|
|
364
|
+
denies rather than guessing. A reviewed action whose binding read observes the
|
|
365
|
+
new revision cannot use an older allow. An action already linearized for
|
|
366
|
+
admission before the publication may execute once; Off is not a retroactive
|
|
367
|
+
process-kill mechanism. Re-enabling preserves each session's requested mode. A
|
|
368
|
+
catastrophic pre-runtime failure has no state reader and follows the fixed-denial
|
|
369
|
+
rule in Section 4.7.
|
|
370
|
+
|
|
371
|
+
### I5. Pre-execution denial invariant
|
|
372
|
+
|
|
373
|
+
A denied reviewed action reaches no tool executor and creates no process. A normal
|
|
374
|
+
sandboxed call may perform permitted effects before the OS reports a later denied
|
|
375
|
+
operation, but the extension never elevates or repeats it automatically.
|
|
376
|
+
|
|
377
|
+
### I6. Unsandboxed-shell invariant
|
|
378
|
+
|
|
379
|
+
In effective Auto, an unsandboxed model-originated shell process starts only after
|
|
380
|
+
an allow verdict bound to that exact command and current state.
|
|
381
|
+
|
|
382
|
+
### I7. Sandboxed-shell invariant
|
|
383
|
+
|
|
384
|
+
In effective Auto with a healthy sandbox, an unreviewed shell command can start
|
|
385
|
+
only under the fixed sandbox policy.
|
|
386
|
+
|
|
387
|
+
### I8. Verdict-binding invariant
|
|
388
|
+
|
|
389
|
+
An Auto allow authorizes one canonical action under one captured reviewer tuple,
|
|
390
|
+
global revision, session revision, backend, and session. It authorizes no similar
|
|
391
|
+
action, retry, or future call.
|
|
392
|
+
|
|
393
|
+
### I9. Exactly-once orchestration invariant
|
|
394
|
+
|
|
395
|
+
The extension invokes an admitted tool at most once and never automatically
|
|
396
|
+
replays a call after denial, timeout, sandbox rejection, or ambiguous runtime
|
|
397
|
+
failure.
|
|
398
|
+
|
|
399
|
+
### I10. Fail-closed invariant
|
|
400
|
+
|
|
401
|
+
Authorization, binding, verdict, configuration, and containment uncertainty in
|
|
402
|
+
a guarded Auto path maps to denial before execution. Classification uncertainty
|
|
403
|
+
maps to Guardian review or denial, never directly to unreviewed execution.
|
|
404
|
+
ReviewOnly is selected only by a positive unsupported-platform classification;
|
|
405
|
+
neither path ever maps to Unrestricted.
|
|
406
|
+
|
|
407
|
+
### I11. No-dialog invariant
|
|
408
|
+
|
|
409
|
+
No action-review branch invokes a user confirmation, selection, input, or editor
|
|
410
|
+
API. Denial returns a fixed tool result to the model:
|
|
411
|
+
|
|
412
|
+
> Permission denied. This action was not executed. No override will be requested.
|
|
413
|
+
> Choose a materially safer action.
|
|
414
|
+
|
|
415
|
+
User-facing selection UI exists only inside explicit user slash commands.
|
|
416
|
+
|
|
417
|
+
### I12. Direct-tool no-self-elevation invariant
|
|
418
|
+
|
|
419
|
+
Only Pi's explicit slash-command dispatch can invoke the extension's global
|
|
420
|
+
enablement, reviewer-selection, or session-mode mutations. Auto direct-file
|
|
421
|
+
tools are additionally denied access to the durable control-plane paths. Tool
|
|
422
|
+
text, reviewer output, and assistant prose cannot invoke the command handlers.
|
|
423
|
+
This does not claim containment after an approved host-level shell or trusted
|
|
424
|
+
custom-tool call; those trust-boundary cases can alter any same-user file.
|
|
425
|
+
|
|
426
|
+
### I13. Atomic-configuration invariant
|
|
427
|
+
|
|
428
|
+
Readers observe one complete old or new global state, never a field-wise mixture.
|
|
429
|
+
A failure before the first metadata rename leaves the old state; a failure
|
|
430
|
+
between metadata renames produces explicit Fault; a failure after both metadata
|
|
431
|
+
renames but before config publication leaves either a harmless gap above an old
|
|
432
|
+
config or, when no prior config exists, explicit Fault. Explicit repair
|
|
433
|
+
publishes a revision strictly above every surviving valid watermark and does not
|
|
434
|
+
report healthy until both metadata copies and the complete config agree. A
|
|
435
|
+
post-config-rename/pre-acknowledgement failure is resolved by rereading rather
|
|
436
|
+
than assuming either outcome.
|
|
437
|
+
|
|
438
|
+
### I14. Stale-review invariant
|
|
439
|
+
|
|
440
|
+
Any relevant global, session, backend, lifecycle, or request change between review
|
|
441
|
+
capture and a binding read makes that verdict non-admitting. Same-runtime command
|
|
442
|
+
and lifecycle changes also abort the local review signal. Changes committed by a
|
|
443
|
+
sibling process cannot actively abort provider I/O; they are detected at the
|
|
444
|
+
next applicable pre-model, post-model, or final binding read, and the bounded
|
|
445
|
+
review then denies. A change after final admission is governed by the
|
|
446
|
+
linearization rule in Section 3.
|
|
447
|
+
|
|
448
|
+
### I15. Boundedness invariant
|
|
449
|
+
|
|
450
|
+
Review attempts, aggregate review time, input size, denial history, and queued
|
|
451
|
+
work are bounded. No configuration lock is held across model or tool I/O. Subject
|
|
452
|
+
to OS/provider scheduler progress, the extension cannot deadlock or retry forever.
|
|
453
|
+
|
|
454
|
+
### I16. Denial-loop invariant
|
|
455
|
+
|
|
456
|
+
Three consecutive permission denials, or ten denials in the latest fifty guarded
|
|
457
|
+
Auto decisions in one active permission runtime and turn, interrupt the turn.
|
|
458
|
+
Any admitted Auto decision resets the consecutive count. Interruption remains
|
|
459
|
+
sticky until turn end. Catastrophic pre-runtime initialization failure has no
|
|
460
|
+
Guardian state to count against; its gates still deny every call and the
|
|
461
|
+
extension itself performs no retry.
|
|
462
|
+
|
|
463
|
+
### I17. Honest-status invariant
|
|
464
|
+
|
|
465
|
+
The extension never labels ReviewOnly as sandboxed and never treats a failed
|
|
466
|
+
macOS/Linux sandbox as ReviewOnly or Unrestricted. Unsupported-platform fallback
|
|
467
|
+
and supported-platform failure are reported non-interactively and distinctly.
|
|
468
|
+
Catastrophic engine construction is separately labeled `Auto (unavailable)` and
|
|
469
|
+
does not expose a functioning command host.
|
|
470
|
+
|
|
471
|
+
## 6. Proof sketches
|
|
472
|
+
|
|
473
|
+
### Theorem A: no denied reviewed action executes
|
|
474
|
+
|
|
475
|
+
By inspection of the lifecycle graph, `Executed` has only `Admitted` as a
|
|
476
|
+
predecessor. The review branch reaches `Admitted` only when every predicate in
|
|
477
|
+
Section 3 is true. `deny` and every failure class transition to terminal `Denied`.
|
|
478
|
+
There is no edge from `Denied` to `Admitted`; therefore a denied reviewed action
|
|
479
|
+
cannot execute.
|
|
480
|
+
|
|
481
|
+
### Theorem B: Auto never silently broadens to host shell access
|
|
482
|
+
|
|
483
|
+
Exhaust the backend cases. Under `Sandboxed`, every default-permission command is
|
|
484
|
+
spawned only by the sandbox runner; dangerous commands require an allow without
|
|
485
|
+
losing containment, and only an explicit escalation can reach the local runner
|
|
486
|
+
after an allow. Under `ReviewOnly`, every command requires an allow before spawn. Under
|
|
487
|
+
`Unavailable`, and for unknown backend state, shell execution is denied. These
|
|
488
|
+
are exhaustive, so no Auto branch starts an unreviewed unsandboxed shell.
|
|
489
|
+
|
|
490
|
+
### Theorem C: an Unrestricted parent cannot create an Unrestricted child by
|
|
491
|
+
inheritance
|
|
492
|
+
|
|
493
|
+
Session initialization is the constant function `requestedMode := Auto`; it has
|
|
494
|
+
no parent-mode input. The base case holds for a direct child. Applying the same
|
|
495
|
+
function at each edge proves by induction that every finite descendant starts
|
|
496
|
+
with requested Auto.
|
|
497
|
+
|
|
498
|
+
### Theorem D: stale approval cannot execute
|
|
499
|
+
|
|
500
|
+
Every policy-relevant mutation increments a captured revision or changes the
|
|
501
|
+
captured backend/session lifecycle. Admission requires equality of all captured
|
|
502
|
+
and current values plus equality of the canonical request. Therefore any such
|
|
503
|
+
mutation falsifies at least one necessary predicate. Monotonic revision prevents
|
|
504
|
+
Auto -> Unrestricted -> Auto ABA from restoring validity.
|
|
505
|
+
|
|
506
|
+
### Theorem E: no approval dialog can occur
|
|
507
|
+
|
|
508
|
+
The admission state machine contains no Ask state or UI edge. Reviewer outcomes
|
|
509
|
+
outside exact allow all map to `Denied`. Only explicit slash-command handlers can
|
|
510
|
+
call selection UI, and model tool calls cannot dispatch those handlers. Therefore
|
|
511
|
+
action review cannot produce an approval dialog.
|
|
512
|
+
|
|
513
|
+
### Theorem F: the extension cannot create an infinite approval loop
|
|
514
|
+
|
|
515
|
+
Each review attempt and total review have finite deadlines and attempt bounds.
|
|
516
|
+
Each action has terminal states and is never internally replayed. Across model
|
|
517
|
+
actions in an active permission runtime, every guarded Auto denial—including
|
|
518
|
+
pre-review denials—feeds the finite denial circuit breaker. Catastrophic
|
|
519
|
+
pre-runtime failure has no review engine, but its outer gates only return denial
|
|
520
|
+
and never retry. Thus the extension itself has no infinite retry path.
|
|
521
|
+
|
|
522
|
+
### Theorem G: global Off is coherent across active processes
|
|
523
|
+
|
|
524
|
+
The global mutation is serialized and atomically committed before success is
|
|
525
|
+
reported. Each active gate reads global state. Every potentially admitting
|
|
526
|
+
review reads it before model I/O and, after an allow, after model I/O and once
|
|
527
|
+
more before final admission. Order a gate and the Off commit by the gate's
|
|
528
|
+
controlling successful read. A successful read after the commit observes
|
|
529
|
+
Disabled, while a read failure denies; a reviewed binding read after the commit
|
|
530
|
+
also differs from an old capture and denies. If the final controlling read and
|
|
531
|
+
admission precede the commit, the action may enter its executor once even if
|
|
532
|
+
scheduling makes that entry occur after the commit. Thus Off is coherent and
|
|
533
|
+
linearizable among active runtimes without the false stronger claim that it
|
|
534
|
+
revokes actions already admitted. In the separately modeled catastrophic state
|
|
535
|
+
there is no active gate capable of reading Off, so the outer interception layer
|
|
536
|
+
denies.
|
|
537
|
+
|
|
538
|
+
## 7. Explicit non-guarantees
|
|
539
|
+
|
|
540
|
+
The extension does not claim:
|
|
541
|
+
|
|
542
|
+
- that the review model always makes the correct risk judgment;
|
|
543
|
+
- containment of trusted third-party extension implementation code;
|
|
544
|
+
- protection from kernel, sandbox-runtime, provider, or Pi vulnerabilities;
|
|
545
|
+
- rollback of permitted partial effects from a sandboxed compound command;
|
|
546
|
+
- mediation of commands explicitly entered by the user;
|
|
547
|
+
- protection from a hostile local process racing filesystem operations;
|
|
548
|
+
- retroactive cancellation of an action already linearized for admission when a
|
|
549
|
+
sibling process subsequently changes global policy;
|
|
550
|
+
- concurrent strong-sandbox ownership by multiple Pi sessions in one Node
|
|
551
|
+
process, or recovery from a poisoned Sandbox Runtime reset without restarting
|
|
552
|
+
that process.
|
|
553
|
+
|
|
554
|
+
These exclusions are necessary to make the positive guarantees precise rather
|
|
555
|
+
than aspirational.
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-auto-permissions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A minimal Codex-style Auto/Unrestricted permission guardrail for Pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.19.0"
|
|
9
|
+
},
|
|
10
|
+
"pi": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./src/extension.ts"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"vendor",
|
|
18
|
+
"docs",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"NOTICE",
|
|
22
|
+
"THIRD_PARTY_NOTICES.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:unit": "vitest run tests/unit",
|
|
28
|
+
"test:integration": "vitest run tests/integration",
|
|
29
|
+
"test:e2e": "vitest run tests/e2e",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
31
|
+
"test:real-sandbox": "PI_AUTO_PERMISSIONS_REAL_SANDBOX=1 vitest run tests/integration/sandbox-real.test.ts",
|
|
32
|
+
"check": "npm run typecheck && npm test",
|
|
33
|
+
"pack:check": "npm pack --dry-run",
|
|
34
|
+
"audit:runtime": "npm audit --omit=dev",
|
|
35
|
+
"sbom": "npm sbom --sbom-format cyclonedx"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@anthropic-ai/sandbox-runtime": "0.0.65",
|
|
39
|
+
"proper-lockfile": "4.1.2",
|
|
40
|
+
"tree-sitter-bash": "0.25.1",
|
|
41
|
+
"web-tree-sitter": "0.25.10"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@earendil-works/pi-ai": "0.80.10",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
46
|
+
"typebox": "1.1.38"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@earendil-works/pi-agent-core": "0.80.10",
|
|
50
|
+
"@earendil-works/pi-ai": "0.80.10",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
52
|
+
"@earendil-works/pi-tui": "0.80.10",
|
|
53
|
+
"@types/node": "24.12.4",
|
|
54
|
+
"@types/proper-lockfile": "4.1.4",
|
|
55
|
+
"typebox": "1.1.38",
|
|
56
|
+
"typescript": "5.9.3",
|
|
57
|
+
"vitest": "4.1.9"
|
|
58
|
+
}
|
|
59
|
+
}
|