@tduyng/pi-guard 0.0.3 → 0.0.4

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 (2) hide show
  1. package/README.md +44 -43
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,48 +1,47 @@
1
1
  # @tduyng/pi-guard
2
2
 
3
- Four guards that cost nothing until something goes wrong: parallel sessions off each other's files, secrets out of read results, git never hanging on an editor, and a stray rejection never killing the session.
3
+ Four guards for the ways a session ends badly. Three are hooks and spawn no schema, so a session that never needs them pays nothing.
4
4
 
5
- ## What it costs
5
+ | Guard | Failure it prevents |
6
+ | --- | --- |
7
+ | `reserve` | A peer session edits a file this session is working on |
8
+ | `cloak` | A key file lands in the transcript and goes to the provider |
9
+ | `git-interceptor` | Git opens an editor and hangs the turn, or `--no-verify` skips a hook |
10
+ | `reject-guard` | One unhandled promise takes down the session |
6
11
 
7
- `reserve` is a deferred tool at 215 tokens in the sessions that load it. The other three are event hooks and spawn no schema at all, so a session that never reserves a path pays nothing for this package.
12
+ Only sessions that load `reserve` pay for it, at 215 tokens.
8
13
 
9
- The failures it prevents are the expensive kind. A session killed mid task costs the whole turn, and a credential in a transcript costs a rotation.
14
+ ## Sessions in one folder
10
15
 
11
- ## Reserve
16
+ Pi serializes writes inside one process. Two sessions in one folder do not see each other, so the second overwrites a file the first is halfway through reshaping.
12
17
 
13
- Pi serializes writes inside one process, so two sessions in one folder still overwrite each other. This is the cross-session half.
18
+ A session writes what it holds to `.pi/reservations/<session>.json`. A `tool_call` hook refuses `edit` and `write` against a path a live peer holds, and it checks `bash` too, so `sed -i`, `git checkout` and a redirect into a reserved path are turned away while a read-only command passes.
14
19
 
15
- A session records what it holds in `.pi/reservations/<session>.json`, and a `tool_call` hook refuses `edit` and `write` against a path a live peer holds. Reserving `extensions/pi-goal/src/` takes the whole subtree, and an exact path takes one file. `release` with no paths gives everything back, and `list` prints every live session with what it holds.
16
-
17
- Liveness is `process.kill(pid, 0)`, so a killed session takes its reservations with it. Nothing to keep alive, no heartbeat, no daemon.
18
-
19
- `bash` is checked too: a command that writes a reserved path is turned away, whether it says `sed -i`, `git checkout`, or redirects into the path, while a command that only reads it is left alone.
20
-
21
- What this cannot see is a mutation that names no path, so `git add -A` and a bare `git commit` still take whatever is staged. Two sessions committing in one repo also race on `.git/index.lock`, and no reservation covers that. Delegate the commit with `isolation: "worktree"` on `subagent`, which works in a checkout of its own, and the branch it names outlives the checkout.
22
-
23
- ## Cloak
24
-
25
- A `tool_result` hook redacts secrets out of `read` results before they enter the model's context. Three checks, each switchable.
26
-
27
- ### Vendor keys
28
-
29
- Known API key shapes by fixed literal prefix, with a word boundary so a substring inside a longer identifier does not match. No entropy scanning and no shape guessing.
20
+ ```text
21
+ reserve({action: "reserve", paths: ["extensions/pi-goal/src/"]})
22
+ reserve({action: "list"})
23
+ reserve({action: "release"})
24
+ ```
30
25
 
31
- `AKIA` and `ASIA` for AWS, `sk-` for OpenAI and Anthropic, `sk_live_` and `sk_test_` for Stripe, `ghp_` `gho_` `ghs_` `ghr_` and `github_pat_` for GitHub, `eyJ` for JWTs, `xoxb-` `xoxp-` `xoxa-` `xoxr-` `xoxs-` for Slack, `glpat-` for GitLab, `AIza` for Google, `npm_` for npm, `SG.` for SendGrid, and `Bearer` followed by a token.
26
+ A directory reserves its whole subtree, an exact path reserves one file, and `release` with no paths gives everything back. The refusal names the holder, so the other session is one that can be inspected.
32
27
 
33
- Each match becomes the first 4 characters, then stars (minimum 8, maximum 20), then `[REDACTED:type]`.
28
+ Liveness is `process.kill(pid, 0)`, so a killed session takes its reservations with it. No heartbeat and no daemon. Writes go through a temp file and a rename, so two sessions reserving at once cannot leave a half-written file.
34
29
 
35
- ### PEM blocks
30
+ A mutation that names no path is invisible to this, so `git add -A` and a bare `git commit` still take whatever is staged. Two sessions committing in one repository also race on `.git/index.lock`, and no reservation covers that. Commit from a `subagent` with `isolation: "worktree"` instead, whose checkout is its own.
36
31
 
37
- `-----BEGIN ...-----` certificate and key blocks are replaced with `[REDACTED:PEM TYPE]`.
32
+ ## Secrets in the transcript
38
33
 
39
- ### Sensitive file types
34
+ A `tool_result` hook cleans `read` results on their way into the model. Three checks, each switchable:
40
35
 
41
- A path that matches a sensitive pattern replaces the whole read with one marker, such as `[REDACTED:Protected File (SSH Key)]`. The list covers `.pem`, `.crt`, `.cer`, `.p12`, `.pfx`, SSH keys and `known_hosts` and `authorized_keys`, `.aws/credentials`, `.npmrc`, `.pypirc`, `.netrc`, `.docker/config.json`, and `.kube/config`.
36
+ | Check | Catches | Replacement |
37
+ | --- | --- | --- |
38
+ | Vendor keys | AWS, OpenAI, Anthropic, Stripe, GitHub, Slack, GitLab, Google, npm, SendGrid, JWTs, `Bearer` | First 4 characters, stars, `[REDACTED:type]` |
39
+ | PEM blocks | `-----BEGIN ...-----` certificates and keys | `[REDACTED:PEM TYPE]` |
40
+ | Sensitive paths | `.pem`, `.crt`, `.p12`, SSH keys, `.aws/credentials`, `.npmrc`, `.netrc`, `.docker/config.json`, `.kube/config` | The whole read becomes `[REDACTED:Protected File (...)]` |
42
41
 
43
- ### Rules of your own
42
+ Secrets have known prefixes, so `AKIA`, `sk-`, `ghp_`, `glpat-`, `xoxb-`, `AIza` and a dozen more are matched as literals with a word boundary. An earlier version scanned by entropy and flagged nix store hashes and `sha256` digests in derivations, so that check is gone.
44
43
 
45
- `cloak.json` in the agent directory, or `.pi/cloak.json` in a project, adds patterns. A project file overrides the scalar keys and appends its `patterns` to the agent dir ones.
44
+ Custom rules go in `<agent-dir>/cloak.json` or `.pi/cloak.json`. A project file overrides the scalar keys and appends its `patterns`.
46
45
 
47
46
  | Key | Default | Meaning |
48
47
  | --- | --- | --- |
@@ -50,28 +49,24 @@ A path that matches a sensitive pattern replaces the whole read with one marker,
50
49
  | `cloakCharacter` | `*` | Masking character |
51
50
  | `cloakLength` | `null` | Fixed replacement length, null keeps the original |
52
51
  | `tryAllPatterns` | `true` | Apply every matching rule, not just the first |
53
- | `patterns` | `[]` | Rules with `filePattern` (glob), `cloakPattern` (regex), and optional `replace` |
52
+ | `patterns` | `[]` | Rules with `filePattern` (glob), `cloakPattern` (regex), optional `replace` |
54
53
  | `vendorKeys` | `true` | Built-in prefix detection |
55
54
  | `pemBlocks` | `true` | PEM detection |
56
- | `sensitiveFileTypes` | `true` | Whole-file redaction for sensitive paths |
57
-
58
- When two matches overlap the same span, the first applied wins and later overlapping matches are skipped. `/cloak-status` shows whether cloak is on, how many custom patterns loaded, and which built-in checks are active.
55
+ | `sensitiveFileTypes` | `true` | Whole-file redaction |
59
56
 
60
- A nix store hash, a `narHash` field, or a sha256 digest in a derivation will not false-positive, because nothing scans by entropy. The hook does not touch the outbound provider payload, the assistant's reply, or any tool other than `read`. A custom `patterns` match replaces only the matched spans, while a sensitive file type replaces the whole read.
57
+ When two matches overlap, the first rule applied wins. A custom pattern replaces only its matched spans, while a sensitive file type replaces the whole read. `/cloak-status` shows whether cloak is on, how many custom patterns loaded, and which built-in checks are active.
61
58
 
62
- ## Git interceptor
59
+ ## Git in a non-interactive turn
63
60
 
64
- Two guards for agent-driven git.
61
+ `GIT_EDITOR=true`, `GIT_SEQUENCE_EDITOR=true` and `GIT_MERGE_AUTOEDIT=no` go on `process.env`. Pi builds a child's environment by spreading `process.env`, so one assignment covers the bash tool, `bg`, and every other spawn.
65
62
 
66
- Editor hang prevention sets `GIT_EDITOR=true`, `GIT_SEQUENCE_EDITOR=true`, and `GIT_MERGE_AUTOEDIT=no` on `process.env`. Pi builds a child's environment by spreading `process.env`, so one assignment covers the bash tool, `bg`, and every other spawn.
63
+ The extension blocks a bash command containing `--no-verify`, because the agent should fix what the hook complains about or ask. The check is string matching, best effort, and not a security boundary.
67
64
 
68
- Hook bypass prevention blocks any bash command containing `--no-verify`. The agent should fix what the hook is complaining about, or ask you. This is string matching and best effort, not a security boundary.
65
+ ## The rejection that kills a session
69
66
 
70
- ## Reject guard
67
+ Pi installs an `uncaughtException` handler and no `unhandledRejection` handler, so one unhandled rejected promise tears down the session. The guard adds the missing listener, logs the reason with a stack trace to stderr prefixed `[reject-guard] unhandled rejection (session kept alive):`, and the turn continues.
71
68
 
72
- Pi registers an `uncaughtException` handler but no `unhandledRejection` handler, so one unhandled rejected promise tears down the whole session. This guard adds the missing listener, logs the reason with a stack trace to stderr, and keeps the session alive. The line starts with `[reject-guard] unhandled rejection (session kept alive):`.
73
-
74
- The listener installs once. `/reload` re-evaluates the module, and an install flag on `globalThis` keeps a second registration from logging every rejection twice.
69
+ It installs once. `/reload` re-evaluates the module, and a flag on `globalThis` stops a second registration from logging every rejection twice.
75
70
 
76
71
  ## Install
77
72
 
@@ -81,4 +76,10 @@ pi install npm:@tduyng/pi-guard
81
76
 
82
77
  Local checkout: `pi install /path/to/dotfiles/pi/agent/extensions/pi-guard`
83
78
 
84
- All four guards activate on install with no configuration. Customize redaction through `cloak.json`.
79
+ All four guards activate on install. Add `cloak.json` when a rule fires too widely or misses a secret shape.
80
+
81
+ ## Limits
82
+
83
+ - Cloak reads the `read` result on its way into the model. It does not touch the outbound provider payload, the assistant's reply, or any tool other than `read`, and it does not redact a file pasted into a prompt.
84
+ - A reservation is advisory between pi sessions. A shell opened by hand is outside it.
85
+ - `--no-verify` blocking can be worked around by anyone who wants to.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tduyng/pi-guard",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Secret redaction, cross-session path reservations, git safety, and a rejection guard. Three of the four add no schema at all.",
5
5
  "keywords": [
6
6
  "pi",