pi-openappa 0.2.0 → 0.3.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.
@@ -0,0 +1,1068 @@
1
+ # pi-openappa default policy — a Pi session's complete, self-contained guard.
2
+ #
3
+ # Install (either):
4
+ # cp templates/appa.toml ~/.config/appa/appa.toml # from a checkout
5
+ # /appa init # from inside Pi
6
+ # or scope it to one project:
7
+ # /appa init project # writes ./appa.toml + .pi/openappa
8
+ #
9
+ # ══════════════════════════════════════════════════════════════════════════
10
+ # THE FENCE MAP (what this file encodes; validate: `just policy-test`)
11
+ # ══════════════════════════════════════════════════════════════════════════
12
+ # appa gates flows, not tools. Sources mark data; sinks check it:
13
+ #
14
+ # web (curl/wget, web_explore, context7) ──suspicious──▶ the session
15
+ # .env / credentials ──self-narrow─▶ the session
16
+ #
17
+ # A session that carries suspicious or self-narrowed data is REFUSED at:
18
+ # • any edit to an existing file — except documentation (docs/, openspec/,
19
+ # *.md): research lands in docs, it does not rewrite code [day B]
20
+ # • writes to files that execute later: tests, devops (Makefile,
21
+ # justfile, scripts, CI), infra (terraform, k8s, helm…), and
22
+ # credential-shaped paths [day A]
23
+ # • git push / gh pr|issue|release|api|repo: no tainted data leaves
24
+ # the machine [day C]
25
+ # • web tools after reading .env: secrets may not reach a query [day D]
26
+ # • mem_save / mem_update: poisoned memory must not persist [day F]
27
+ # • edits to appa's own policy files [guard]
28
+ #
29
+ # A clean session (no web, no secrets) flows everywhere: reads, edits,
30
+ # tests, commits — nothing below ever trips. [day A]
31
+ #
32
+ # Reset: taint lives in the trajectory label and only narrows. The manual
33
+ # reset is a NEW session (resume/fork inherit the label). There is no
34
+ # /untaint by design.
35
+ #
36
+ # ══════════════════════════════════════════════════════════════════════════
37
+ # GROUND RULES (verified against appa 0.31.1; docs/wire-notes.md)
38
+ # ══════════════════════════════════════════════════════════════════════════
39
+ # 1. Tool naming: Pi built-ins map to Claude-Code policy names (bash→Bash,
40
+ # read→Read, edit→Edit, write→Write, grep→Grep, find→Glob, ls→LS,
41
+ # powershell→PowerShell); custom/MCP tools pass through verbatim
42
+ # (`subagent`, `mem_search`, …), declared here under their Pi names.
43
+ # 2. Argument naming: `tool_input` crosses the wire exactly as Pi sent it,
44
+ # so selectors use Pi's argument names — Read/Edit/Write take `path`,
45
+ # Bash/PowerShell take `command`. The stock claude-code battery selects
46
+ # on `file_path` and never matches a Pi call; this file spells it right.
47
+ # 3. No annotators, on purpose: `builtin = "claude-code"` annotators are
48
+ # host-coupled and unreachable under Pi (they fail closed and withhold
49
+ # results). This policy is static rules only — nothing here can be
50
+ # unreachable. Consequence: deny-undeclared is the only stance (a `*`
51
+ # wildcard REQUIRES an annotator; static deltas on it refuse to load).
52
+ # 4. Rules match in order; first match wins. Selector patterns are globs on
53
+ # the full argument value (`*` = any sequence).
54
+ # 5. Approximations: `write` cannot distinguish create from overwrite, so
55
+ # "research may create, never overwrite" is enforced for protected
56
+ # categories only; a tainted overwrite of unprotected src via `write`
57
+ # is a documented hole (use `edit` discipline). "Flow ends after a docs
58
+ # edit" is approximated by docs being the only tainted-editable sink.
59
+
60
+ [policy]
61
+ version = 2
62
+
63
+ # ─── Bash: commands that name requester credentials are `self` data ────────
64
+ #
65
+ # Narrows the session to `self`: the result is withheld and `redact-secrets`
66
+ # (below) masks tokens before the output returns to `public`. A selector is
67
+ # a glob on the command as written; a spelling these miss — inside `$(...)`,
68
+ # a heredoc, an alias — falls through to later rules. Extend freely.
69
+
70
+ [[policy.tool]]
71
+ name = "host/claude-code/Bash(command:*.env*)"
72
+ delta = { audience = ["self"] }
73
+ tags = ["credentials"]
74
+
75
+ [[policy.tool]]
76
+ name = "host/claude-code/Bash(command:*.claude.json*)"
77
+ delta = { audience = ["self"] }
78
+ tags = ["credentials"]
79
+
80
+ [[policy.tool]]
81
+ name = "host/claude-code/Bash(command:*.credentials.json*)"
82
+ delta = { audience = ["self"] }
83
+ tags = ["credentials"]
84
+
85
+ [[policy.tool]]
86
+ name = "host/claude-code/Bash(command:*.ssh/*)"
87
+ delta = { audience = ["self"] }
88
+ tags = ["credentials"]
89
+
90
+ [[policy.tool]]
91
+ name = "host/claude-code/Bash(command:*id_rsa*)"
92
+ delta = { audience = ["self"] }
93
+ tags = ["credentials"]
94
+
95
+ [[policy.tool]]
96
+ name = "host/claude-code/Bash(command:*id_ed25519*)"
97
+ delta = { audience = ["self"] }
98
+ tags = ["credentials"]
99
+
100
+ [[policy.tool]]
101
+ name = "host/claude-code/Bash(command:*.netrc*)"
102
+ delta = { audience = ["self"] }
103
+ tags = ["credentials"]
104
+
105
+ [[policy.tool]]
106
+ name = "host/claude-code/Bash(command:*.git-credentials*)"
107
+ delta = { audience = ["self"] }
108
+ tags = ["credentials"]
109
+
110
+ [[policy.tool]]
111
+ name = "host/claude-code/Bash(command:*.aws/*)"
112
+ delta = { audience = ["self"] }
113
+ tags = ["credentials"]
114
+
115
+ [[policy.tool]]
116
+ name = "host/claude-code/Bash(command:*.azure/*)"
117
+ delta = { audience = ["self"] }
118
+ tags = ["credentials"]
119
+
120
+ [[policy.tool]]
121
+ name = "host/claude-code/Bash(command:*.kube/*)"
122
+ delta = { audience = ["self"] }
123
+ tags = ["credentials"]
124
+
125
+ [[policy.tool]]
126
+ name = "host/claude-code/Bash(command:*.gnupg/*)"
127
+ delta = { audience = ["self"] }
128
+ tags = ["credentials"]
129
+
130
+ [[policy.tool]]
131
+ name = "host/claude-code/Bash(command:*.docker/config.json*)"
132
+ delta = { audience = ["self"] }
133
+ tags = ["credentials"]
134
+
135
+ [[policy.tool]]
136
+ name = "host/claude-code/Bash(command:*.config/gh/*)"
137
+ delta = { audience = ["self"] }
138
+ tags = ["credentials"]
139
+
140
+ [[policy.tool]]
141
+ name = "host/claude-code/Bash(command:*.config/gcloud/*)"
142
+ delta = { audience = ["self"] }
143
+ tags = ["credentials"]
144
+
145
+ [[policy.tool]]
146
+ name = "host/claude-code/Bash(command:*.npmrc*)"
147
+ delta = { audience = ["self"] }
148
+ tags = ["credentials"]
149
+
150
+ [[policy.tool]]
151
+ name = "host/claude-code/Bash(command:*.pypirc*)"
152
+ delta = { audience = ["self"] }
153
+ tags = ["credentials"]
154
+
155
+ [[policy.tool]]
156
+ name = "host/claude-code/Bash(command:*.password-store/*)"
157
+ delta = { audience = ["self"] }
158
+ tags = ["credentials"]
159
+
160
+ [[policy.tool]]
161
+ name = "host/claude-code/Bash(command:*.vault-token*)"
162
+ delta = { audience = ["self"] }
163
+ tags = ["credentials"]
164
+
165
+ [[policy.tool]]
166
+ name = "host/claude-code/Bash(command:*/proc/*environ*)"
167
+ delta = { audience = ["self"] }
168
+ tags = ["credentials"]
169
+
170
+ # ─── Bash: trusted documentation domains stay clean ────────────────────────
171
+ #
172
+ # The official default allow-lists trusted docs domains on WebFetch; Pi has
173
+ # no URL-argumented web tool, so the same carve-out is spelled on curl.
174
+ # Reading these keeps the session's label: docs sites whose authors are the
175
+ # platform vendors. Everything else fetched is foreign text (next block).
176
+ # Extend with the domains you trust; wget is not carved out — add mirrors
177
+ # if you use it for docs.
178
+
179
+ [[policy.tool]]
180
+ name = "host/claude-code/Bash(command:*curl*docs.anthropic.com*)"
181
+ delta = {}
182
+
183
+ [[policy.tool]]
184
+ name = "host/claude-code/Bash(command:*curl*code.claude.com*)"
185
+ delta = {}
186
+
187
+ [[policy.tool]]
188
+ name = "host/claude-code/Bash(command:*curl*openappa.com*)"
189
+ delta = {}
190
+
191
+ [[policy.tool]]
192
+ name = "host/claude-code/Bash(command:*curl*nodejs.org*)"
193
+ delta = {}
194
+
195
+ [[policy.tool]]
196
+ name = "host/claude-code/Bash(command:*curl*developer.mozilla.org*)"
197
+ delta = {}
198
+
199
+ [[policy.tool]]
200
+ name = "host/claude-code/Bash(command:*curl*typescriptlang.org*)"
201
+ delta = {}
202
+
203
+ [[policy.tool]]
204
+ name = "host/claude-code/Bash(command:*curl*doc.rust-lang.org*)"
205
+ delta = {}
206
+
207
+ [[policy.tool]]
208
+ name = "host/claude-code/Bash(command:*curl*docs.python.org*)"
209
+ delta = {}
210
+
211
+ # ─── Bash: publishing — tainted data may not leave the machine ─────────────
212
+ #
213
+ # A push, PR, issue, release, or API call carries the session's data to a
214
+ # remote: the injection-to-GitHub exfil channel. Deterministic "ask": the
215
+ # call runs only from a clean (fully trusted) trajectory. Normal `git
216
+ # commit` is NOT fenced — local history stays local. Blocked after
217
+ # research? Start a fresh session and push from there.
218
+
219
+ [[policy.tool]]
220
+ name = "host/claude-code/Bash(command:*git push*)"
221
+ requires = { trust = "trusted" }
222
+ delta = {}
223
+
224
+ [[policy.tool]]
225
+ name = "host/claude-code/Bash(command:*git -* push*)"
226
+ requires = { trust = "trusted" }
227
+ delta = {}
228
+
229
+ [[policy.tool]]
230
+ name = "host/claude-code/Bash(command:*gh pr *)"
231
+ requires = { trust = "trusted" }
232
+ delta = {}
233
+
234
+ [[policy.tool]]
235
+ name = "host/claude-code/Bash(command:*gh issue *)"
236
+ requires = { trust = "trusted" }
237
+ delta = {}
238
+
239
+ [[policy.tool]]
240
+ name = "host/claude-code/Bash(command:*gh release *)"
241
+ requires = { trust = "trusted" }
242
+ delta = {}
243
+
244
+ [[policy.tool]]
245
+ name = "host/claude-code/Bash(command:*gh api *)"
246
+ requires = { trust = "trusted" }
247
+ delta = {}
248
+
249
+ [[policy.tool]]
250
+ name = "host/claude-code/Bash(command:*gh repo *)"
251
+ requires = { trust = "trusted" }
252
+ delta = {}
253
+
254
+ [[policy.tool]]
255
+ name = "host/claude-code/Bash(command:*gh gist *)"
256
+ requires = { trust = "trusted" }
257
+ delta = {}
258
+
259
+ # ─── Bash: fetch-shaped commands are the web source ────────────────────────
260
+ #
261
+ # This is the middle path: not all shell output is foreign — `ls` and
262
+ # `npm test` are yours — but curl/wget return text written by strangers.
263
+ # Only these narrow trust. A session that curled a blog post keeps working
264
+ # everywhere except the fenced sinks (see the fence map).
265
+
266
+ [[policy.tool]]
267
+ name = "host/claude-code/Bash(command:*curl*)"
268
+ delta = { trust = "suspicious" }
269
+
270
+ [[policy.tool]]
271
+ name = "host/claude-code/Bash(command:*wget*)"
272
+ delta = { trust = "suspicious" }
273
+
274
+ # ─── Bash: every other command ─────────────────────────────────────────────
275
+ #
276
+ # Your own machine's output, trusted. (The strict alternative — marking all
277
+ # shell output suspicious — over-taints: one `ls` would refuse every
278
+ # trusted-only sink below forever, with no annotator to tell `ls` from
279
+ # `curl`. Fetch-selectors above are the honest split.)
280
+ #
281
+ # Scope note, cross-checked against community hardening guides: appa is an
282
+ # information-flow engine — it fences where DATA goes, not what a command
283
+ # DOES. Destructive-command deny-lists (`rm -rf /`, `sudo`, `chmod`,
284
+ # shutdown…) that Claude Code / Cursor configs carry are not expressible
285
+ # here (v2 has no static unconditional deny) and are deliberately out of
286
+ # scope: the OS, backups, and you remain that guard.
287
+
288
+ [[policy.tool]]
289
+ name = "host/claude-code/Bash"
290
+ description = "Runs one shell command and returns its output."
291
+ delta = {}
292
+
293
+ # ─── PowerShell: the Bash rules, spelled for Windows hosts ─────────────────
294
+ # Directory rules are written for both separators; `\\` is one literal
295
+ # backslash in a selector.
296
+
297
+ [[policy.tool]]
298
+ name = 'host/claude-code/PowerShell(command:*.env*)'
299
+ delta = { audience = ["self"] }
300
+ tags = ["credentials"]
301
+
302
+ [[policy.tool]]
303
+ name = 'host/claude-code/PowerShell(command:*.ssh/*)'
304
+ delta = { audience = ["self"] }
305
+ tags = ["credentials"]
306
+
307
+ [[policy.tool]]
308
+ name = 'host/claude-code/PowerShell(command:*.ssh\\*)'
309
+ delta = { audience = ["self"] }
310
+ tags = ["credentials"]
311
+
312
+ [[policy.tool]]
313
+ name = 'host/claude-code/PowerShell(command:*id_rsa*)'
314
+ delta = { audience = ["self"] }
315
+ tags = ["credentials"]
316
+
317
+ [[policy.tool]]
318
+ name = 'host/claude-code/PowerShell(command:*id_ed25519*)'
319
+ delta = { audience = ["self"] }
320
+ tags = ["credentials"]
321
+
322
+ [[policy.tool]]
323
+ name = 'host/claude-code/PowerShell(command:*.netrc*)'
324
+ delta = { audience = ["self"] }
325
+ tags = ["credentials"]
326
+
327
+ [[policy.tool]]
328
+ name = 'host/claude-code/PowerShell(command:*.git-credentials*)'
329
+ delta = { audience = ["self"] }
330
+ tags = ["credentials"]
331
+
332
+ [[policy.tool]]
333
+ name = 'host/claude-code/PowerShell(command:*.aws/*)'
334
+ delta = { audience = ["self"] }
335
+ tags = ["credentials"]
336
+
337
+ [[policy.tool]]
338
+ name = 'host/claude-code/PowerShell(command:*.aws\\*)'
339
+ delta = { audience = ["self"] }
340
+ tags = ["credentials"]
341
+
342
+ [[policy.tool]]
343
+ name = 'host/claude-code/PowerShell(command:*.kube/*)'
344
+ delta = { audience = ["self"] }
345
+ tags = ["credentials"]
346
+
347
+ [[policy.tool]]
348
+ name = 'host/claude-code/PowerShell(command:*.kube\\*)'
349
+ delta = { audience = ["self"] }
350
+ tags = ["credentials"]
351
+
352
+ [[policy.tool]]
353
+ name = 'host/claude-code/PowerShell(command:*.gnupg/*)'
354
+ delta = { audience = ["self"] }
355
+ tags = ["credentials"]
356
+
357
+ [[policy.tool]]
358
+ name = 'host/claude-code/PowerShell(command:*.gnupg\\*)'
359
+ delta = { audience = ["self"] }
360
+ tags = ["credentials"]
361
+
362
+ [[policy.tool]]
363
+ name = 'host/claude-code/PowerShell(command:*.docker/config.json*)'
364
+ delta = { audience = ["self"] }
365
+ tags = ["credentials"]
366
+
367
+ [[policy.tool]]
368
+ name = 'host/claude-code/PowerShell(command:*.docker\\config.json*)'
369
+ delta = { audience = ["self"] }
370
+ tags = ["credentials"]
371
+
372
+ [[policy.tool]]
373
+ name = 'host/claude-code/PowerShell(command:*.config/gh/*)'
374
+ delta = { audience = ["self"] }
375
+ tags = ["credentials"]
376
+
377
+ [[policy.tool]]
378
+ name = 'host/claude-code/PowerShell(command:*.config\\gh\\*)'
379
+ delta = { audience = ["self"] }
380
+ tags = ["credentials"]
381
+
382
+ [[policy.tool]]
383
+ name = 'host/claude-code/PowerShell(command:*curl*)'
384
+ delta = { trust = "suspicious" }
385
+
386
+ [[policy.tool]]
387
+ name = 'host/claude-code/PowerShell(command:*wget*)'
388
+ delta = { trust = "suspicious" }
389
+
390
+ [[policy.tool]]
391
+ name = 'host/claude-code/PowerShell(command:*git push*)'
392
+ requires = { trust = "trusted" }
393
+ delta = {}
394
+
395
+ [[policy.tool]]
396
+ name = 'host/claude-code/PowerShell(command:*git -* push*)'
397
+ requires = { trust = "trusted" }
398
+ delta = {}
399
+
400
+ [[policy.tool]]
401
+ name = "host/claude-code/PowerShell"
402
+ description = "Runs one PowerShell command and returns its output."
403
+ delta = {}
404
+
405
+ # ─── Read: the requester's own secrets are `self` data ─────────────────────
406
+ # A selector matches the argument as written, absolute or relative, not the
407
+ # resolved path. Every other path keeps the session's label; no rule here
408
+ # blocks a read or lowers its trust. Pi's argument is `path`.
409
+
410
+ [[policy.tool]]
411
+ name = "host/claude-code/Read(path:*/.env*)"
412
+ delta = { audience = ["self"] }
413
+
414
+ [[policy.tool]]
415
+ name = "host/claude-code/Read(path:.env*)"
416
+ delta = { audience = ["self"] }
417
+
418
+ [[policy.tool]]
419
+ name = "host/claude-code/Read(path:*/.ssh/*)"
420
+ delta = { audience = ["self"] }
421
+
422
+ [[policy.tool]]
423
+ name = "host/claude-code/Read(path:.ssh/*)"
424
+ delta = { audience = ["self"] }
425
+
426
+ [[policy.tool]]
427
+ name = "host/claude-code/Read(path:*/.aws/*)"
428
+ delta = { audience = ["self"] }
429
+
430
+ [[policy.tool]]
431
+ name = "host/claude-code/Read(path:*/.kube/*)"
432
+ delta = { audience = ["self"] }
433
+
434
+ [[policy.tool]]
435
+ name = "host/claude-code/Read(path:*/.gnupg/*)"
436
+ delta = { audience = ["self"] }
437
+
438
+ [[policy.tool]]
439
+ name = "host/claude-code/Read(path:*/.netrc)"
440
+ delta = { audience = ["self"] }
441
+
442
+ [[policy.tool]]
443
+ name = "host/claude-code/Read(path:*/.git-credentials)"
444
+ delta = { audience = ["self"] }
445
+
446
+ [[policy.tool]]
447
+ name = "host/claude-code/Read(path:*/.npmrc)"
448
+ delta = { audience = ["self"] }
449
+
450
+ [[policy.tool]]
451
+ name = "host/claude-code/Read(path:*/.vault-token)"
452
+ delta = { audience = ["self"] }
453
+
454
+ [[policy.tool]]
455
+ name = "host/claude-code/Read(path:*/.password-store/*)"
456
+ delta = { audience = ["self"] }
457
+
458
+ [[policy.tool]]
459
+ name = "host/claude-code/Read"
460
+ description = "Reads a file from the local filesystem."
461
+ delta = {}
462
+
463
+ # ─── Grep / Glob / LS: local search over this user's files ─────────────────
464
+
465
+ [[policy.tool]]
466
+ name = "host/claude-code/Grep"
467
+ description = "Searches file contents with a pattern."
468
+ delta = {}
469
+
470
+ [[policy.tool]]
471
+ name = "host/claude-code/Glob"
472
+ description = "Finds files by name pattern."
473
+ delta = {}
474
+
475
+ [[policy.tool]]
476
+ name = "host/claude-code/LS"
477
+ description = "Lists a directory."
478
+ delta = {}
479
+
480
+ # ─── Edit: research lands in docs, it does not rewrite code ────────────────
481
+ #
482
+ # Documentation first (the landing zone): docs directories, openspec, and
483
+ # markdown anywhere stay editable even by a tainted session — that is where
484
+ # research output belongs. Everything else below requires a fully trusted
485
+ # trajectory: a session that curled a blog post may create new files (see
486
+ # Write) and edit docs, but not touch existing code. This mirrors the
487
+ # read-only scout / full Engineer split: research sessions report, clean
488
+ # sessions build.
489
+
490
+ [[policy.tool]]
491
+ name = "host/claude-code/Edit(path:*/docs/*)"
492
+ delta = {}
493
+
494
+ [[policy.tool]]
495
+ name = "host/claude-code/Edit(path:docs/*)"
496
+ delta = {}
497
+
498
+ [[policy.tool]]
499
+ name = "host/claude-code/Edit(path:*/openspec/*)"
500
+ delta = {}
501
+
502
+ [[policy.tool]]
503
+ name = "host/claude-code/Edit(path:openspec/*)"
504
+ delta = {}
505
+
506
+ [[policy.tool]]
507
+ name = "host/claude-code/Edit(path:*.md)"
508
+ delta = {}
509
+
510
+ [[policy.tool]]
511
+ name = "host/claude-code/Edit(path:*.mdx)"
512
+ delta = {}
513
+
514
+ # The policy guards itself: a session may not rewrite the rules that bind
515
+ # it, tainted or not. Edit policies by hand, in a fresh session.
516
+
517
+ [[policy.tool]]
518
+ name = "host/claude-code/Edit(path:*/appa/appa.toml)"
519
+ requires = { trust = "trusted" }
520
+ delta = {}
521
+
522
+ [[policy.tool]]
523
+ name = "host/claude-code/Edit(path:*/appa/batteries/*)"
524
+ requires = { trust = "trusted" }
525
+ delta = {}
526
+
527
+ [[policy.tool]]
528
+ name = "host/claude-code/Edit"
529
+ description = "Applies targeted edits to an existing file."
530
+ requires = { trust = "trusted" }
531
+ delta = {}
532
+
533
+ # ─── Write: create freely; protect what executes later ─────────────────────
534
+ #
535
+ # `write` is the create path (new files, notes, reports) and stays open —
536
+ # with two families fenced. First, anything credential-shaped (an untrusted
537
+ # flow must not be laundered into secrets). Second, the files that execute
538
+ # later with more privilege than the session: tests (a poisoned test that
539
+ # always passes), devops (Makefile, justfile, scripts/, CI workflows,
540
+ # Dockerfiles), and infra (terraform, k8s, helm, ansible, pulumi). Poison
541
+ # that persists or executes is the memory-poisoning problem, spelled for
542
+ # files. NOTE: `write` cannot distinguish create from overwrite — a tainted
543
+ # overwrite of unprotected src is a known hole (documented, not fenced).
544
+
545
+ [[policy.tool]]
546
+ name = "host/claude-code/Write(path:*/.env*)"
547
+ requires = { trust = "trusted" }
548
+ delta = {}
549
+
550
+ [[policy.tool]]
551
+ name = "host/claude-code/Write(path:.env*)"
552
+ requires = { trust = "trusted" }
553
+ delta = {}
554
+
555
+ [[policy.tool]]
556
+ name = "host/claude-code/Write(path:*/.ssh/*)"
557
+ requires = { trust = "trusted" }
558
+ delta = {}
559
+
560
+ [[policy.tool]]
561
+ name = "host/claude-code/Write(path:*/.aws/*)"
562
+ requires = { trust = "trusted" }
563
+ delta = {}
564
+
565
+ [[policy.tool]]
566
+ name = "host/claude-code/Write(path:*/.kube/*)"
567
+ requires = { trust = "trusted" }
568
+ delta = {}
569
+
570
+ [[policy.tool]]
571
+ name = "host/claude-code/Write(path:*/.gnupg/*)"
572
+ requires = { trust = "trusted" }
573
+ delta = {}
574
+
575
+ [[policy.tool]]
576
+ name = "host/claude-code/Write(path:*/.netrc)"
577
+ requires = { trust = "trusted" }
578
+ delta = {}
579
+
580
+ [[policy.tool]]
581
+ name = "host/claude-code/Write(path:*/.git-credentials)"
582
+ requires = { trust = "trusted" }
583
+ delta = {}
584
+
585
+ [[policy.tool]]
586
+ name = "host/claude-code/Write(path:*/.npmrc)"
587
+ requires = { trust = "trusted" }
588
+ delta = {}
589
+
590
+ [[policy.tool]]
591
+ name = "host/claude-code/Write(path:*/.pypirc)"
592
+ requires = { trust = "trusted" }
593
+ delta = {}
594
+
595
+ [[policy.tool]]
596
+ name = "host/claude-code/Write(path:*/.vault-token)"
597
+ requires = { trust = "trusted" }
598
+ delta = {}
599
+
600
+ [[policy.tool]]
601
+ name = "host/claude-code/Write(path:*/.password-store/*)"
602
+ requires = { trust = "trusted" }
603
+ delta = {}
604
+
605
+ [[policy.tool]]
606
+ name = "host/claude-code/Write(path:*id_rsa*)"
607
+ requires = { trust = "trusted" }
608
+ delta = {}
609
+
610
+ [[policy.tool]]
611
+ name = "host/claude-code/Write(path:*id_ed25519*)"
612
+ requires = { trust = "trusted" }
613
+ delta = {}
614
+
615
+ [[policy.tool]]
616
+ name = "host/claude-code/Write(path:*/id_*)"
617
+ requires = { trust = "trusted" }
618
+ delta = {}
619
+
620
+ [[policy.tool]]
621
+ name = "host/claude-code/Write(path:*credentials*)"
622
+ requires = { trust = "trusted" }
623
+ delta = {}
624
+
625
+ [[policy.tool]]
626
+ name = "host/claude-code/Write(path:*secrets*)"
627
+ requires = { trust = "trusted" }
628
+ delta = {}
629
+
630
+ [[policy.tool]]
631
+ name = "host/claude-code/Write(path:*token)"
632
+ requires = { trust = "trusted" }
633
+ delta = {}
634
+
635
+ [[policy.tool]]
636
+ name = "host/claude-code/Write(path:*auth.json)"
637
+ requires = { trust = "trusted" }
638
+ delta = {}
639
+
640
+ [[policy.tool]]
641
+ name = "host/claude-code/Write(path:*.pem)"
642
+ requires = { trust = "trusted" }
643
+ delta = {}
644
+
645
+ [[policy.tool]]
646
+ name = "host/claude-code/Write(path:*.key)"
647
+ requires = { trust = "trusted" }
648
+ delta = {}
649
+
650
+ [[policy.tool]]
651
+ name = "host/claude-code/Write(path:*.p12)"
652
+ requires = { trust = "trusted" }
653
+ delta = {}
654
+
655
+ [[policy.tool]]
656
+ name = "host/claude-code/Write(path:*.pfx)"
657
+ requires = { trust = "trusted" }
658
+ delta = {}
659
+
660
+ [[policy.tool]]
661
+ name = "host/claude-code/Write(path:*.jks)"
662
+ requires = { trust = "trusted" }
663
+ delta = {}
664
+
665
+ [[policy.tool]]
666
+ name = "host/claude-code/Write(path:*.keystore)"
667
+ requires = { trust = "trusted" }
668
+ delta = {}
669
+
670
+ [[policy.tool]]
671
+ name = "host/claude-code/Write(path:/etc/ssh/*)"
672
+ requires = { trust = "trusted" }
673
+ delta = {}
674
+
675
+ [[policy.tool]]
676
+ name = "host/claude-code/Write(path:/etc/sudoers*)"
677
+ requires = { trust = "trusted" }
678
+ delta = {}
679
+
680
+ [[policy.tool]]
681
+ name = "host/claude-code/Write(path:*_history)"
682
+ requires = { trust = "trusted" }
683
+ delta = {}
684
+
685
+ # — files that execute later: tests —
686
+
687
+ [[policy.tool]]
688
+ name = "host/claude-code/Write(path:*/test/*)"
689
+ requires = { trust = "trusted" }
690
+ delta = {}
691
+
692
+ [[policy.tool]]
693
+ name = "host/claude-code/Write(path:*/tests/*)"
694
+ requires = { trust = "trusted" }
695
+ delta = {}
696
+
697
+ [[policy.tool]]
698
+ name = "host/claude-code/Write(path:test/*)"
699
+ requires = { trust = "trusted" }
700
+ delta = {}
701
+
702
+ [[policy.tool]]
703
+ name = "host/claude-code/Write(path:tests/*)"
704
+ requires = { trust = "trusted" }
705
+ delta = {}
706
+
707
+ [[policy.tool]]
708
+ name = "host/claude-code/Write(path:*.test.*)"
709
+ requires = { trust = "trusted" }
710
+ delta = {}
711
+
712
+ [[policy.tool]]
713
+ name = "host/claude-code/Write(path:*.spec.*)"
714
+ requires = { trust = "trusted" }
715
+ delta = {}
716
+
717
+ [[policy.tool]]
718
+ name = "host/claude-code/Write(path:*_test.*)"
719
+ requires = { trust = "trusted" }
720
+ delta = {}
721
+
722
+ [[policy.tool]]
723
+ name = "host/claude-code/Write(path:*_spec.*)"
724
+ requires = { trust = "trusted" }
725
+ delta = {}
726
+
727
+ [[policy.tool]]
728
+ name = "host/claude-code/Write(path:*/__tests__/*)"
729
+ requires = { trust = "trusted" }
730
+ delta = {}
731
+
732
+ # — files that execute later: devops / build / CI —
733
+
734
+ [[policy.tool]]
735
+ name = "host/claude-code/Write(path:*Makefile*)"
736
+ requires = { trust = "trusted" }
737
+ delta = {}
738
+
739
+ [[policy.tool]]
740
+ name = "host/claude-code/Write(path:*.mk)"
741
+ requires = { trust = "trusted" }
742
+ delta = {}
743
+
744
+ [[policy.tool]]
745
+ name = "host/claude-code/Write(path:*justfile*)"
746
+ requires = { trust = "trusted" }
747
+ delta = {}
748
+
749
+ [[policy.tool]]
750
+ name = "host/claude-code/Write(path:*Justfile*)"
751
+ requires = { trust = "trusted" }
752
+ delta = {}
753
+
754
+ [[policy.tool]]
755
+ name = "host/claude-code/Write(path:*/scripts/*)"
756
+ requires = { trust = "trusted" }
757
+ delta = {}
758
+
759
+ [[policy.tool]]
760
+ name = "host/claude-code/Write(path:scripts/*)"
761
+ requires = { trust = "trusted" }
762
+ delta = {}
763
+
764
+ [[policy.tool]]
765
+ name = "host/claude-code/Write(path:*/.github/workflows/*)"
766
+ requires = { trust = "trusted" }
767
+ delta = {}
768
+
769
+ [[policy.tool]]
770
+ name = "host/claude-code/Write(path:*.gitlab-ci*)"
771
+ requires = { trust = "trusted" }
772
+ delta = {}
773
+
774
+ [[policy.tool]]
775
+ name = "host/claude-code/Write(path:*/.circleci/*)"
776
+ requires = { trust = "trusted" }
777
+ delta = {}
778
+
779
+ [[policy.tool]]
780
+ name = "host/claude-code/Write(path:*Dockerfile*)"
781
+ requires = { trust = "trusted" }
782
+ delta = {}
783
+
784
+ [[policy.tool]]
785
+ name = "host/claude-code/Write(path:*docker-compose*)"
786
+ requires = { trust = "trusted" }
787
+ delta = {}
788
+
789
+ # — files that execute later: infrastructure —
790
+
791
+ [[policy.tool]]
792
+ name = "host/claude-code/Write(path:*.tf)"
793
+ requires = { trust = "trusted" }
794
+ delta = {}
795
+
796
+ [[policy.tool]]
797
+ name = "host/claude-code/Write(path:*.tfvars)"
798
+ requires = { trust = "trusted" }
799
+ delta = {}
800
+
801
+ [[policy.tool]]
802
+ name = "host/claude-code/Write(path:*/terraform/*)"
803
+ requires = { trust = "trusted" }
804
+ delta = {}
805
+
806
+ [[policy.tool]]
807
+ name = "host/claude-code/Write(path:*/k8s/*)"
808
+ requires = { trust = "trusted" }
809
+ delta = {}
810
+
811
+ [[policy.tool]]
812
+ name = "host/claude-code/Write(path:*/helm/*)"
813
+ requires = { trust = "trusted" }
814
+ delta = {}
815
+
816
+ [[policy.tool]]
817
+ name = "host/claude-code/Write(path:*/ansible/*)"
818
+ requires = { trust = "trusted" }
819
+ delta = {}
820
+
821
+ [[policy.tool]]
822
+ name = "host/claude-code/Write(path:*/pulumi/*)"
823
+ requires = { trust = "trusted" }
824
+ delta = {}
825
+
826
+ [[policy.tool]]
827
+ name = "host/claude-code/Write(path:*.cloudformation*)"
828
+ requires = { trust = "trusted" }
829
+ delta = {}
830
+
831
+ # — the policy guards itself, for writes too —
832
+
833
+ [[policy.tool]]
834
+ name = "host/claude-code/Write(path:*/appa/appa.toml)"
835
+ requires = { trust = "trusted" }
836
+ delta = {}
837
+
838
+ [[policy.tool]]
839
+ name = "host/claude-code/Write(path:*/appa/batteries/*)"
840
+ requires = { trust = "trusted" }
841
+ delta = {}
842
+
843
+ [[policy.tool]]
844
+ name = "host/claude-code/Write"
845
+ description = "Writes a file to the local filesystem (create path)."
846
+ delta = {}
847
+
848
+ # ─── Web sources: foreign text, and a query that leaves the machine ────────
849
+ #
850
+ # The official default's dual rule, spelled for Pi's tools: the content is
851
+ # suspicious (someone else wrote it), and the query itself is a public sink
852
+ # — so a session that has read `.env` (self-narrowed) is refused web
853
+ # research until it can prove the query shareable, which it cannot. Fresh
854
+ # sessions start public and research freely.
855
+
856
+ [[policy.tool]]
857
+ name = "host/claude-code/web_explore"
858
+ description = "Researches a web question with search and fetch passes."
859
+ requires = { audience = { contains = ["public"] } }
860
+ delta = { trust = "suspicious" }
861
+
862
+ [[policy.tool]]
863
+ name = "host/claude-code/resolve-library-id"
864
+ description = "Resolves a package name to a Context7 library ID."
865
+ requires = { audience = { contains = ["public"] } }
866
+ delta = { trust = "suspicious" }
867
+
868
+ [[policy.tool]]
869
+ name = "host/claude-code/query-docs"
870
+ description = "Queries Context7 documentation for a library."
871
+ requires = { audience = { contains = ["public"] } }
872
+ delta = { trust = "suspicious" }
873
+
874
+ # codemode composes other tools' results; each nested tool call is itself
875
+ # gated (they are events in the same trajectory), so taint enters at the
876
+ # nested source, not here. Marking codemode itself suspicious would
877
+ # over-taint every later edit after routine script use.
878
+
879
+ [[policy.tool]]
880
+ name = "host/claude-code/codemode"
881
+ description = "Runs JavaScript that calls other tools."
882
+ delta = {}
883
+
884
+ # ─── Subagent delegation ───────────────────────────────────────────────────
885
+ #
886
+ # Spawning is mediated as a plain tool call. `context_control` below makes
887
+ # the runtime treat the child's context as exactly the spawn call's prompt,
888
+ # so the child's return is checked as a branch of this trajectory — with
889
+ # `attest-schema` available to let a schema-matching structured return
890
+ # cross at this session's own trust.
891
+
892
+ [[policy.tool]]
893
+ name = "host/claude-code/subagent"
894
+ description = "Delegates work to a specialized subagent."
895
+ delta = {}
896
+
897
+ # ─── Harness bookkeeping: session-local state and memory ───────────────────
898
+ #
899
+ # LSP, focus, and session tools mutate nothing outside the session. Memory
900
+ # READS are open (your own store). Memory WRITES are fenced: a session
901
+ # carrying suspicious data may not persist it as trusted memory — poisoned
902
+ # learnings would outlive the session and be trusted by every later one.
903
+ # Cost: after heavy web research, save memories from a fresh session (or
904
+ # relax this rule if your scouts must save mid-research — it is a one-block
905
+ # delete).
906
+
907
+ [[policy.tool]]
908
+ name = "host/claude-code/lsp_diagnostics"
909
+ description = "Runs language-server diagnostics."
910
+ delta = {}
911
+
912
+ [[policy.tool]]
913
+ name = "host/claude-code/lsp_fix"
914
+ description = "Applies a language-server source fix."
915
+ delta = {}
916
+
917
+ [[policy.tool]]
918
+ name = "host/claude-code/work_focus"
919
+ description = "Shows or sets ephemeral session work metadata."
920
+ delta = {}
921
+
922
+ [[policy.tool]]
923
+ name = "host/claude-code/stage"
924
+ description = "Declares the current session stage."
925
+ delta = {}
926
+
927
+ [[policy.tool]]
928
+ name = "host/claude-code/subject"
929
+ description = "Shows or sets the session subject."
930
+ delta = {}
931
+
932
+ [[policy.tool]]
933
+ name = "host/claude-code/openspec_focus"
934
+ description = "Shows or sets OpenSpec Task focus."
935
+ delta = {}
936
+
937
+ [[policy.tool]]
938
+ name = "host/claude-code/mem_search"
939
+ description = "Searches Engram memory."
940
+ delta = {}
941
+
942
+ [[policy.tool]]
943
+ name = "host/claude-code/mem_save"
944
+ description = "Saves an Engram observation."
945
+ requires = { trust = "trusted" }
946
+ delta = {}
947
+
948
+ [[policy.tool]]
949
+ name = "host/claude-code/mem_update"
950
+ description = "Updates an Engram observation."
951
+ requires = { trust = "trusted" }
952
+ delta = {}
953
+
954
+ [[policy.tool]]
955
+ name = "host/claude-code/mem_capture_passive"
956
+ description = "Persists passive learnings to Engram."
957
+ requires = { trust = "trusted" }
958
+ delta = {}
959
+
960
+ [[policy.tool]]
961
+ name = "host/claude-code/mem_delete"
962
+ description = "Deletes an Engram observation."
963
+ delta = {}
964
+
965
+ [[policy.tool]]
966
+ name = "host/claude-code/mem_context"
967
+ description = "Loads Engram context."
968
+ delta = {}
969
+
970
+ [[policy.tool]]
971
+ name = "host/claude-code/mem_stats"
972
+ description = "Reports Engram statistics."
973
+ delta = {}
974
+
975
+ [[policy.tool]]
976
+ name = "host/claude-code/mem_timeline"
977
+ description = "Shows an Engram observation timeline."
978
+ delta = {}
979
+
980
+ [[policy.tool]]
981
+ name = "host/claude-code/mem_get_observation"
982
+ description = "Fetches one Engram observation."
983
+ delta = {}
984
+
985
+ [[policy.tool]]
986
+ name = "host/claude-code/mem_list_projects"
987
+ description = "Lists Engram projects."
988
+ delta = {}
989
+
990
+ [[policy.tool]]
991
+ name = "host/claude-code/mem_current_project"
992
+ description = "Reports the current Engram project."
993
+ delta = {}
994
+
995
+ [[policy.tool]]
996
+ name = "host/claude-code/mem_doctor"
997
+ description = "Runs Engram diagnostics."
998
+ delta = {}
999
+
1000
+ # ─── Sanitizers ────────────────────────────────────────────────────────────
1001
+ #
1002
+ # redact-secrets masks private-key blocks, well-known token shapes, the AWS
1003
+ # secret access key, secret-named assignment values, and long high-entropy
1004
+ # runs. It is offered for every withheld Bash/PowerShell result (see
1005
+ # confined_results below), so a credential-narrowed command still returns
1006
+ # useful, masked output. It runs inside the runtime.
1007
+
1008
+ [[policy.sanitizer]]
1009
+ name = "redact-secrets"
1010
+ on = ["tool_output"]
1011
+
1012
+ [policy.sanitizer.permits]
1013
+ audience = { from = ["self"], to = ["public"] }
1014
+
1015
+ # attest-schema is the reserved builtin for a structured subagent return: the
1016
+ # spawn call declares a JSON schema, and a final message matching it crosses
1017
+ # at this session's own trust instead of the rank the subagent's reads left
1018
+ # it at. Runs in the runtime itself; needs no externals entry.
1019
+
1020
+ [[policy.sanitizer]]
1021
+ name = "attest-schema"
1022
+ on = ["tool_output"]
1023
+
1024
+ [policy.sanitizer.permits]
1025
+ trust = { from = "suspicious", to = "trusted" }
1026
+
1027
+ # ─── Deployment: how subagents start, which results are held back ──────────
1028
+
1029
+ [policy.deployment]
1030
+ context_control = true
1031
+ confined_results = ["host/claude-code/Bash", "host/claude-code/PowerShell"]
1032
+
1033
+ # ─── Externals: only runtime builtins, nothing host-coupled ────────────────
1034
+ # timeout_ms and max_body_bytes are required fields of [externals].
1035
+
1036
+ [externals]
1037
+ timeout_ms = 5000
1038
+ max_body_bytes = 65536
1039
+
1040
+ [externals.sanitizers.redact-secrets]
1041
+ builtin = "redact-secrets"
1042
+
1043
+ # ─── Reporting ─────────────────────────────────────────────────────────────
1044
+
1045
+ [reporting]
1046
+ agent_yell = false
1047
+
1048
+ # ─── Undeclared tools ──────────────────────────────────────────────────────
1049
+ #
1050
+ # Deny-by-default, and under static rules the only stance: appa requires a
1051
+ # wildcard `*` entry to name an annotator and forbids static delta on it.
1052
+ # A tool this file does not name is refused before execution, with the
1053
+ # runtime's reason — declare new MCP tools by copying a block above, under
1054
+ # their verbatim Pi name.
1055
+ #
1056
+ # If you want per-call classification of unknown tools instead, register a
1057
+ # model annotator (builtin = "llm", see the OpenAPPA policy reference) and
1058
+ # bind the wildcard to it. The trade is reachability and latency in the
1059
+ # decision path — the host-coupled-annotator failure this policy exists to
1060
+ # avoid — so it stays commented out:
1061
+ #
1062
+ # [[policy.annotator]]
1063
+ # name = "classify-unknown"
1064
+ # builtin = "llm"
1065
+ #
1066
+ # [[policy.tool]]
1067
+ # name = "*"
1068
+ # annotator = "classify-unknown"