@gotgenes/pi-permission-system 40.0.0 → 40.0.1

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.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v40.0.0...pi-permission-system-v40.0.1) (2026-10-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **pi-permission-system:** a /dev/null redirect keeps the core-reader wrapper exemption ([25d8455](https://github.com/gotgenes/pi-packages/commit/25d84557eac5e09974ba1ffcb5b81fccf25e0f34))
14
+
8
15
  ## [40.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v39.1.2...pi-permission-system-v40.0.0) (2026-10-06)
9
16
 
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "40.0.0",
3
+ "version": "40.0.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,4 +1,5 @@
1
1
  import { type TokenEffect, UNPROVEN_EFFECT } from "#src/access-intent/effect";
2
+ import { isDiscardDevice } from "#src/path/safe-system-paths";
2
3
  import { redirectDestinationEffect } from "./command-effects";
3
4
  import { parseUnresolvedAt } from "./parse-health";
4
5
  import type { TSNode } from "./parser";
@@ -88,9 +89,15 @@ export function redirectEffectForDestination(
88
89
  * are the operator and a descriptor, so the loop finds nothing to refuse on and
89
90
  * clears the exemption for a form nobody understood (#814).
90
91
  *
91
- * Past that, only two things clear it: a descriptor duplication (`2>&1`), which
92
- * names no file, and an operator that proves a read — reading a file alongside
93
- * a pure reader leaves it a pure reader.
92
+ * Past that, only three things clear it: a descriptor duplication (`2>&1`),
93
+ * which names no file; a destination spelled exactly `/dev/null`, whose writes
94
+ * touch no file (ADR 0013 §11 withholds the exemption only for a *real* output
95
+ * redirect, #951); and an operator that proves a read — reading a file
96
+ * alongside a pure reader leaves it a pure reader.
97
+ *
98
+ * The device check reads the node's raw text, so a quoted or computed spelling
99
+ * stays unproven, and it answers only this refusal: the token collector's proof
100
+ * still attributes the device a write, which `path_write` rules match.
94
101
  */
95
102
  export function redirectMayWriteFile(redirect: TSNode): boolean {
96
103
  if (parseUnresolvedAt(redirect)) return true;
@@ -100,6 +107,8 @@ export function redirectMayWriteFile(redirect: TSNode): boolean {
100
107
  if (!child?.isNamed) continue;
101
108
  // A source or duplicated descriptor (`2`, `&1`) names no file.
102
109
  if (DESCRIPTOR_NODE_TYPES.has(child.type)) continue;
110
+ // A write to the discard device touches no file.
111
+ if (isDiscardDevice(child.text)) continue;
103
112
  if (redirectEffectForDestination(redirect, child)?.effect !== "read") {
104
113
  return true;
105
114
  }
@@ -1,9 +1,12 @@
1
+ /** The device a write discards: nothing written to it reaches any file. */
2
+ const DISCARD_DEVICE = "/dev/null";
3
+
1
4
  /**
2
5
  * Paths that are universally safe and should never trigger external-directory checks.
3
6
  * These are OS device files: read returns EOF or process streams, write discards or goes to process streams.
4
7
  */
5
8
  export const SAFE_SYSTEM_PATHS: ReadonlySet<string> = new Set([
6
- "/dev/null",
9
+ DISCARD_DEVICE,
7
10
  "/dev/stdin",
8
11
  "/dev/stdout",
9
12
  "/dev/stderr",
@@ -16,3 +19,16 @@ export const SAFE_SYSTEM_PATHS: ReadonlySet<string> = new Set([
16
19
  export function isSafeSystemPath(normalizedPath: string): boolean {
17
20
  return SAFE_SYSTEM_PATHS.has(normalizedPath);
18
21
  }
22
+
23
+ /**
24
+ * Returns true only for the discard device, `/dev/null`, so a write to `path`
25
+ * touches no file.
26
+ *
27
+ * Narrower than {@link isSafeSystemPath}: on Linux, `/dev/std{in,out,err}`
28
+ * link to `/proc/self/fd/N`, and opening one for writing reopens the
29
+ * descriptor's underlying file with `O_TRUNC` — `cat < f > /dev/stdin`
30
+ * truncates `f`.
31
+ */
32
+ export function isDiscardDevice(path: string): boolean {
33
+ return path === DISCARD_DEVICE;
34
+ }