pi-mono-all 1.0.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 (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * sentinel — content-aware security guard for pi coding agents.
3
+ *
4
+ * Guards addressing cross-cutting security gaps:
5
+ *
6
+ * 1. **output-scanner** (Gap 2 — content-in-location):
7
+ * Pre-reads files before `read` tool calls and scans for secret patterns.
8
+ * Asks the user before allowing reads that contain credentials.
9
+ *
10
+ * 2. **execution-tracker** (Gap 3 — indirect execution):
11
+ * Tracks files written during the session and scans for dangerous patterns.
12
+ * When `bash` executes a file written this session, correlates the write
13
+ * with the execution and asks/denies based on flagged content.
14
+ *
15
+ * 3. **permission-gate** (Gap 4 — out-of-scope operations):
16
+ * Intercepts raw bash commands and write/edit calls that perform
17
+ * system-level operations (sudo, curl|bash, brew install, writes to
18
+ * shell configs / system directories, rm -rf on system paths).
19
+ */
20
+
21
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
22
+
23
+ import { SentinelSession } from "./session.js";
24
+ import { registerOutputScanner } from "./guards/output-scanner.js";
25
+ import { registerExecutionTracker } from "./guards/execution-tracker.js";
26
+ import { registerPermissionGate } from "./guards/permission-gate.js";
27
+
28
+ export default function (pi: ExtensionAPI): void {
29
+ const session = new SentinelSession();
30
+
31
+ pi.on("session_start", async () => {
32
+ session.reset();
33
+ });
34
+
35
+ // Gap 2: scan file content before reads
36
+ registerOutputScanner(pi, session);
37
+
38
+ // Gap 3: track writes + correlate with bash execution
39
+ registerExecutionTracker(pi, session);
40
+
41
+ // Gap 4: proactive permission gate for bash + out-of-scope writes
42
+ registerPermissionGate(pi, session);
43
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "pi-mono-sentinel",
3
+ "version": "1.10.1",
4
+ "description": "Pi extension that guards against content-based secret leaks and indirect script execution",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension"
8
+ ],
9
+ "scripts": {
10
+ "test": "npx tsx --test '__tests__/**/*.test.ts'"
11
+ },
12
+ "peerDependencies": {
13
+ "@mariozechner/pi-coding-agent": "*",
14
+ "@sinclair/typebox": "*"
15
+ },
16
+ "pi": {
17
+ "extensions": [
18
+ "./index.ts"
19
+ ]
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/emanuelcasco/pi-mono-extensions.git",
24
+ "directory": "extensions/sentinel"
25
+ }
26
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Permission-gate pattern matchers.
3
+ *
4
+ * Pure helpers (no I/O, no extension API) so they can be unit-tested in
5
+ * isolation from the pi runtime.
6
+ */
7
+
8
+ import { homedir } from "node:os";
9
+ import { isAbsolute, normalize, resolve, sep } from "node:path";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Bash risk classes
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export type BashRiskClass =
16
+ | "remote-pipe-exec"
17
+ | "privilege-escalation"
18
+ | "destructive-system-rm"
19
+ | "package-manager-install"
20
+ | "persistence"
21
+ | "shell-config-write"
22
+ | "system-binary-install";
23
+
24
+ type BashPattern = {
25
+ label: BashRiskClass;
26
+ pattern: RegExp;
27
+ };
28
+
29
+ const BASH_PATTERNS: readonly BashPattern[] = [
30
+ {
31
+ label: "remote-pipe-exec",
32
+ pattern: /(?:curl|wget)\b[^\n|]*\|\s*(?:bash|sh|zsh)\b/,
33
+ },
34
+ {
35
+ label: "privilege-escalation",
36
+ pattern: /\bsudo\b/,
37
+ },
38
+ {
39
+ // rm -rf targeting system roots or the user's home directory itself.
40
+ // Project-local rm -rf is intentionally NOT matched here.
41
+ label: "destructive-system-rm",
42
+ pattern:
43
+ /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\b[^\n;]*?(?:\s|=)(?:\/(?:usr|Library|System|opt|etc|var|bin|sbin|private)(?:\/|\b)|~(?:\/|\s|$)|\$HOME\b)/,
44
+ },
45
+ {
46
+ label: "package-manager-install",
47
+ pattern: /\bbrew\s+(?:install|upgrade|update|reinstall)\b/,
48
+ },
49
+ {
50
+ label: "persistence",
51
+ pattern: /\b(?:crontab\s+-|systemctl\s+enable|launchctl\s+(?:load|bootstrap))\b/,
52
+ },
53
+ {
54
+ label: "shell-config-write",
55
+ pattern:
56
+ /(?:>>?|tee\b[^\n|]*)\s*~?\/?\.?(?:zshrc|bashrc|bash_profile|profile|zprofile|zshenv|zlogin|inputrc)\b/,
57
+ },
58
+ {
59
+ label: "system-binary-install",
60
+ pattern:
61
+ /\b(?:cp|mv|install|ln)\b[^\n|;]*\s\/usr\/local\/(?:bin|sbin|lib)\/?/,
62
+ },
63
+ ];
64
+
65
+ /** Scan a bash command string. Returns matched risk-class labels. */
66
+ export function classifyBashCommand(command: string): BashRiskClass[] {
67
+ const matched: BashRiskClass[] = [];
68
+ for (const { label, pattern } of BASH_PATTERNS) {
69
+ if (pattern.test(command)) matched.push(label);
70
+ }
71
+ return matched;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Path classification
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export type PathCategory =
79
+ | "shell-config"
80
+ | "system-directory"
81
+ | "outside-project";
82
+
83
+ const SYSTEM_PREFIXES: readonly string[] = [
84
+ "/usr/",
85
+ "/Library/",
86
+ "/System/",
87
+ "/opt/",
88
+ "/etc/",
89
+ "/var/",
90
+ "/bin/",
91
+ "/sbin/",
92
+ "/private/",
93
+ ];
94
+
95
+ const SHELL_CONFIG_BASENAMES: readonly string[] = [
96
+ ".zshrc",
97
+ ".bashrc",
98
+ ".bash_profile",
99
+ ".profile",
100
+ ".zprofile",
101
+ ".zshenv",
102
+ ".zlogin",
103
+ ".inputrc",
104
+ ".config/fish/config.fish",
105
+ ];
106
+
107
+ /** Resolve a raw user-supplied path to an absolute, normalized form. */
108
+ export function resolveTargetPath(rawPath: string, cwd: string): string {
109
+ const home = homedir();
110
+ let expanded = rawPath;
111
+
112
+ if (expanded === "~") {
113
+ expanded = home;
114
+ } else if (expanded.startsWith("~/")) {
115
+ expanded = `${home}/${expanded.slice(2)}`;
116
+ }
117
+
118
+ const absolute = isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
119
+ return normalize(absolute);
120
+ }
121
+
122
+ /**
123
+ * Classify an absolute path against permission-gate categories.
124
+ *
125
+ * Returns the matched category (most-specific wins: shell-config first, then
126
+ * system-directory, then outside-project) or `null` for safe in-project paths.
127
+ */
128
+ export function classifyPath(
129
+ absolutePath: string,
130
+ projectRoot: string,
131
+ ): PathCategory | null {
132
+ const home = homedir();
133
+
134
+ // 1. Shell config files
135
+ for (const basename of SHELL_CONFIG_BASENAMES) {
136
+ const candidate = normalize(`${home}/${basename}`);
137
+ if (absolutePath === candidate) return "shell-config";
138
+ }
139
+
140
+ // 2. System directories
141
+ for (const prefix of SYSTEM_PREFIXES) {
142
+ if (absolutePath.startsWith(prefix)) return "system-directory";
143
+ }
144
+
145
+ // 3. Outside project root
146
+ const root = normalize(projectRoot).replace(/\/$/, "");
147
+ if (
148
+ absolutePath !== root &&
149
+ !absolutePath.startsWith(`${root}${sep}`)
150
+ ) {
151
+ return "outside-project";
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Human-readable labels
159
+ // ---------------------------------------------------------------------------
160
+
161
+ export const BASH_RISK_DESCRIPTIONS: Record<BashRiskClass, string> = {
162
+ "remote-pipe-exec": "Pipes a remote download into a shell",
163
+ "privilege-escalation": "Runs with elevated privileges (sudo)",
164
+ "destructive-system-rm": "Recursively deletes a system or home path",
165
+ "package-manager-install": "Installs/upgrades a system package",
166
+ persistence: "Installs a persistence hook (cron / launchd / systemd)",
167
+ "shell-config-write": "Modifies a user shell config file",
168
+ "system-binary-install": "Installs a binary into a system path",
169
+ };
170
+
171
+ export const PATH_CATEGORY_DESCRIPTIONS: Record<PathCategory, string> = {
172
+ "shell-config": "user shell configuration file",
173
+ "system-directory": "system directory",
174
+ "outside-project": "path outside the project root",
175
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Pure helpers for extracting file-read targets from bash commands.
3
+ * Kept separate from output-scanner.ts so they can be unit-tested
4
+ * without pulling in ExtensionAPI imports.
5
+ */
6
+
7
+ import { readdir } from "node:fs/promises";
8
+ import { basename, dirname, join, resolve } from "node:path";
9
+
10
+ /**
11
+ * Strip quoted substrings so they don't confuse the tokenizer.
12
+ */
13
+ function stripQuotes(s: string): string {
14
+ return s.replace(/"[^"]*"/g, " ").replace(/'[^']*'/g, " ");
15
+ }
16
+
17
+ /**
18
+ * Extract file paths from bash commands that read file content.
19
+ * Targets: cat, head, tail, less, more, grep, rg, sed, awk, strings,
20
+ * nl, sort, uniq, wc, diff, tac, rev, xxd, hexdump, od, base64, file,
21
+ * jq, yq, and similar file-reading utilities.
22
+ */
23
+ export function extractReadTargets(command: string): string[] {
24
+ const paths: string[] = [];
25
+
26
+ // Commands that take a script / filter / pattern first, then files:
27
+ // grep [options] pattern file...
28
+ // rg [options] pattern file...
29
+ // awk [options] 'script' file...
30
+ // sed [options] 'script' file...
31
+ // jq [options] filter file...
32
+ const scriptCommands =
33
+ /\b(?:grep|rg|egrep|fgrep|awk|sed|perl|python3?|jq|yq)\b/g;
34
+ let sm: RegExpExecArray | null;
35
+ while ((sm = scriptCommands.exec(command)) !== null) {
36
+ const tail = command.slice(sm.index + sm[0].length);
37
+ const tokens = stripQuotes(tail)
38
+ .split(/\s+/)
39
+ .filter(Boolean);
40
+ for (const token of tokens) {
41
+ // Stop at shell operators and redirects
42
+ if (/^[|;&]|^(\d?[<>]|>>|<<)/.test(token)) break;
43
+ // Skip flags
44
+ if (token.startsWith("-")) continue;
45
+ paths.push(token);
46
+ }
47
+ }
48
+
49
+ // Commands that take files directly after the command name.
50
+ const directCommands =
51
+ /\b(?:cat|head|tail|less|more|nl|tac|rev|strings|xxd|hexdump|od|base64|file|sort|uniq|wc|diff|comm|join|cut|paste|column)\b/g;
52
+ let dm: RegExpExecArray | null;
53
+ while ((dm = directCommands.exec(command)) !== null) {
54
+ const tail = command.slice(dm.index + dm[0].length);
55
+ const tokens = stripQuotes(tail)
56
+ .split(/\s+/)
57
+ .filter(Boolean);
58
+ for (const token of tokens) {
59
+ // Stop at shell operators and redirects
60
+ if (/^[|;&]|^(\d?[<>]|>>|<<)/.test(token)) break;
61
+ // Skip flags
62
+ if (token.startsWith("-")) continue;
63
+ paths.push(token);
64
+ }
65
+ }
66
+
67
+ return [...new Set(paths)];
68
+ }
69
+
70
+ /**
71
+ * Expand globs like `.env*` into concrete file paths.
72
+ * Falls back to the raw path if expansion fails or if there is no wildcard.
73
+ */
74
+ export async function expandPaths(
75
+ cwd: string,
76
+ rawTarget: string,
77
+ ): Promise<string[]> {
78
+ const absolutePath = resolve(cwd, rawTarget);
79
+ const base = basename(absolutePath);
80
+ const dir = dirname(absolutePath);
81
+
82
+ if (!base.includes("*") && !base.includes("?")) {
83
+ return [absolutePath];
84
+ }
85
+
86
+ try {
87
+ const entries = await readdir(dir);
88
+ const regex = new RegExp(
89
+ "^" +
90
+ base
91
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
92
+ .replace(/\*/g, ".*")
93
+ .replace(/\?/g, ".") +
94
+ "$",
95
+ );
96
+ const matches = entries.filter((f) => regex.test(f));
97
+ if (matches.length === 0) {
98
+ return [absolutePath];
99
+ }
100
+ return matches.map((f) => join(dir, f));
101
+ } catch {
102
+ return [absolutePath];
103
+ }
104
+ }
@@ -0,0 +1,143 @@
1
+ import type { ScanMatch, ScanResult } from "../types.js";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Known secret patterns
5
+ // ---------------------------------------------------------------------------
6
+
7
+ type SecretPattern = {
8
+ label: string;
9
+ regex: RegExp;
10
+ };
11
+
12
+ const SECRET_PATTERNS: readonly SecretPattern[] = [
13
+ { label: "AWS Access Key", regex: /AKIA[A-Z0-9]{16}/ },
14
+ {
15
+ label: "AWS Secret Key",
16
+ regex: /(?:aws_secret_access_key|secret_key)\s*[=:]\s*\S{20,}/i,
17
+ },
18
+ { label: "GitHub Token", regex: /gh[ps]_[a-zA-Z0-9]{36,}/ },
19
+ { label: "GitHub OAuth Token", regex: /gho_[a-zA-Z0-9]{36,}/ },
20
+ { label: "Anthropic Key", regex: /sk-ant-[a-zA-Z0-9_-]{20,}/ },
21
+ { label: "OpenAI Key", regex: /sk-[a-zA-Z0-9]{40,}/ },
22
+ {
23
+ label: "PEM Private Key",
24
+ regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/,
25
+ },
26
+ {
27
+ label: "Generic Secret",
28
+ regex: /(?:secret|password|token|api_key|apikey|api-key)\s*[=:]\s*['"][^'"]{8,}['"]/i,
29
+ },
30
+ { label: "Slack Token", regex: /xox[bpsa]-[a-zA-Z0-9-]{10,}/ },
31
+ {
32
+ label: "Stripe Key",
33
+ regex: /[sr]k_(?:live|test)_[a-zA-Z0-9]{20,}/,
34
+ },
35
+ {
36
+ label: "Google OAuth Secret",
37
+ regex: /GOCSPX-[a-zA-Z0-9_-]{28,}/,
38
+ },
39
+ ];
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Shannon entropy helper
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Compute Shannon entropy (bits per character) of a string. */
46
+ function shannonEntropy(s: string): number {
47
+ const freq = new Map<string, number>();
48
+ for (const ch of s) {
49
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
50
+ }
51
+ let entropy = 0;
52
+ for (const count of freq.values()) {
53
+ const p = count / s.length;
54
+ entropy -= p * Math.log2(p);
55
+ }
56
+ return entropy;
57
+ }
58
+
59
+ const ENTROPY_THRESHOLD = 4.0;
60
+ const ENTROPY_MIN_LENGTH = 16;
61
+
62
+ /**
63
+ * Match high-entropy values in `.env`-style assignments:
64
+ * KEY=value or KEY="value" or KEY='value'
65
+ */
66
+ const ENV_ASSIGNMENT = /^([A-Z_][A-Z0-9_]*)\s*=\s*['"]?([^'"#\s]+)['"]?/gm;
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Masking
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /** Mask a secret value, keeping the first 4 and last 4 characters. */
73
+ function mask(value: string): string {
74
+ if (value.length <= 10) return `${value.slice(0, 3)}****`;
75
+ return `${value.slice(0, 4)}****${value.slice(-4)}`;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Public API
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /** Maximum bytes to scan (1 MB). */
83
+ export const MAX_SCAN_BYTES = 1_048_576;
84
+
85
+ /**
86
+ * Scan text content for known secret patterns and high-entropy values.
87
+ * Returns all matches with line numbers and masked snippets.
88
+ */
89
+ export function scanForSecrets(content: string): ScanResult {
90
+ const matches: ScanMatch[] = [];
91
+ const lines = content.split("\n");
92
+
93
+ for (let i = 0; i < lines.length; i++) {
94
+ const line = lines[i];
95
+ const lineNum = i + 1;
96
+
97
+ // Check each known pattern
98
+ for (const { label, regex } of SECRET_PATTERNS) {
99
+ const match = regex.exec(line);
100
+ if (match) {
101
+ matches.push({
102
+ label,
103
+ line: lineNum,
104
+ snippet: mask(match[0]),
105
+ });
106
+ }
107
+ }
108
+
109
+ // Check high-entropy env-style assignments
110
+ let envMatch: RegExpExecArray | null;
111
+ ENV_ASSIGNMENT.lastIndex = 0;
112
+ while ((envMatch = ENV_ASSIGNMENT.exec(line)) !== null) {
113
+ const value = envMatch[2];
114
+ if (
115
+ value.length >= ENTROPY_MIN_LENGTH &&
116
+ shannonEntropy(value) >= ENTROPY_THRESHOLD
117
+ ) {
118
+ // Avoid duplicate if already matched by a known pattern
119
+ const alreadyMatched = matches.some(
120
+ (m) => m.line === lineNum,
121
+ );
122
+ if (!alreadyMatched) {
123
+ matches.push({
124
+ label: "High-Entropy Value",
125
+ line: lineNum,
126
+ snippet: `${envMatch[1]}=${mask(value)}`,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ return { hasSecrets: matches.length > 0, matches };
134
+ }
135
+
136
+ /**
137
+ * Returns true if the buffer likely contains binary data.
138
+ * Checks the first 512 bytes for null bytes.
139
+ */
140
+ export function isBinaryContent(content: string): boolean {
141
+ const sample = content.slice(0, 512);
142
+ return sample.includes("\0");
143
+ }
@@ -0,0 +1,95 @@
1
+ import type { ScanResult, WriteEntry } from "./types.js";
2
+ import {
3
+ loadReadWhitelist,
4
+ loadWhitelist,
5
+ saveReadWhitelist,
6
+ saveWhitelist,
7
+ } from "./whitelist.js";
8
+
9
+ /**
10
+ * Session-scoped state for Sentinel guards.
11
+ *
12
+ * Tracks files written during the session (Gap 3) and caches scan results
13
+ * to avoid redundant filesystem reads (Gap 2).
14
+ * Reset on every `session_start`.
15
+ *
16
+ * Also holds the persistent whitelist loaded from disk so that user
17
+ * decisions to "allow and remember" a path survive across sessions.
18
+ */
19
+ export class SentinelSession {
20
+ /** Files written during this session, keyed by absolute path. */
21
+ private writeRegistry = new Map<string, WriteEntry>();
22
+
23
+ /** Scan-result cache keyed by absolute path. Invalidated when mtime changes. */
24
+ private scanCache = new Map<string, { mtimeMs: number; result: ScanResult }>();
25
+
26
+ /** Persistent whitelist of paths the user chose to remember. */
27
+ private whitelist = loadWhitelist();
28
+
29
+ /** Persistent whitelist of read paths that are safe despite secret matches. */
30
+ private readWhitelist = loadReadWhitelist();
31
+
32
+ /** Clear all session state (called on session_start). */
33
+ reset(): void {
34
+ this.writeRegistry.clear();
35
+ this.scanCache.clear();
36
+ // whitelist is intentionally NOT cleared here so it persists across sessions
37
+ }
38
+
39
+ // -- Write registry (Gap 3) ------------------------------------------------
40
+
41
+ registerWrite(entry: WriteEntry): void {
42
+ this.writeRegistry.set(entry.path, entry);
43
+ }
44
+
45
+ getWrite(absolutePath: string): WriteEntry | undefined {
46
+ return this.writeRegistry.get(absolutePath);
47
+ }
48
+
49
+ // -- Scan cache (Gap 2) ----------------------------------------------------
50
+
51
+ getCachedScan(
52
+ absolutePath: string,
53
+ currentMtimeMs: number,
54
+ ): ScanResult | undefined {
55
+ const cached = this.scanCache.get(absolutePath);
56
+ if (!cached) return undefined;
57
+ if (cached.mtimeMs !== currentMtimeMs) {
58
+ this.scanCache.delete(absolutePath);
59
+ return undefined;
60
+ }
61
+ return cached.result;
62
+ }
63
+
64
+ cacheScan(
65
+ absolutePath: string,
66
+ mtimeMs: number,
67
+ result: ScanResult,
68
+ ): void {
69
+ this.scanCache.set(absolutePath, { mtimeMs, result });
70
+ }
71
+
72
+ invalidateScanCache(absolutePath: string): void {
73
+ this.scanCache.delete(absolutePath);
74
+ }
75
+
76
+ // -- Whitelist (permission-gate persistence) -------------------------------
77
+
78
+ isWhitelisted(absolutePath: string): boolean {
79
+ return this.whitelist.has(absolutePath);
80
+ }
81
+
82
+ addToWhitelist(absolutePath: string): void {
83
+ this.whitelist.add(absolutePath);
84
+ saveWhitelist(this.whitelist);
85
+ }
86
+
87
+ isReadWhitelisted(absolutePath: string): boolean {
88
+ return this.readWhitelist.has(absolutePath);
89
+ }
90
+
91
+ addToReadWhitelist(absolutePath: string): void {
92
+ this.readWhitelist.add(absolutePath);
93
+ saveReadWhitelist(this.readWhitelist);
94
+ }
95
+ }