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.
Files changed (39) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +6 -0
  3. package/README.md +304 -0
  4. package/THIRD_PARTY_NOTICES.md +70 -0
  5. package/docs/implementation-plan.md +311 -0
  6. package/docs/invariants.md +555 -0
  7. package/package.json +59 -0
  8. package/src/canonical.ts +314 -0
  9. package/src/commands/index.ts +362 -0
  10. package/src/domain.ts +198 -0
  11. package/src/extension.ts +430 -0
  12. package/src/guardian/circuit-breaker.ts +102 -0
  13. package/src/guardian/index.ts +74 -0
  14. package/src/guardian/policy.ts +135 -0
  15. package/src/guardian/prompt.ts +379 -0
  16. package/src/guardian/reviewer.ts +599 -0
  17. package/src/guardian/types.ts +149 -0
  18. package/src/guardian/verdict.ts +211 -0
  19. package/src/pi/index.ts +13 -0
  20. package/src/pi/model-reviewer.ts +235 -0
  21. package/src/pi/transcript.ts +524 -0
  22. package/src/policy/dangerous-command.ts +312 -0
  23. package/src/policy/path-policy.ts +501 -0
  24. package/src/runtime/index.ts +1 -0
  25. package/src/runtime/permission-engine.ts +474 -0
  26. package/src/sandbox/config.ts +188 -0
  27. package/src/sandbox/controller.ts +354 -0
  28. package/src/sandbox/index.ts +26 -0
  29. package/src/sandbox/runtime.ts +28 -0
  30. package/src/sandbox/types.ts +125 -0
  31. package/src/state/config-store.ts +580 -0
  32. package/src/state/index.ts +2 -0
  33. package/src/tools/bash.ts +101 -0
  34. package/src/tools/gate.ts +74 -0
  35. package/src/tools/index.ts +2 -0
  36. package/vendor/openai-codex/NOTICE +6 -0
  37. package/vendor/openai-codex/README.md +17 -0
  38. package/vendor/openai-codex/guardian/policy.md +42 -0
  39. 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
+ }