@te-river/opencode-team-mode 1.5.5 → 1.5.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 (158) hide show
  1. package/README.md +732 -634
  2. package/README.zh-CN.md +655 -576
  3. package/dist/agents.d.ts +6 -1
  4. package/dist/agents.d.ts.map +1 -1
  5. package/dist/agents.js +103 -531
  6. package/dist/agents.js.map +1 -1
  7. package/dist/approval-gate.d.ts +16 -0
  8. package/dist/approval-gate.d.ts.map +1 -1
  9. package/dist/approval-gate.js +8 -1
  10. package/dist/approval-gate.js.map +1 -1
  11. package/dist/blackboard.d.ts.map +1 -1
  12. package/dist/blackboard.js +3 -2
  13. package/dist/blackboard.js.map +1 -1
  14. package/dist/commands.d.ts.map +1 -1
  15. package/dist/commands.js +7 -6
  16. package/dist/commands.js.map +1 -1
  17. package/dist/envprotect/bash-classify.d.ts +42 -0
  18. package/dist/envprotect/bash-classify.d.ts.map +1 -0
  19. package/dist/envprotect/bash-classify.js +221 -0
  20. package/dist/envprotect/bash-classify.js.map +1 -0
  21. package/dist/envprotect/gate-predicates.d.ts +57 -0
  22. package/dist/envprotect/gate-predicates.d.ts.map +1 -0
  23. package/dist/envprotect/gate-predicates.js +174 -0
  24. package/dist/envprotect/gate-predicates.js.map +1 -0
  25. package/dist/envprotect/hook.d.ts +45 -0
  26. package/dist/envprotect/hook.d.ts.map +1 -0
  27. package/dist/envprotect/hook.js +158 -0
  28. package/dist/envprotect/hook.js.map +1 -0
  29. package/dist/envprotect/path-classify.d.ts +16 -0
  30. package/dist/envprotect/path-classify.d.ts.map +1 -0
  31. package/dist/envprotect/path-classify.js +72 -0
  32. package/dist/envprotect/path-classify.js.map +1 -0
  33. package/dist/envprotect/patterns.d.ts +77 -0
  34. package/dist/envprotect/patterns.d.ts.map +1 -0
  35. package/dist/envprotect/patterns.js +182 -0
  36. package/dist/envprotect/patterns.js.map +1 -0
  37. package/dist/envprotect.d.ts +19 -217
  38. package/dist/envprotect.d.ts.map +1 -1
  39. package/dist/envprotect.js +18 -803
  40. package/dist/envprotect.js.map +1 -1
  41. package/dist/fs-safe.d.ts +16 -0
  42. package/dist/fs-safe.d.ts.map +1 -0
  43. package/dist/fs-safe.js +56 -0
  44. package/dist/fs-safe.js.map +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +22 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/prompts/lead.d.ts +7 -0
  49. package/dist/prompts/lead.d.ts.map +1 -0
  50. package/dist/prompts/lead.js +230 -0
  51. package/dist/prompts/lead.js.map +1 -0
  52. package/dist/prompts/shared.d.ts +8 -0
  53. package/dist/prompts/shared.d.ts.map +1 -0
  54. package/dist/prompts/shared.js +118 -0
  55. package/dist/prompts/shared.js.map +1 -0
  56. package/dist/prompts/specialists.d.ts +12 -0
  57. package/dist/prompts/specialists.d.ts.map +1 -0
  58. package/dist/prompts/specialists.js +218 -0
  59. package/dist/prompts/specialists.js.map +1 -0
  60. package/dist/shell-text.d.ts +14 -0
  61. package/dist/shell-text.d.ts.map +1 -0
  62. package/dist/shell-text.js +16 -0
  63. package/dist/shell-text.js.map +1 -0
  64. package/dist/tm/args-schema.d.ts +36 -0
  65. package/dist/tm/args-schema.d.ts.map +1 -0
  66. package/dist/tm/args-schema.js +191 -0
  67. package/dist/tm/args-schema.js.map +1 -0
  68. package/dist/tm/browser.d.ts +52 -0
  69. package/dist/tm/browser.d.ts.map +1 -0
  70. package/dist/tm/browser.js +361 -0
  71. package/dist/tm/browser.js.map +1 -0
  72. package/dist/tm/client-unwrap.d.ts +38 -0
  73. package/dist/tm/client-unwrap.d.ts.map +1 -0
  74. package/dist/tm/client-unwrap.js +113 -0
  75. package/dist/tm/client-unwrap.js.map +1 -0
  76. package/dist/tm/config.d.ts +46 -18
  77. package/dist/tm/config.d.ts.map +1 -1
  78. package/dist/tm/config.js +60 -17
  79. package/dist/tm/config.js.map +1 -1
  80. package/dist/tm/guard.d.ts.map +1 -1
  81. package/dist/tm/guard.js +4 -7
  82. package/dist/tm/guard.js.map +1 -1
  83. package/dist/tm/index.d.ts +15 -5
  84. package/dist/tm/index.d.ts.map +1 -1
  85. package/dist/tm/index.js +95 -19
  86. package/dist/tm/index.js.map +1 -1
  87. package/dist/tm/memory.d.ts +82 -0
  88. package/dist/tm/memory.d.ts.map +1 -0
  89. package/dist/tm/memory.js +350 -0
  90. package/dist/tm/memory.js.map +1 -0
  91. package/dist/tm/pipelines.d.ts +58 -0
  92. package/dist/tm/pipelines.d.ts.map +1 -0
  93. package/dist/tm/pipelines.js +301 -0
  94. package/dist/tm/pipelines.js.map +1 -0
  95. package/dist/tm/preview.d.ts +6 -1
  96. package/dist/tm/preview.d.ts.map +1 -1
  97. package/dist/tm/preview.js +31 -8
  98. package/dist/tm/preview.js.map +1 -1
  99. package/dist/tm/ptc/budgets.d.ts +38 -0
  100. package/dist/tm/ptc/budgets.d.ts.map +1 -0
  101. package/dist/tm/ptc/budgets.js +59 -0
  102. package/dist/tm/ptc/budgets.js.map +1 -0
  103. package/dist/tm/ptc/contract.d.ts +131 -0
  104. package/dist/tm/ptc/contract.d.ts.map +1 -0
  105. package/dist/tm/ptc/contract.js +47 -0
  106. package/dist/tm/ptc/contract.js.map +1 -0
  107. package/dist/tm/ptc/driver.d.ts +37 -0
  108. package/dist/tm/ptc/driver.d.ts.map +1 -0
  109. package/dist/tm/ptc/driver.js +181 -0
  110. package/dist/tm/ptc/driver.js.map +1 -0
  111. package/dist/tm/ptc/engines.d.ts +32 -0
  112. package/dist/tm/ptc/engines.d.ts.map +1 -0
  113. package/dist/tm/ptc/engines.js +201 -0
  114. package/dist/tm/ptc/engines.js.map +1 -0
  115. package/dist/tm/ptc/gate.d.ts +28 -0
  116. package/dist/tm/ptc/gate.d.ts.map +1 -0
  117. package/dist/tm/ptc/gate.js +127 -0
  118. package/dist/tm/ptc/gate.js.map +1 -0
  119. package/dist/tm/ptc/index.d.ts +19 -0
  120. package/dist/tm/ptc/index.d.ts.map +1 -0
  121. package/dist/tm/ptc/index.js +15 -0
  122. package/dist/tm/ptc/index.js.map +1 -0
  123. package/dist/tm/ptc/pscan.d.ts +11 -0
  124. package/dist/tm/ptc/pscan.d.ts.map +1 -0
  125. package/dist/tm/ptc/pscan.js +27 -0
  126. package/dist/tm/ptc/pscan.js.map +1 -0
  127. package/dist/tm/ptc/summary.d.ts +17 -0
  128. package/dist/tm/ptc/summary.d.ts.map +1 -0
  129. package/dist/tm/ptc/summary.js +107 -0
  130. package/dist/tm/ptc/summary.js.map +1 -0
  131. package/dist/tm/ptc/tool.d.ts +35 -0
  132. package/dist/tm/ptc/tool.d.ts.map +1 -0
  133. package/dist/tm/ptc/tool.js +135 -0
  134. package/dist/tm/ptc/tool.js.map +1 -0
  135. package/dist/tm/ptc.d.ts +45 -26
  136. package/dist/tm/ptc.d.ts.map +1 -1
  137. package/dist/tm/ptc.js +71 -45
  138. package/dist/tm/ptc.js.map +1 -1
  139. package/dist/tm/result.d.ts +34 -0
  140. package/dist/tm/result.d.ts.map +1 -0
  141. package/dist/tm/result.js +87 -0
  142. package/dist/tm/result.js.map +1 -0
  143. package/dist/tm/shell-bridge.d.ts +22 -0
  144. package/dist/tm/shell-bridge.d.ts.map +1 -0
  145. package/dist/tm/shell-bridge.js +70 -0
  146. package/dist/tm/shell-bridge.js.map +1 -0
  147. package/dist/tm/store.d.ts.map +1 -1
  148. package/dist/tm/store.js +2 -1
  149. package/dist/tm/store.js.map +1 -1
  150. package/dist/tm/tools.d.ts +13 -71
  151. package/dist/tm/tools.d.ts.map +1 -1
  152. package/dist/tm/tools.js +21 -655
  153. package/dist/tm/tools.js.map +1 -1
  154. package/dist/tm/webfetch.d.ts +94 -0
  155. package/dist/tm/webfetch.d.ts.map +1 -0
  156. package/dist/tm/webfetch.js +225 -0
  157. package/dist/tm/webfetch.js.map +1 -0
  158. package/package.json +2 -2
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * R6 — code-level environment-variable read protection for Team mode.
3
3
  *
4
+ * MODULE FACADE: the implementation lives in ./envprotect/ (patterns,
5
+ * bash-classify, path-classify, gate-predicates, hook). This file only
6
+ * re-exports the historical surface so every existing import specifier
7
+ * (`../envprotect.js`, `./envprotect.js`) and the dist path keep working.
8
+ *
4
9
  * HUMAN-approved feature: in Team mode, EVERY path the model can use to
5
10
  * read environment variables / env files is intercepted in code, before
6
11
  * the tool runs, independent of prompt compliance. This hook throws inside
@@ -8,813 +13,23 @@
8
13
  * into a failed tool call whose error text is returned to the model — so
9
14
  * the block is hard, not advisory.
10
15
  *
11
- * Surfaces covered (bash + PowerShell dialects, matched against the bash
12
- * tool's `args.command`, and against path-class args of the read / grep /
13
- * glob / list tools):
14
- * 1. explicit env commands: `env` — a dump whenever no outside command
15
- * word follows: flags/assignments only (`env -0`, `env -u HOME`,
16
- * `env FOO=bar`) or a command word that is itself `env`/`printenv`
17
- * (`env printenv`); a real launcher (`env node app.js`, `env -i node
18
- * app.js`) passes — `printenv`, bare `set` (sh context only —
19
- * PowerShell `Set-*` cmdlets are never matched), `declare -p` and
20
- * its dump synonyms `typeset -p` / `export -p` / `local -p` (flags
21
- * may be split: `declare -x -p FOO`), and the PowerShell `env:`
22
- * drive via Get-ChildItem / gci / dir / ls / Get-Item / gi /
23
- * Get-Content / gc / cat / type (flags and optional quotes allowed
24
- * between cmdlet and drive: `Get-Content 'env:HOME'`). The same
25
- * statement heads are caught behind statement noise: subshells and
26
- * groups (`(env)`, `{ env; }`), escaped commands (`\env`), the `time`
27
- * keyword prefix, command substitution (`$(printenv)`, backticks), the
28
- * program-path twins of the dump heads (bare `/usr/bin/env` = full
29
- * dump, `/usr/bin/printenv …`), and the Windows launcher head
30
- * (`cmd /c set` = cmd.exe's own caseless full dump; `/k` and quoted
31
- * inners included).
32
- * 2. `$env:` / `${env:...}` expansion (PowerShell);
33
- * 3. strict mode only: `${VAR}` and `$ALLCAPS_VAR` expansion;
34
- * 4. env-file paths: `.env`, `.env.*`, `*.env`, `.bashrc`,
35
- * `.bash_profile`, `.profile`, `.zshrc`, `.zprofile`, `.zshenv`
36
- * (checked-in template copies like `.env.example` are exempt, and
37
- * code identifiers like `process.env` are not files).
38
- *
39
- * TeamMode's own tm_* tools (tm_read / tm_grep / tm_bash) are aliased onto
40
- * read/grep/bash in `inspectToolCall`, so this interception covers them
41
- * identically — the plugin tools can never serve as an R6 bypass.
42
- *
43
- * Known limitation (accepted, documented): interpreter execution paths —
44
- * `bash -c "env"`, `node -e 'process.env'`, `python -c "import os; ..."` —
45
- * are NOT intercepted; the pattern surface stops at the command line
46
- * itself. One decode pass covers URL-encoded names (`/%2Eenv`); a double
47
- * encoding (`/%252Eenv`) and a quoted dump head behind `env`
48
- * (`env 'printenv'`) stay ahead of the matcher by design.
49
- *
50
- * Configuration (read by the PLUGIN at startup — this is program-level
51
- * configuration of the tool itself, the same category as the ttlDays
52
- * option below; it is NOT what R6 defends against, which is the MODEL
53
- * reading environment variables through tool calls at runtime):
16
+ * Configuration (plugin options + env, read at startup):
17
+ * - plugin option `envProtect` (v1.5.4, default FALSE) gates the whole
18
+ * feature: without opting in, nothing intercepts, no gate arms, no
19
+ * audit lines are written. When opted in:
54
20
  * - `TM_ENV_PROTECT` = "strict" (default) | "standard" | "off";
55
21
  * unknown values fail closed into strict.
56
22
  * - `TM_ENV_PROTECT_EXTRA_DENY` = semicolon-separated user regexes,
57
23
  * applied to every scanned string in any non-off mode.
58
24
  *
59
25
  * Audit red line (HUMAN privacy constraint): every interception is logged
60
- * via `client.app.log()` (level "warn", service "team-mode-env-protect")
61
- * recording ONLY the tool name and the pattern category. Command text,
62
- * paths, variable names and values are NEVER logged — the audit trail must
63
- * answer "what class of read was blocked", and must not become a place
64
- * where secrets accumulate.
65
- */
66
- /** Audit log service identifier (also the query key for "what was blocked"). */
67
- export const ENV_PROTECT_SERVICE = "team-mode-env-protect";
68
- /**
69
- * Fixed structured message returned to the model on every block. The
70
- * category is appended as a machine-readable suffix; tests pin both.
71
- */
72
- export const ENV_PROTECT_MESSAGE = "TeamMode R6 env protection: 环境变量读取已被拦截。需要变量值请向 HUMAN 申请。";
73
- /** Pattern categories surfaced in the block message and the audit log. */
74
- export const CATEGORY_BASH_ENV_COMMAND = "bash-env-command";
75
- export const CATEGORY_BASH_ENV_EXPANSION = "bash-env-expansion";
76
- export const CATEGORY_ENV_FILE_PATH = "env-file-path";
77
- export const CATEGORY_EXTRA_DENY = "extra-deny";
78
- /* ------------------------------------------------------------------ */
79
- /* Unified approval gate — host-native `ask` pattern sets (Layer 1) */
80
- /* ------------------------------------------------------------------ */
81
- /**
82
- * Two faces are routed to the OpenCode official confirmation dialog by
83
- * declaring them `ask` in each agent's bash permission object. The host
84
- * pops a dialog for any command matching one of these patterns and SUSPENDS
85
- * it: a human reply ("once"/"always") runs the command, a "reject" or NO
86
- * REPLY is handled by the approval timer (Layer 2, see approval-gate.ts)
87
- * which auto-REJECTS after TM_ASK_TIMEOUT_MIN. The plugin NEVER self-allows.
88
- *
89
- * These are the shapes a wildcard CAN express (head-anchored globs, matching
90
- * the probe-verified grammar: `printenv*`, `rm *`, `Get-ChildItem env:*`).
91
- * Wildcards CANNOT express (embedded `${VAR}` / `$ALLCAPS` / `$env:` inside
92
- * another command, command substitution `$(printenv)`, subshell/escaped/
93
- * `time`-prefixed heads, `VAR=… env` assignment prefixes, split `declare -xp`,
94
- * env-file paths at arbitrary positions) stay the classifier's HARD THROW —
95
- * that is the `isAskGatedEnvCommand` boundary, kept in lockstep with the
96
- * classifier so a form we stop throwing on is exactly a form the dialog shows.
97
- */
98
- /** R6 env face — the wildcard-expressible environment-variable reads. */
99
- export const R6_ENV_BASH_ASK = Object.freeze({
100
- printenv: "ask",
101
- "printenv *": "ask",
102
- env: "ask",
103
- "env *": "ask",
104
- set: "ask",
105
- "export -p": "ask",
106
- "export -p *": "ask",
107
- "declare -p": "ask",
108
- "declare -p *": "ask",
109
- "typeset -p": "ask",
110
- "typeset -p *": "ask",
111
- "local -p": "ask",
112
- "local -p *": "ask",
113
- "$env:*": "ask",
114
- "Get-ChildItem env:*": "ask",
115
- "gci env:*": "ask",
116
- "dir env:*": "ask",
117
- "ls env:*": "ask",
118
- "Get-Item env:*": "ask",
119
- "gi env:*": "ask",
120
- "Get-Content env:*": "ask",
121
- "gc env:*": "ask",
122
- "cat env:*": "ask",
123
- "type env:*": "ask",
124
- });
125
- /** R2 danger face — destructive / publishing / network / install / process /
126
- * privilege commands. Independent of TM_ENV_PROTECT (R2 is its own red
127
- * line); the built-in bash tool stays `allow` for everything else, so the
128
- * normal verification stack (npm test, tsc, git status/diff) never pops. */
129
- export const R2_DANGER_BASH_ASK = Object.freeze({
130
- // deletion
131
- rm: "ask",
132
- "rm *": "ask",
133
- rmdir: "ask",
134
- "rmdir *": "ask",
135
- rd: "ask",
136
- "rd *": "ask",
137
- del: "ask",
138
- "del *": "ask",
139
- Erase: "ask",
140
- "Erase *": "ask",
141
- "Remove-Item *": "ask",
142
- "Remove-Item": "ask",
143
- // git publish (bare forms too — `git push` / `git commit` WITHOUT args are
144
- // the shapes the arg-carrying globs cannot match)
145
- "git push *": "ask",
146
- "git push": "ask",
147
- "git commit *": "ask",
148
- "git commit": "ask",
149
- // network
150
- curl: "ask",
151
- "curl *": "ask",
152
- "wget *": "ask",
153
- "Invoke-WebRequest *": "ask",
154
- "Invoke-RestMethod *": "ask",
155
- // package management
156
- "npm install *": "ask",
157
- "npm i *": "ask",
158
- "npm publish *": "ask",
159
- "npm publish": "ask",
160
- "pip install *": "ask",
161
- "pip3 install *": "ask",
162
- "winget *": "ask",
163
- "choco *": "ask",
164
- // process / system
165
- "taskkill *": "ask",
166
- "Stop-Process *": "ask",
167
- "kill *": "ask",
168
- "shutdown *": "ask",
169
- "format *": "ask",
170
- // privilege
171
- "chmod *": "ask",
172
- "takeown *": "ask",
173
- "icacls *": "ask",
174
- });
175
- /** Ordered key lists (frozen) — single source for the matcher + the gate. */
176
- export const R6_ENV_BASH_ASK_PATTERNS = Object.freeze(Object.keys(R6_ENV_BASH_ASK));
177
- export const R2_DANGER_BASH_ASK_PATTERNS = Object.freeze(Object.keys(R2_DANGER_BASH_ASK));
178
- /**
179
- * The bash permission object injected into the four execution-role agents
180
- * (team / implementer / reviewer / tester). Default stays `allow`
181
- * (`"*": "allow"`, preserving the T2.1 matrix semantics — bash is granted,
182
- * not removed); only the listed shapes escalate to the host dialog. The R6
183
- * env face is dropped when `mode === "off"` (the R6 kill switch) AND when
184
- * `envFace` is false — index.ts passes false while the approval gate cannot
185
- * arm: the R6 hook then hard-throws every env read BEFORE its dialog could
186
- * ever satisfy it, so injecting the env asks would only create dead popups
187
- * a human can answer yet never make runnable. The R2 face is always
188
- * present (its dialog is the whole gate — no code-level throw is involved).
189
- */
190
- export function bashAskPatterns(mode, envFace = true) {
191
- const out = { "*": "allow" };
192
- if (mode !== "off" && envFace) {
193
- for (const p of R6_ENV_BASH_ASK_PATTERNS)
194
- out[p] = "ask";
195
- }
196
- for (const p of R2_DANGER_BASH_ASK_PATTERNS)
197
- out[p] = "ask";
198
- return out;
199
- }
200
- /**
201
- * Resolve the protection mode. Only exact "standard" / "off" switch those
202
- * modes on; everything else (unset, typos, wrong case) fails CLOSED into
203
- * strict — a mistyped opt-out must never disable the red line.
204
- */
205
- export function resolveEnvProtectMode(raw) {
206
- if (raw === "standard")
207
- return "standard";
208
- if (raw === "off")
209
- return "off";
210
- return "strict";
211
- }
212
- /**
213
- * Parse `TM_ENV_PROTECT_EXTRA_DENY` (semicolon-separated regex bodies).
214
- * Invalid regexes are skipped rather than crashing plugin startup; the
215
- * remaining rules still apply. Empty/absent input yields no extra rules.
216
- */
217
- export function parseExtraDeny(raw) {
218
- if (typeof raw !== "string")
219
- return [];
220
- const rules = [];
221
- for (const part of raw.split(";")) {
222
- const body = part.trim();
223
- if (!body)
224
- continue;
225
- try {
226
- rules.push(new RegExp(body));
227
- }
228
- catch {
229
- /* user regex failed to compile — skip it, keep the rest */
230
- }
231
- }
232
- return rules;
233
- }
234
- /** The block error handed back to the model (fixed text + category tag). */
235
- export function envProtectError(category) {
236
- return new Error(`${ENV_PROTECT_MESSAGE} [category=${category}]`);
237
- }
238
- // ---------- env-file path matching ----------
239
- /** Shell/rc basenames whose content is effectively an env dump. */
240
- const ENV_FILE_BASENAMES = new Set([
241
- ".bashrc",
242
- ".bash_profile",
243
- ".profile",
244
- ".zshrc",
245
- ".zprofile",
246
- ".zshenv",
247
- ]);
248
- /** Checked-in template suffixes — documentation copies, not real secrets. */
249
- const ENV_TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)$/;
250
- /**
251
- * Code identifiers that merely end in ".env" and name an object, not a
252
- * file (`process.env`, `import.meta.env`, ...). The escaped form is
253
- * listed too because a grep pattern like `process\.env` survives
254
- * tokenization with its backslash.
255
- */
256
- const ENV_IDENTIFIER = /^(?:process|deno|bun|os|import\.meta)\.env$/i;
257
- /**
258
- * True when the value points at an env file. Matches on the BASENAME with
259
- * both separators handled; URL query/fragment tails are cut first so
260
- * `https://x/.env?raw=1` matches the same basename as a plain path, and
261
- * asterisks are stripped so glob patterns like "*.env" or a recursive deep
262
- * form of ".env.local" are caught by the same matcher as real paths.
263
- * (Note: glob spellings are written without backticks here — a double-star
264
- * + slash inside JSDoc would close the comment block early.)
265
- */
266
- export function isEnvFilePath(raw) {
267
- const value = String(raw ?? "")
268
- // one percent-decode pass FIRST, so URL-encoded names like
269
- // `https://x/%2Eenv` land on the same basename as the plain path
270
- // (single pass only: `%252E` decodes to `%2E` and stays encoded)
271
- .replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
272
- .replace(/[?#].*$/, "")
273
- .replace(/\*/g, "")
274
- .trim();
275
- if (!value)
276
- return false;
277
- // A code identifier like `process.env` is not a file path, however it is
278
- // segmented. The unescaped form matters because a grep pattern spelled
279
- // `process\.env` would otherwise split at the backslash into path +
280
- // basename `.env` and slip past the identifier check below.
281
- if (ENV_IDENTIFIER.test(value) || ENV_IDENTIFIER.test(value.replace(/\\/g, ""))) {
282
- return false;
283
- }
284
- const segments = value.split(/[\\/]/);
285
- const base = (segments[segments.length - 1] || "").toLowerCase();
286
- if (!base)
287
- return false;
288
- if (base === ".env")
289
- return true;
290
- if (base.startsWith(".env.")) {
291
- // `.env.example` and friends are safe checked-in documentation
292
- if (ENV_TEMPLATE_SUFFIX.test(base))
293
- return false;
294
- return true;
295
- }
296
- if (base.endsWith(".env")) {
297
- if (ENV_IDENTIFIER.test(base) || ENV_IDENTIFIER.test(base.replace(/\\/g, ""))) {
298
- return false;
299
- }
300
- return true;
301
- }
302
- return ENV_FILE_BASENAMES.has(base);
303
- }
304
- // ---------- bash command classification ----------
305
- /**
306
- * Split a command into shell segments so compound statements like
307
- * `cd /x && printenv` are checked at every statement boundary.
308
- */
309
- function splitSegments(command) {
310
- return command.split(/&&|\|\||[;\n|&]/);
311
- }
312
- /**
313
- * First token + remainder of one segment. Statement noise is peeled off
314
- * until stable so the real statement head surfaces: subshell/group
315
- * openers and closers (`(env)`, `{ env; }`), escaped commands (`\env`),
316
- * and the `time` keyword prefix (`time (env)` needs two passes). Leading
317
- * `VAR=value` assignment prefixes are skipped so `FOO=1 env` is still
318
- * recognized as an env dump.
319
- */
320
- function commandToken(segment) {
321
- let s = segment.trim();
322
- for (;;) {
323
- for (;;) {
324
- const next = s
325
- .replace(/^[({\\]+/, "")
326
- .replace(/[)}]+$/, "")
327
- .replace(/^time\s+/, "")
328
- .trim();
329
- if (next === s)
330
- break;
331
- s = next;
332
- }
333
- const sp = s.search(/\s/);
334
- const token = sp === -1 ? s : s.slice(0, sp);
335
- const isAssignment = /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
336
- if (!isAssignment)
337
- return { token, rest: sp === -1 ? "" : s.slice(sp).trim() };
338
- s = sp === -1 ? "" : s.slice(sp).trim();
339
- if (!s)
340
- return { token: "", rest: "" };
341
- }
342
- }
343
- /**
344
- * env options whose value is a SEPARATE token (GNU env: `-u NAME`,
345
- * `-S STRING`, `-C DIR`, long spellings included) — without this table the
346
- * value token would be mistaken for the launched command word and a dump
347
- * like `env -u HOME` would pass as a launcher.
348
- */
349
- const ENV_VALUE_OPTS = new Set(["u", "s", "c", "--unset", "--split-string", "--chdir"]);
350
- /**
351
- * Classify the argument tail of an `env` invocation. True (dump) when no
352
- * outside command word remains: a bare `env`, flags/assignments only
353
- * (`env -0`, `env -i`, `env -u HOME`, `env FOO=bar`), or when the command
354
- * word is itself `env`/`printenv` (`env printenv`, `env -i env`) — the
355
- * single-token bypass of the bare-dump rule. A real launcher — the first
356
- * non-flag, non-assignment token names any other program (`env node
357
- * app.js`, `env -i node app.js`) — returns false and stays allowed.
358
- */
359
- function envRestIsDump(rest) {
360
- const tokens = rest.split(/\s+/).filter(Boolean);
361
- for (let i = 0; i < tokens.length; i++) {
362
- const token = tokens[i];
363
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token))
364
- continue;
365
- if (token.startsWith("-")) {
366
- const shortTakesValue = /^-[a-z0-9]+$/i.test(token) &&
367
- [...token.slice(1)].some((ch) => ENV_VALUE_OPTS.has(ch.toLowerCase()));
368
- if ((shortTakesValue || ENV_VALUE_OPTS.has(token.toLowerCase())) && i + 1 < tokens.length) {
369
- i++;
370
- }
371
- continue;
372
- }
373
- const cmd = token.toLowerCase();
374
- return cmd === "env" || cmd === "printenv";
375
- }
376
- return true;
377
- }
378
- /**
379
- * Statement-head env dumps. Case matters for `set`: the exact lowercase
380
- * bare `set` (no arguments) is the sh builtin that prints every variable,
381
- * while PowerShell `Set-*` cmdlets and `set -euo pipefail` style flags are
382
- * never variable dumps and must pass. `env`/`printenv` are matched
383
- * case-insensitively AND path-agnostically: the basename decides, so the
384
- * formerly double-blind path heads are covered too — a bare `/usr/bin/env`
385
- * (or `./env`) is the SAME full dump as bare `env`, and `/usr/bin/printenv`
386
- * (with or without vars) the same read as bare `printenv`; `env <command>`
387
- * keeps its launcher/dump boundary via envRestIsDump either way
388
- * (`/usr/bin/env node app.js` still passes, bare `/usr/bin/env` throws —
389
- * the dialog grammar cannot express path heads, so these stay hard).
390
- * `declare -p` prints variable definitions in reusable form, and so do its
391
- * synonyms `typeset -p` / `export -p` / `local -p` — with the flag allowed
392
- * to be split from `-p` by one other option (`declare -x -p FOO`).
393
- */
394
- function isEnvDumpStatement(token, rest) {
395
- if (!token)
396
- return false;
397
- const lower = token.toLowerCase();
398
- // program-path heads resolve by basename (`/usr/bin/env`, `C:\bin\printenv`)
399
- const base = lower.split(/[\\/]/).pop() || lower;
400
- if (base === "env")
401
- return envRestIsDump(rest);
402
- if (base === "printenv")
403
- return true;
404
- if (base === "declare" || base === "typeset" || base === "export" || base === "local") {
405
- return /^\s*(?:-[a-z]+\s+)?-[a-z]*p[a-z]*\b/.test(` ${rest}`);
406
- }
407
- if (token === "set")
408
- return rest === "";
409
- return false;
410
- }
411
- /**
412
- * `cmd /c <inner>` / `cmd.exe /k "<inner>"` (case-insensitive — cmd.exe is
413
- * the Windows shell): the inner string runs as its own cmd statement, so a
414
- * bare `set` there is the SAME full env dump the sh builtin is — and cmd's
415
- * `SET` is caseless (no PowerShell `Set-*`-cmdlet confusion is possible
416
- * inside cmd.exe). Returns the peeled inner command, or null when the
417
- * segment head is not a cmd launcher.
418
- */
419
- function cmdShellInner(token, rest) {
420
- if (!/^(?:cmd|cmd\.exe)$/i.test(token))
421
- return null;
422
- const m = /^\/[ck]\s+(.*)$/i.exec(rest.trim());
423
- if (!m)
424
- return null;
425
- let inner = m[1].trim();
426
- const q = /^(["'])([\s\S]*)\1$/.exec(inner);
427
- if (q)
428
- inner = q[2].trim();
429
- return inner || null;
430
- }
431
- /** PowerShell `env:` drive access without the `$` prefix. Optional
432
- * parameter flags and optional quotes between the cmdlet and the drive
433
- * (`Get-Content -Path env:HOME`, `Get-Content 'env:HOME'`, `ls env:`) are
434
- * part of the same read. */
435
- const PS_ENV_DRIVE = /(?:^|[\s;&|("'`])(?:get-childitem|gci|dir|ls|get-item|gi|get-content|gc|cat|type)\s+(?:-[A-Za-z]+\s+)*['"]?env:/i;
436
- /** `$env:` / `${env:...}` expansion (PowerShell), case-insensitive like
437
- * the drive itself. */
438
- const PS_ENV_EXPANSION = /\$\{?env:/i;
439
- /**
440
- * Command substitutions that run a command line of their own. Captured
441
- * content excludes parens, so there is no nesting and the recursive
442
- * classification in classifyBashCommand cannot loop.
443
- */
444
- const COMMAND_SUBSTITUTION = /\$\(([^()]*)\)|`([^`]*)`/g;
445
- /** strict-mode brace expansion, e.g. `${HOME}`. */
446
- const BRACE_EXPANSION = /\$\{[^}]*\}/;
447
- /** strict-mode bare all-caps variable, e.g. `$API_KEY` (also `$F`, `$_`).
448
- * The lookahead pins the END of the identifier so mixed-case names like
449
- * `$Path` never match on their capitalized prefix alone. */
450
- const ALLCAPS_EXPANSION = /\$[A-Z_][A-Z0-9_]*(?![A-Za-z0-9_])/;
451
- /**
452
- * Tokenize a bash command and report whether any token names an env file —
453
- * catches `cat .env`, `head -50 .env.local`, `curl https://x/.env`,
454
- * `X=.env ./run` (the token survives because `=`, `.`, `/`, `:` stay inside
455
- * tokens). Quote/glob leftovers are stripped per token.
456
- */
457
- function bashEnvFileHit(command) {
458
- const tokens = command.split(/[\s'"`|;&<>()]+/);
459
- for (const rawToken of tokens) {
460
- const token = rawToken.replace(/^[*'"`]+|[*'"`]+$/g, "");
461
- if (token && isEnvFilePath(token))
462
- return true;
463
- }
464
- return false;
465
- }
466
- /**
467
- * Classify a bash/PowerShell command string. Returns the pattern category
468
- * when the command must be blocked, null when it may pass. `off` is
469
- * handled by the caller; user extra-deny rules take precedence.
470
- */
471
- export function classifyBashCommand(command, mode, extra = []) {
472
- const text = String(command ?? "");
473
- if (!text)
474
- return null;
475
- for (const rule of extra) {
476
- if (rule.test(text))
477
- return CATEGORY_EXTRA_DENY;
478
- }
479
- for (const segment of splitSegments(text)) {
480
- const { token, rest } = commandToken(segment);
481
- if (isEnvDumpStatement(token, rest))
482
- return CATEGORY_BASH_ENV_COMMAND;
483
- // `cmd /c <inner>` runs a statement of its own — classify the inner
484
- // command (covers `cmd /c printenv`, `cmd /c "cat .env"`, …) and add the
485
- // cmd-case-specific dump heads the sh rules keep case-strict
486
- const cmdInner = cmdShellInner(token, rest);
487
- if (cmdInner) {
488
- const innerCategory = classifyBashCommand(cmdInner, mode, extra);
489
- if (innerCategory)
490
- return innerCategory;
491
- if (!cmdInner.includes("=") && /^set(?:\s|$)/i.test(cmdInner)) {
492
- return CATEGORY_BASH_ENV_COMMAND; // cmd.exe prints env for `set` / `set NAME`
493
- }
494
- }
495
- }
496
- // command substitution runs its own command line: classify the inner
497
- // text recursively (`$(printenv) PATH`, backtick `cat .env`)
498
- for (const match of text.matchAll(COMMAND_SUBSTITUTION)) {
499
- const inner = match[1] ?? match[2];
500
- if (!inner)
501
- continue;
502
- const innerCategory = classifyBashCommand(inner, mode, extra);
503
- if (innerCategory)
504
- return innerCategory;
505
- }
506
- if (PS_ENV_DRIVE.test(text))
507
- return CATEGORY_BASH_ENV_COMMAND;
508
- if (PS_ENV_EXPANSION.test(text))
509
- return CATEGORY_BASH_ENV_EXPANSION;
510
- if (mode === "strict") {
511
- if (BRACE_EXPANSION.test(text))
512
- return CATEGORY_BASH_ENV_EXPANSION;
513
- if (ALLCAPS_EXPANSION.test(text))
514
- return CATEGORY_BASH_ENV_EXPANSION;
515
- }
516
- if (bashEnvFileHit(text))
517
- return CATEGORY_ENV_FILE_PATH;
518
- return null;
519
- }
520
- // ---------- approval-gate deferral predicates ----------
521
- /** Compile `*` into `.*`, escape every other regex metacharacter, anchor. */
522
- function globToRegex(pattern, flags) {
523
- let out = "";
524
- for (const ch of pattern) {
525
- if (ch === "*")
526
- out += ".*";
527
- else if (".+?^${}()|[]\\".includes(ch))
528
- out += "\\" + ch;
529
- else
530
- out += ch;
531
- }
532
- return new RegExp("^" + out + "$", flags);
533
- }
534
- /**
535
- * Two spellings of the same glob grammar:
536
- * - LOOSE (case-insensitive, trimmed) — dialog IDENTIFICATION only
537
- * (categorizePermission): over-matching here can merely arm a timer for a
538
- * dialog the host already opened, never let a command run;
539
- * - EXACT (byte-for-byte, no trim) — the DEFERRAL side: a command is only
540
- * passed through to the dialog when it reproduces the injected config
541
- * pattern head exactly, so ANY host matcher at least as greedy as ours
542
- * must pop the dialog. Case/trim leniency on this side would defer forms
543
- * like `GET-CONTENT ENV:PATH` that a byte-exact host grammar never pops
544
- * — a SILENT env read with no popup. If the host turns out to be
545
- * case-insensitive, exact costs nothing but a few extra hard throws
546
- * (fail-closed direction).
547
- */
548
- const GLOB_CACHE = new Map();
549
- function glob(pattern, flags) {
550
- const key = flags + pattern;
551
- let re = GLOB_CACHE.get(key);
552
- if (!re) {
553
- re = globToRegex(pattern, flags);
554
- GLOB_CACHE.set(key, re);
555
- }
556
- return re;
557
- }
558
- /** True when `text` matches ANY of the given ask globs (LOOSE, host-style). */
559
- export function commandMatchesAnyAskPattern(text, patterns) {
560
- const t = String(text ?? "").trim();
561
- if (!t)
562
- return false;
563
- for (const p of patterns)
564
- if (glob(p, "i").test(t))
565
- return true;
566
- return false;
567
- }
568
- /** Byte-exact deferral-side match (see the GLOB_CACHE note above). */
569
- function commandMatchesAnyAskPatternExact(text, patterns) {
570
- if (!text)
571
- return false;
572
- for (const p of patterns)
573
- if (glob(p, "").test(text))
574
- return true;
575
- return false;
576
- }
577
- /**
578
- * True when a built-in-bash command is an environment read in a form the
579
- * host is configured to ASK about (and therefore the dialog governs it).
580
- * The hook then lets it proceed WITHOUT throwing, so a human approval is
581
- * meaningful. Kept deliberately narrower than classifyBashCommand: every
582
- * form it accepts is a head-anchored single statement that matches one of
583
- * R6_ENV_BASH_ASK BYTE-EXACTLY (so the dialog WILL fire under any host
584
- * grammar at least as greedy as ours — no case/trim leniency, see
585
- * commandMatchesAnyAskPatternExact), and every hazard that breaks that
586
- * alignment (compound statements, substitutions, `${…}`, `$ALLCAPS`,
587
- * assignment prefixes, `time`/subshell/escaped heads, user extra-deny, path
588
- * heads like `/usr/bin/env`, `cmd /c …`, and the PowerShell env-drive forms
589
- * with intervening flags or quotes that the globs miss) returns false → the
590
- * classifier keeps hard-throwing. tm_* channels are hard-blocked separately
591
- * (the dialog never opens for them).
592
- */
593
- export function isAskGatedEnvCommand(command, mode, extra = []) {
594
- const text = String(command ?? "");
595
- if (!text.trim())
596
- return false;
597
- for (const r of extra)
598
- if (r.test(text))
599
- return false;
600
- // command substitution runs its own line — the outer pattern never matches
601
- if (/\$\(|`|<\(|>\(/.test(text))
602
- return false;
603
- // ${VAR} brace expansion is wildcard-inexpressible
604
- if (/\$\{[^}]*\}/.test(text))
605
- return false;
606
- const segs = splitSegments(text).map((s) => s.trim()).filter(Boolean);
607
- if (segs.length !== 1)
608
- return false;
609
- const seg = segs[0];
610
- // byte-exact head: a VAR=value prefix, stray leading whitespace or any
611
- // case deviation from the injected pattern fails here and stays hard
612
- if (!commandMatchesAnyAskPatternExact(text, R6_ENV_BASH_ASK_PATTERNS))
613
- return false;
614
- const { token, rest } = commandToken(seg);
615
- // classic dump head: printenv / env / set / declare|typeset|export|local -p
616
- if (isEnvDumpStatement(token, rest))
617
- return true;
618
- // PowerShell env: drive read with no flags between the cmdlet and the
619
- // drive, spellings byte-identical to the config globs (see the doc above)
620
- if (/^(?:Get-ChildItem|gci|dir|ls|Get-Item|gi|Get-Content|gc|cat|type)\s+env:/.test(seg)) {
621
- return true;
622
- }
623
- // leading `$env:` drive form (matches the `$env:*` config glob)
624
- if (text.startsWith("$env:"))
625
- return true;
626
- return false;
627
- }
628
- /**
629
- * Coarse category for a host permission-event payload, used ONLY to gate the
630
- * timer, to register the deferral session, and to tag the audit (never the
631
- * pattern text itself). Returns null for anything that is not one of OUR
632
- * injected bash asks, so the gate leaves other tools' dialogs (edit,
633
- * webfetch, host-native rules) alone.
634
- *
635
- * Real 1.18.29 `permission.asked` props carry NO `type` field: the tool name
636
- * rides in `permission` and the concrete command segments in `patterns[]`
637
- * (plus `metadata.command` with the raw line). Inference order, defensive
638
- * across builds:
639
- * 1. explicit tool field (`type` || `permission`) — non-bash is a hard no;
640
- * 2. absent tool field → bash evidence = `metadata.command` present, or a
641
- * candidate that is COMMAND-SHAPED (contains whitespace: our command
642
- * globs all need an embedded space except bare keys, and bare-key
643
- * patterns like a file named `set` cannot prove bash without the
644
- * command metadata beside them).
645
- * Matching here is the LOOSE glob (case-insensitive): over-matching can only
646
- * arm a timer for a dialog the host already opened, never run a command —
647
- * the deferral side keeps the byte-exact matcher (isAskGatedEnvCommand).
648
- */
649
- export function categorizePermission(props) {
650
- if (!props)
651
- return null;
652
- const type = String(props.type ?? props.permission ?? "").trim().toLowerCase();
653
- if (type && type !== "bash")
654
- return null;
655
- const raw = props.patterns ?? props.pattern;
656
- const list = raw == null ? [] : Array.isArray(raw) ? raw.map(String) : [String(raw)];
657
- const metaCmd = typeof props.metadata?.command === "string" ? props.metadata.command : "";
658
- const title = typeof props.title === "string" ? props.title : "";
659
- const candidates = [...list];
660
- if (metaCmd)
661
- candidates.push(metaCmd);
662
- if (title)
663
- candidates.push(title);
664
- if (!candidates.length)
665
- return null;
666
- if (!type) {
667
- // no tool field at all (live 1.18.29 asked): bash evidence = the bash
668
- // tool's own metadata.command, or a command-shaped candidate (contains
669
- // whitespace — every multi-word ask glob needs one; single bare words
670
- // like a path named `set` prove nothing without metadata beside them)
671
- if (!metaCmd && !candidates.some((c) => /\s/.test(c)))
672
- return null;
673
- }
674
- const hits = (patterns) => candidates.some((p) => patterns.includes(p) || commandMatchesAnyAskPattern(p, patterns));
675
- if (hits(R6_ENV_BASH_ASK_PATTERNS))
676
- return "env";
677
- if (hits(R2_DANGER_BASH_ASK_PATTERNS))
678
- return "danger";
679
- return null;
680
- }
681
- // ---------- file-tool path classification ----------
682
- /**
683
- * Path-class argument keys scanned defensively across read / grep / glob /
684
- * list (read uses `filePath`; the others historically use `path`, and
685
- * `pattern` / `include` select files for glob / grep). Keys outside this
686
- * set (descriptions, notes) are never scanned.
687
- */
688
- const PATH_LIKE_KEYS = /^(filepath|path|file|dir|directory|pattern|include)$/i;
689
- /**
690
- * Classify the path-class arguments of a file tool. Returns the pattern
691
- * category when the call must be blocked, null when it may pass.
692
- */
693
- export function classifyPathFields(args, mode, extra = []) {
694
- if (!args || typeof args !== "object")
695
- return null;
696
- for (const [key, value] of Object.entries(args)) {
697
- if (typeof value !== "string" || !value)
698
- continue;
699
- if (!PATH_LIKE_KEYS.test(key))
700
- continue;
701
- for (const rule of extra) {
702
- if (rule.test(value))
703
- return CATEGORY_EXTRA_DENY;
704
- }
705
- if (isEnvFilePath(value))
706
- return CATEGORY_ENV_FILE_PATH;
707
- }
708
- return null;
709
- }
710
- // ---------- tool-call level routing ----------
711
- /** File tools whose path-class arguments are scanned for env files. */
712
- const PATH_SCAN_TOOLS = new Set(["read", "grep", "glob", "list"]);
713
- /**
714
- * TeamMode's own tm_* tools alias onto the built-in surface so the SAME
715
- * interception applies to them (anti-backdoor: tm_read/tm_grep/tm_bash must
716
- * never become an R6 bypass by virtue of a different tool name). The tools
717
- * ALSO re-check with the same matchers inside their own pipelines — this
718
- * hook-level alias is the outer defense layer.
719
- */
720
- const TM_TOOL_ALIASES = {
721
- tm_read: "read",
722
- tm_grep: "grep",
723
- tm_bash: "bash",
724
- };
725
- /**
726
- * Top routing: tool name + tool args -> category to block, or null to pass.
727
- * `off` always passes (the hook stays installed but is a no-op).
728
- */
729
- export function inspectToolCall(tool, args, mode, extra = []) {
730
- if (mode === "off")
731
- return null;
732
- const rawName = String(tool ?? "").trim().toLowerCase();
733
- const name = TM_TOOL_ALIASES[rawName] ?? rawName;
734
- if (name === "bash") {
735
- const command = args?.command;
736
- return classifyBashCommand(String(command ?? ""), mode, extra);
737
- }
738
- if (PATH_SCAN_TOOLS.has(name)) {
739
- return classifyPathFields(args, mode, extra);
740
- }
741
- return null;
742
- }
743
- /**
744
- * Best-effort audit of one interception. Records ONLY the tool name and
745
- * the pattern category (privacy red line: never command text, paths,
746
- * variable names or values); the message embeds the service id itself so
747
- * file-backed log sinks that drop the `service` field stay greppable. A
748
- * failing audit endpoint must never turn a block into a pass-through, so
749
- * every failure is swallowed after the call attempt.
750
- */
751
- async function auditInterception(client, tool, category) {
752
- try {
753
- // Call the method THROUGH the app object. Destructuring `app.log`
754
- // first would drop the SDK's `this` binding, so every real call died
755
- // inside the SDK and was silently swallowed here — zero audit entries.
756
- const app = client?.app;
757
- if (!app || typeof app.log !== "function")
758
- return;
759
- await app.log({
760
- body: {
761
- level: "warn",
762
- service: ENV_PROTECT_SERVICE,
763
- message: `${ENV_PROTECT_SERVICE} :: ${tool} :: ${category}`,
764
- },
765
- });
766
- }
767
- catch {
768
- /* audit is best-effort; the block below still takes effect */
769
- }
770
- }
771
- /**
772
- * Build the `tool.execute.before` hook. The hook is installed in every
773
- * mode; with mode "off" it returns immediately and everything passes.
774
- * When a block triggers, the audit entry is written first, then the fixed
775
- * structured error is thrown so opencode surfaces it to the model as the
776
- * failed tool result.
777
- *
778
- * `options.deferToApproval` (the unified approval gate) — called with the
779
- * tool call's `input.sessionID`; when it reports `true`, a built-in-bash
780
- * environment read in an EXACT ask-glob form is passed through WITHOUT
781
- * throwing, because the official confirmation dialog is the live gate for
782
- * it (approve → runs, reject or timeout → the host blocks it). The signal
783
- * is SESSION-SCOPED on purpose: the gate registers only sessions carrying
784
- * our injected bash ask set (exec-role chat.message, or an R6-env-classified
785
- * `permission.asked`), so a stock build/plan session — where no dialog would
786
- * ever open for `printenv` — keeps the hard throw instead of slipping the
787
- * read through a global arm flag. Reaching the deferral point at all
788
- * additionally requires the command to match the config globs byte-exactly,
789
- * so the dialog is GUARANTEED to fire wherever deferral happens. A failed
790
- * auto-reject flips the gate back to `false` everywhere, restoring the hard
791
- * throw (fail-closed). tm_* channels are never deferred (no dialog opens
792
- * for them).
793
- */
794
- export function createEnvProtectHook(client, mode, extra = [], options = {}) {
795
- return async (input, output) => {
796
- if (mode === "off")
797
- return;
798
- const tool = String(input?.tool ?? "");
799
- const rawSid = input?.sessionID;
800
- const sessionID = typeof rawSid === "string" && rawSid.trim() !== "" ? rawSid.trim() : undefined;
801
- const args = output?.args;
802
- const category = inspectToolCall(tool, args, mode, extra);
803
- if (!category)
804
- return;
805
- // Session-wide env approval ("always" on first env ask) — pass silently;
806
- // the "always" event itself was already audited by the approval gate.
807
- if (sessionID && options.envApproved?.(sessionID))
808
- return;
809
- // Defer ONLY the built-in bash tool (never its tm_bash alias), ONLY in a
810
- // registered session, for the exact forms the config escalates to `ask`.
811
- if (tool.trim().toLowerCase() === "bash" && sessionID && options.deferToApproval?.(sessionID)) {
812
- const command = args?.command;
813
- if (isAskGatedEnvCommand(String(command ?? ""), mode, extra))
814
- return;
815
- }
816
- await auditInterception(client, tool, category);
817
- throw envProtectError(category);
818
- };
819
- }
26
+ * via `client.app.log()` recording ONLY the tool name and the pattern
27
+ * category. Command text, paths, variable names and values are NEVER
28
+ * logged.
29
+ */
30
+ export { CATEGORY_BASH_ENV_COMMAND, CATEGORY_BASH_ENV_EXPANSION, CATEGORY_ENV_FILE_PATH, CATEGORY_EXTRA_DENY, R2_DANGER_BASH_ASK, R2_DANGER_BASH_ASK_PATTERNS, R6_ENV_BASH_ASK, R6_ENV_BASH_ASK_PATTERNS, ENV_PROTECT_SERVICE, ENV_PROTECT_MESSAGE, bashAskPatterns, envProtectError, parseExtraDeny, resolveEnvProtectMode, } from "./envprotect/patterns.js";
31
+ export { isEnvFilePath } from "./envprotect/path-classify.js";
32
+ export { classifyBashCommand } from "./envprotect/bash-classify.js";
33
+ export { categorizePermission, commandMatchesAnyAskPattern, isAskGatedEnvCommand, } from "./envprotect/gate-predicates.js";
34
+ export { classifyPathFields, createEnvProtectHook, inspectToolCall } from "./envprotect/hook.js";
820
35
  //# sourceMappingURL=envprotect.js.map