@herbertgao/pi-extensions 2026.9.4 → 2026.9.6

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 (146) hide show
  1. package/README.md +3 -15
  2. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  3. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  4. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  6. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  10. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  11. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  14. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  15. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  16. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
  21. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  22. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  23. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  34. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  35. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  37. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  38. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  39. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  40. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  41. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  42. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  43. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  44. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  45. package/node_modules/@tifan/pi-recap/package.json +3 -2
  46. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  47. package/node_modules/pi-mcp-adapter/README.md +125 -19
  48. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  49. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  50. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  51. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  52. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  53. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  54. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  55. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  56. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  57. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  58. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  59. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  60. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  61. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  62. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  63. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  64. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  65. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  66. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  67. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  68. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  69. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  70. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  71. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  72. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  73. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  74. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  75. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  76. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  77. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  78. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  79. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  80. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  81. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  82. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  83. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  84. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  85. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  86. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  87. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  88. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  89. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  90. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  91. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  92. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  93. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  94. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  95. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  96. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  97. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  98. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  99. package/node_modules/pi-mcp-adapter/package.json +13 -3
  100. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  101. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  102. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  103. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  104. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  105. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  106. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  107. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  108. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  109. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  110. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  111. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  112. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  113. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  114. package/package.json +13 -21
  115. package/examples/pi-footer.json +0 -275
  116. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  117. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  118. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  119. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  120. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  121. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  122. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  123. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  124. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  125. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  126. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  127. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  128. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  129. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  130. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  131. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  132. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  133. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  134. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  135. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  136. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  137. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  138. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  139. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  140. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  141. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  142. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  143. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  144. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  145. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  146. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
@@ -1,429 +0,0 @@
1
- import { statSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { resolve } from "node:path";
4
- import {
5
- analyzeBash,
6
- type BashAnalysis,
7
- type BashCommandAnalysis,
8
- type EffectiveCommand,
9
- } from "./bash.ts";
10
- import { HOME } from "./constants.ts";
11
- import {
12
- isProfileOrAuthorizedKeysPath,
13
- isSafetyControlPath,
14
- resolveInputPath,
15
- resolvePathForPolicy,
16
- shellPathTokenToPath,
17
- } from "./paths.ts";
18
-
19
- function isRecursiveRmArg(arg: string): boolean {
20
- return (
21
- (arg.length > 2 && arg.startsWith("--") && "--recursive".startsWith(arg)) ||
22
- /^-[A-Za-z]*r[A-Za-z]*f?[A-Za-z]*$/i.test(arg) ||
23
- /^-[A-Za-z]*f[A-Za-z]*r[A-Za-z]*$/i.test(arg)
24
- );
25
- }
26
-
27
- export type RmInvocation = {
28
- recursive: boolean;
29
- operands: Array<{ value: string; text: string; tildeExpansion: boolean }>;
30
- };
31
-
32
- export function parseRmInvocation(command: EffectiveCommand): RmInvocation {
33
- let recursive = false;
34
- let optionsEnded = false;
35
- const operands: RmInvocation["operands"] = [];
36
-
37
- for (const [index, value] of command.args.entries()) {
38
- const text = command.argTexts[index] ?? value;
39
- const tildeExpansion = command.argTildeExpansions[index] ?? false;
40
- if (!optionsEnded && value === "--") {
41
- optionsEnded = true;
42
- continue;
43
- }
44
- if (!optionsEnded && value !== "-" && value.startsWith("-")) {
45
- if (isRecursiveRmArg(value)) recursive = true;
46
- continue;
47
- }
48
- operands.push({ value, text, tildeExpansion });
49
- }
50
-
51
- return { recursive, operands };
52
- }
53
-
54
- function isUnresolvedUserHomeToken(
55
- shellText: string,
56
- tildeExpansion: boolean,
57
- ): boolean {
58
- return tildeExpansion && shellText !== "~" && !shellText.startsWith("~/");
59
- }
60
-
61
- function isSameExistingPath(left: string, right: string): boolean {
62
- try {
63
- const leftStat = statSync(left);
64
- const rightStat = statSync(right);
65
- return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
66
- } catch {
67
- return false;
68
- }
69
- }
70
-
71
- function matchesPathRoot(path: string, root: string): boolean {
72
- if (path === root || path.startsWith(`${root}/`)) return true;
73
- const lowerPath = path.toLowerCase();
74
- const lowerRoot = root.toLowerCase();
75
- if (lowerPath !== lowerRoot && !lowerPath.startsWith(`${lowerRoot}/`)) {
76
- return false;
77
- }
78
- return isSameExistingPath(path.slice(0, root.length), root);
79
- }
80
-
81
- /**
82
- * Top-level directories whose deletion or wholesale modification is a
83
- * system-wide event. Shared between candidate validation and path
84
- * classification; do not inline copies.
85
- */
86
- const SYSTEM_ROOTS: ReadonlyArray<string> = [
87
- "/bin",
88
- "/boot",
89
- "/dev",
90
- "/etc",
91
- "/home",
92
- "/lib",
93
- "/lib64",
94
- "/Library",
95
- "/private",
96
- "/proc",
97
- "/root",
98
- "/run",
99
- "/sbin",
100
- "/sys",
101
- "/System",
102
- "/usr",
103
- "/var",
104
- ];
105
-
106
- /**
107
- * Normalize a proposed temp-root value into a comparable absolute path.
108
- * Returns undefined for values that cannot denote a subdirectory: the
109
- * empty string, `/`, and slash-only artifacts.
110
- */
111
- function normalizeRootCandidate(value: string): string | undefined {
112
- const stripped = value.replace(/\/+$/, "");
113
- // An empty stripped value must not fall through to `resolve()`, which
114
- // would silently turn `/` into the process working directory.
115
- if (!stripped) return undefined;
116
- const normalized = resolve(stripped);
117
- if (normalized === "/") return undefined;
118
- return normalized;
119
- }
120
-
121
- /**
122
- * True when a candidate temp root would weaken the deterministic deny tiers:
123
- * it aliases (exact, case-folded, or dev/inode) `HOME`, `/`, or any system
124
- * root, or it is a proper ancestor of the canonical home directory.
125
- *
126
- * Dev/inode identity covers symlinked spellings of existing directories;
127
- * string comparison alone handles candidates that do not exist yet. See
128
- * issue #31: without these guards, `TMPDIR=/` reduced every absolute path
129
- * below an empty-string prefix match, and `TMPDIR=/private` made
130
- * `/private/etc/**` disposable.
131
- */
132
- function conflictsWithProtectedRoots(candidate: string, home: string): boolean {
133
- if (candidate === "/") return true;
134
- for (const protectedRoot of ["/", home, ...SYSTEM_ROOTS]) {
135
- const canonical = resolve(protectedRoot);
136
- if (
137
- candidate === canonical ||
138
- candidate.toLowerCase() === canonical.toLowerCase()
139
- ) {
140
- return true;
141
- }
142
- if (isSameExistingPath(candidate, canonical)) return true;
143
- }
144
- const homeCanonical = resolvePathForPolicy(home) ?? resolve(home);
145
- return homeCanonical.toLowerCase().startsWith(`${candidate.toLowerCase()}/`);
146
- }
147
-
148
- let cachedTempRoots: ReadonlyArray<string> | undefined;
149
- let cachedTmpdirValue: string | undefined;
150
-
151
- /**
152
- * Validated launcher-declared temp-dir roots whose subtrees are treated as
153
- * disposable by `isRootHomeOrSystemPath`: the platform tmpdir, plus `/tmp` on
154
- * macOS where it exists independently of `os.tmpdir()`. Each root appears in
155
- * canonical and resolved spelling because callers pass symlink-resolved
156
- * policy paths (`/tmp` → `/private/tmp` on macOS) and unresolved fallbacks
157
- * alike. Values from `os.tmpdir()` are not trusted blindly; see
158
- * `conflictsWithProtectedRoots`. The roots themselves are never exempt:
159
- * deleting one is a system-wide delete.
160
- *
161
- * The memoization keys on the effective `os.tmpdir()` return value so tests
162
- * can mutate `TMPDIR`, `TMP`, or `TEMP` between calls.
163
- */
164
- export function tempRootCandidates(): ReadonlyArray<string> {
165
- const currentTmpdir = tmpdir();
166
- if (cachedTempRoots && cachedTmpdirValue === currentTmpdir) {
167
- return cachedTempRoots;
168
- }
169
- cachedTmpdirValue = currentTmpdir;
170
- cachedTempRoots = (() => {
171
- const roots = new Set<string>();
172
- const consider = (value: string) => {
173
- const candidate = normalizeRootCandidate(value);
174
- if (!candidate || conflictsWithProtectedRoots(candidate, HOME)) return;
175
- roots.add(candidate);
176
- const resolved = resolvePathForPolicy(candidate);
177
- if (resolved) roots.add(resolved);
178
- };
179
- consider(currentTmpdir);
180
- if (process.platform === "darwin") consider("/tmp");
181
- return [...roots];
182
- })();
183
- return cachedTempRoots;
184
- }
185
-
186
- /**
187
- * True for `/`, the user's home root, or a top-level system root such as
188
- * `/etc`, `/usr`, or `/var`. Excludes the home *subtree* and the subtrees of
189
- * platform temp directories (`os.tmpdir()` and `/tmp` on macOS).
190
- *
191
- * On some distros (e.g. Fedora Silverblue) HOME lives under `/var`, which is
192
- * in `systemRoots`. Without the subtree exemption, `path.startsWith("/var/")`
193
- * would treat every path under HOME as a system root and hard-deny routine
194
- * `rm -rf ~/...`. HOME itself is still matched below, so `rm -rf ~` stays
195
- * blocked. `home` is a parameter so this can be unit-tested with a synthetic
196
- * `/var/home/...` value.
197
- *
198
- * The temp exemption mirrors the home one and covers cleanup of directories
199
- * created with `mktemp`, `os.tmpdir()`, or plain `/tmp` paths. On macOS these
200
- * resolve into `/private/tmp` or `/private/var/folders`, which used to match
201
- * the `/private` system root and hard-deny every temp cleanup. Deleting a
202
- * temp root itself still returns true; `tempRoots` is injectable for tests.
203
- *
204
- * Protection order matters (issue #31): exact `/`, the exact home root, and
205
- * system roots win over every exemption. Injected `tempRoots` values are
206
- * validated per call so hostile or malformed candidates cannot weaken the
207
- * deterministic tiers.
208
- */
209
- export function isRootHomeOrSystemPath(
210
- path: string,
211
- home: string,
212
- tempRoots: ReadonlyArray<string> = tempRootCandidates(),
213
- ): boolean {
214
- if (path === "/") return true;
215
- if (path === home || isSameExistingPath(path, home)) return true;
216
- if (matchesPathRoot(path, home) && path.length > home.length) return false;
217
- for (const root of tempRoots) {
218
- const candidate = normalizeRootCandidate(root);
219
- if (!candidate || conflictsWithProtectedRoots(candidate, home)) continue;
220
- if (!matchesPathRoot(path, candidate)) continue;
221
- // Subtree: disposable. Exact match: the temp root stays protected.
222
- return path.length > candidate.length ? false : true;
223
- }
224
- return (
225
- matchesPathRoot(path, home) ||
226
- SYSTEM_ROOTS.some((root) => matchesPathRoot(path, root))
227
- );
228
- }
229
-
230
- function segmentHardDeny(
231
- segment: BashCommandAnalysis,
232
- cwd: string,
233
- ): string | undefined {
234
- for (const target of segment.redirectTargets) {
235
- const path = shellPathTokenToPath(target, cwd);
236
- if (!path) continue;
237
- const profileReason = isProfileOrAuthorizedKeysPath(path);
238
- if (profileReason) return profileReason;
239
- if (isSafetyControlPath(path, cwd)) {
240
- return "auto-mode or permission safety-control modification is hard-denied";
241
- }
242
- }
243
-
244
- for (const word of segment.words) {
245
- if (
246
- /^(NODE_TLS_REJECT_UNAUTHORIZED=0|GIT_SSL_NO_VERIFY=(1|true))$/i.test(
247
- word,
248
- )
249
- ) {
250
- return "TLS verification weakening is hard-denied";
251
- }
252
- }
253
-
254
- const command = segment.effectiveCommand;
255
- const name = command.name;
256
- if (!name) return undefined;
257
- const args = command.args;
258
- const lowerArgs = args.map((arg) => arg.toLowerCase());
259
-
260
- if (
261
- ["curl", "wget"].includes(name) &&
262
- lowerArgs.some((arg) =>
263
- ["--insecure", "-k", "--no-check-certificate"].includes(arg)
264
- )
265
- ) {
266
- return "certificate verification weakening is hard-denied";
267
- }
268
- if (
269
- ["npm", "yarn", "pnpm"].includes(name) &&
270
- lowerArgs[0] === "config" &&
271
- lowerArgs[1] === "set" &&
272
- ["strict-ssl", "cafile"].includes(lowerArgs[2] ?? "") &&
273
- ["false", "null"].includes(lowerArgs[3] ?? "")
274
- ) {
275
- return "package-manager TLS weakening is hard-denied";
276
- }
277
- if (
278
- name === "git" &&
279
- lowerArgs[0] === "config" &&
280
- lowerArgs.some(
281
- (arg) => arg === "sslverify" || arg.endsWith(".sslverify"),
282
- ) &&
283
- lowerArgs.includes("false")
284
- ) {
285
- return "git TLS verification weakening is hard-denied";
286
- }
287
- if (name === "crontab" && !lowerArgs.includes("-l")) {
288
- return "persistence or system service mutation is hard-denied";
289
- }
290
- if (
291
- name === "launchctl" &&
292
- ["load", "bootstrap", "enable"].includes(lowerArgs[0] ?? "")
293
- ) {
294
- return "persistence or system service mutation is hard-denied";
295
- }
296
- if (
297
- name === "systemctl" &&
298
- ["enable", "disable"].includes(lowerArgs[0] ?? "")
299
- ) {
300
- return "persistence or system service mutation is hard-denied";
301
- }
302
- if (name === "security" && lowerArgs[0] === "add-trusted-cert") {
303
- return "platform security weakening is hard-denied";
304
- }
305
- if (name === "spctl" && lowerArgs.includes("--master-disable")) {
306
- return "platform security weakening is hard-denied";
307
- }
308
- if (name === "csrutil" && lowerArgs[0] === "disable") {
309
- return "platform security weakening is hard-denied";
310
- }
311
-
312
- if (name === "rm") {
313
- const rm = parseRmInvocation(command);
314
- if (rm.recursive) {
315
- for (const { value: arg, text: shellText, tildeExpansion } of rm.operands) {
316
- if (isUnresolvedUserHomeToken(shellText, tildeExpansion)) {
317
- return "irreversible deletion of a user-home expansion is hard-denied";
318
- }
319
- const path = shellPathTokenToPath(arg, cwd, shellText);
320
- const policyPath = path ? (resolvePathForPolicy(path) ?? path) : undefined;
321
- const policyHome = resolvePathForPolicy(HOME) ?? HOME;
322
- if (policyPath && isRootHomeOrSystemPath(policyPath, policyHome)) {
323
- return "irreversible deletion of home/root/system paths is hard-denied";
324
- }
325
- }
326
- }
327
- }
328
-
329
- if (name === "find" && lowerArgs.includes("-delete")) {
330
- const root = shellPathTokenToPath(args[0] ?? "", cwd);
331
- const policyRoot = root ? (resolvePathForPolicy(root) ?? root) : undefined;
332
- const policyHome = resolvePathForPolicy(HOME) ?? HOME;
333
- if (
334
- policyRoot &&
335
- isRootHomeOrSystemPath(policyRoot, policyHome) &&
336
- policyRoot !== policyHome
337
- ) {
338
- return "system-wide delete is hard-denied";
339
- }
340
- }
341
-
342
- if (["chmod", "chown"].includes(name)) {
343
- for (const arg of args.filter((arg) => !arg.startsWith("-"))) {
344
- const path = shellPathTokenToPath(arg, cwd);
345
- if (
346
- path &&
347
- (path.startsWith("/etc/") ||
348
- path.startsWith("/usr/") ||
349
- path.startsWith("/bin/") ||
350
- path.startsWith("/sbin/") ||
351
- path.startsWith("/System/") ||
352
- path.startsWith(resolve(HOME, ".ssh")))
353
- ) {
354
- return "system or SSH permission mutation is hard-denied";
355
- }
356
- }
357
- }
358
-
359
- if (
360
- [
361
- "tee",
362
- "mv",
363
- "cp",
364
- "rm",
365
- "unlink",
366
- "truncate",
367
- "python",
368
- "python3",
369
- "node",
370
- "perl",
371
- "ruby",
372
- "sd",
373
- "sed",
374
- ].includes(name) &&
375
- /\.pi\/automode|\.pi\/extensions|pi-automode|auto-mode\.json/i.test(
376
- segment.raw,
377
- )
378
- ) {
379
- return "auto-mode or permission safety-control modification is hard-denied";
380
- }
381
-
382
- return undefined;
383
- }
384
-
385
- /**
386
- * Deterministic deny checks for actions too risky to delegate to the classifier.
387
- *
388
- * Bash checks use the shared unbash AST analysis. The hook passes one analysis
389
- * through every enforcement stage so nested commands are not reparsed.
390
- */
391
- export function deterministicHardDeny(
392
- toolName: string,
393
- input: Record<string, unknown>,
394
- cwd: string,
395
- bashAnalysis?: BashAnalysis,
396
- ): string | undefined {
397
- if (toolName === "write" || toolName === "edit") {
398
- const path = resolveInputPath(cwd, input.path);
399
- if (!path) return undefined;
400
- const policyPath = resolvePathForPolicy(path) ?? path;
401
- const policyCwd = resolvePathForPolicy(cwd) ?? cwd;
402
- const profileReason = isProfileOrAuthorizedKeysPath(policyPath);
403
- if (profileReason) return profileReason;
404
- if (isSafetyControlPath(policyPath, policyCwd)) {
405
- return "auto-mode or permission safety-control modification is hard-denied";
406
- }
407
- }
408
-
409
- if (toolName !== "bash") return undefined;
410
- const command = typeof input.command === "string" ? input.command : "";
411
- const analysis = bashAnalysis ?? analyzeBash(command);
412
- if (analysis.errors.length > 0) {
413
- return `Bash input could not be parsed safely: ${analysis.errors[0]?.message ?? "unknown parser error"}`;
414
- }
415
- for (const target of analysis.redirectTargets) {
416
- const path = shellPathTokenToPath(target, cwd);
417
- if (!path) continue;
418
- const profileReason = isProfileOrAuthorizedKeysPath(path);
419
- if (profileReason) return profileReason;
420
- if (isSafetyControlPath(path, cwd)) {
421
- return "auto-mode or permission safety-control modification is hard-denied";
422
- }
423
- }
424
- for (const segment of analysis.commands) {
425
- const reason = segmentHardDeny(segment, cwd);
426
- if (reason) return reason;
427
- }
428
- return undefined;
429
- }
@@ -1,173 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { appendFileSync, mkdirSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
5
- import type {
6
- ClassifierIo,
7
- ClassifierIoAttempt,
8
- ClassifierReasoningLog,
9
- ClassificationDecision,
10
- DecisionKind,
11
- } from "./types.ts";
12
-
13
- /** A final allow/block decision for a tool call. */
14
- export type DecisionLogEntry = {
15
- type: "decision";
16
- ts: string;
17
- decisionId: string;
18
- sessionId?: string;
19
- cwd: string;
20
- tool: string;
21
- summary: string;
22
- kind: DecisionKind;
23
- outcome: "allow" | "block";
24
- reason: string;
25
- classifierModel?: string;
26
- reasoning: ClassifierReasoningLog;
27
- };
28
-
29
- /** The classifier prompt, raw responses, and parsed decision for one action. */
30
- export type ClassifierLogEntry = {
31
- type: "classifier";
32
- ts: string;
33
- decisionId: string;
34
- model: string;
35
- reasoning: ClassifierIo["reasoning"];
36
- prompt: ClassifierIo["prompt"];
37
- attempts: ClassifierIoAttempt[];
38
- durationMs: number;
39
- parsed: ClassificationDecision;
40
- };
41
-
42
- /** A ccusage-compatible record for one classifier model response. */
43
- export type ClassifierUsageLogEntry = {
44
- type: "message";
45
- timestamp: string;
46
- message: {
47
- role: "assistant";
48
- model: string;
49
- usage: NonNullable<ClassifierIoAttempt["response"]>["usage"];
50
- };
51
- };
52
-
53
- export type LogEntry =
54
- | DecisionLogEntry
55
- | ClassifierLogEntry
56
- | ClassifierUsageLogEntry;
57
-
58
- export type Logger = {
59
- enabled: boolean;
60
- classifierIo: boolean;
61
- append(entry: LogEntry): void;
62
- };
63
-
64
- export type LoggerOptions = {
65
- enabled: boolean;
66
- classifierIo: boolean;
67
- sessionFile?: string;
68
- sessionDir: string;
69
- /** Effective cwd for an in-memory session. */
70
- sessionCwd?: string;
71
- sessionId: string;
72
- /** Test/embedder override. Runtime uses ~/.pi/agent/extensions/pi-automode/logs. */
73
- logRoot?: string;
74
- /** Test clock used for the UTC date partition. */
75
- now?: Date;
76
- };
77
-
78
- export const DEFAULT_AUTOMODE_LOG_ROOT = join(
79
- homedir(),
80
- ".pi/agent/extensions/pi-automode/logs",
81
- );
82
-
83
- const VALID_SESSION_ID =
84
- /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
85
-
86
- function safeLogSessionId(sessionId: string): string {
87
- if (VALID_SESSION_ID.test(sessionId)) return sessionId;
88
- const digest = createHash("sha256")
89
- .update(sessionId)
90
- .digest("hex")
91
- .slice(0, 16);
92
- return `invalid-${digest}`;
93
- }
94
-
95
- /** Short id linking a classifier entry to its decision entry in the same file. */
96
- export function newDecisionId(): string {
97
- return randomBytes(4).toString("hex");
98
- }
99
-
100
- /**
101
- * Derive the log file path from the current session: the session file's
102
- * directory with `-pi-automode` inserted before the extension. Falls back to
103
- * an absolute session directory when one is available. In-memory sessions use
104
- * an application-owned, project- and date-partitioned directory instead of a
105
- * relative path resolved against the launching process cwd.
106
- */
107
- export function resolveLogPath(
108
- sessionFile: string | undefined,
109
- sessionDir: string,
110
- sessionId: string,
111
- sessionCwd = process.cwd(),
112
- logRoot = DEFAULT_AUTOMODE_LOG_ROOT,
113
- now = new Date(),
114
- ): string {
115
- if (sessionFile) {
116
- const ext = extname(sessionFile);
117
- const stem = ext ? basename(sessionFile, ext) : basename(sessionFile);
118
- return join(dirname(sessionFile), `${stem}-pi-automode${ext}`);
119
- }
120
-
121
- const logFile = `${safeLogSessionId(sessionId)}-pi-automode.jsonl`;
122
- if (isAbsolute(sessionDir)) {
123
- return join(sessionDir, logFile);
124
- }
125
-
126
- const resolvedCwd = resolve(sessionCwd);
127
- const projectDir = `--${
128
- resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")
129
- }--`;
130
- const dateDir = now.toISOString().slice(0, 10);
131
- const resolvedLogRoot = isAbsolute(logRoot)
132
- ? logRoot
133
- : DEFAULT_AUTOMODE_LOG_ROOT;
134
- return join(
135
- resolvedLogRoot,
136
- projectDir,
137
- dateDir,
138
- logFile,
139
- );
140
- }
141
-
142
- /** Append one JSON object as a line. Failures are swallowed: logging must
143
- * never change a safety decision. */
144
- function appendJsonl(path: string, entry: unknown): void {
145
- try {
146
- mkdirSync(dirname(path), { recursive: true });
147
- appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf8");
148
- } catch {
149
- // Fail open.
150
- }
151
- }
152
-
153
- /** Build a logger bound to one session's log path. No-ops when disabled. */
154
- export function createLogger(opts: LoggerOptions): Logger {
155
- const { enabled, classifierIo } = opts;
156
- const path = resolveLogPath(
157
- opts.sessionFile,
158
- opts.sessionDir,
159
- opts.sessionId,
160
- opts.sessionCwd,
161
- opts.logRoot,
162
- opts.now,
163
- );
164
- return {
165
- enabled,
166
- classifierIo,
167
- append(entry) {
168
- if (!enabled) return;
169
- if (entry.type === "classifier" && !classifierIo) return;
170
- appendJsonl(path, entry);
171
- },
172
- };
173
- }