@cybedefend/vibedefend 1.5.0 → 1.6.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 (246) hide show
  1. package/README.md +67 -2
  2. package/dist/api/gateway-request.js +49 -0
  3. package/dist/api/gateway-request.js.map +1 -0
  4. package/dist/api/projects.js +67 -0
  5. package/dist/api/projects.js.map +1 -1
  6. package/dist/api/repository-link.js +157 -0
  7. package/dist/api/repository-link.js.map +1 -0
  8. package/dist/auth/auth-store.js +107 -25
  9. package/dist/auth/auth-store.js.map +1 -1
  10. package/dist/auth/auth.js +21 -0
  11. package/dist/auth/auth.js.map +1 -1
  12. package/dist/clients/cursor.js +5 -1
  13. package/dist/clients/cursor.js.map +1 -1
  14. package/dist/clients/vscode.js +38 -29
  15. package/dist/clients/vscode.js.map +1 -1
  16. package/dist/commands/mcp/allow.js +219 -0
  17. package/dist/commands/mcp/allow.js.map +1 -0
  18. package/dist/commands/mcp/connect.js +337 -0
  19. package/dist/commands/mcp/connect.js.map +1 -0
  20. package/dist/commands/mcp/index.js +170 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/mcp/proxy.js +264 -0
  23. package/dist/commands/mcp/proxy.js.map +1 -0
  24. package/dist/commands/mcp/scan.js +349 -0
  25. package/dist/commands/mcp/scan.js.map +1 -0
  26. package/dist/commands/mcp/sync.js +215 -0
  27. package/dist/commands/mcp/sync.js.map +1 -0
  28. package/dist/config.js +6 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/diagnostics.js +78 -12
  31. package/dist/diagnostics.js.map +1 -1
  32. package/dist/doctor.js +19 -0
  33. package/dist/doctor.js.map +1 -1
  34. package/dist/guards-evaluator/analysis-types.js +27 -0
  35. package/dist/guards-evaluator/analysis-types.js.map +1 -0
  36. package/dist/guards-evaluator/command-analysis.js +572 -0
  37. package/dist/guards-evaluator/command-analysis.js.map +1 -0
  38. package/dist/guards-evaluator/command-view.js +586 -0
  39. package/dist/guards-evaluator/command-view.js.map +1 -0
  40. package/dist/guards-evaluator/env.js +11 -0
  41. package/dist/guards-evaluator/env.js.map +1 -0
  42. package/dist/guards-evaluator/evaluate.js +91 -41
  43. package/dist/guards-evaluator/evaluate.js.map +1 -1
  44. package/dist/guards-evaluator/index.js +2 -1
  45. package/dist/guards-evaluator/index.js.map +1 -1
  46. package/dist/guards-evaluator/inline-code.js +259 -0
  47. package/dist/guards-evaluator/inline-code.js.map +1 -0
  48. package/dist/guards-evaluator/normalize.js +716 -0
  49. package/dist/guards-evaluator/normalize.js.map +1 -0
  50. package/dist/guards-evaluator/preprocess.js +13 -5
  51. package/dist/guards-evaluator/preprocess.js.map +1 -1
  52. package/dist/guards-evaluator/redact.js +19 -0
  53. package/dist/guards-evaluator/redact.js.map +1 -1
  54. package/dist/guards-evaluator/regex.js +25 -3
  55. package/dist/guards-evaluator/regex.js.map +1 -1
  56. package/dist/guards-evaluator/safe-commands.js +155 -0
  57. package/dist/guards-evaluator/safe-commands.js.map +1 -0
  58. package/dist/guards-evaluator/triage.js +315 -0
  59. package/dist/guards-evaluator/triage.js.map +1 -0
  60. package/dist/hook-runner.js +24679 -5079
  61. package/dist/hooks/install.js +38 -0
  62. package/dist/hooks/install.js.map +1 -1
  63. package/dist/hooks/runtime/config.js +38 -0
  64. package/dist/hooks/runtime/config.js.map +1 -1
  65. package/dist/hooks/runtime/emit.js +94 -61
  66. package/dist/hooks/runtime/emit.js.map +1 -1
  67. package/dist/hooks/runtime/fetch-rules.js +22 -7
  68. package/dist/hooks/runtime/fetch-rules.js.map +1 -1
  69. package/dist/hooks/runtime/gateway-http-error.js +34 -0
  70. package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
  71. package/dist/hooks/runtime/guard-rules-cache.js +139 -17
  72. package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
  73. package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
  74. package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
  75. package/dist/hooks/runtime/judge-cache.js +6 -0
  76. package/dist/hooks/runtime/judge-cache.js.map +1 -1
  77. package/dist/hooks/runtime/judge-triage.js +193 -0
  78. package/dist/hooks/runtime/judge-triage.js.map +1 -0
  79. package/dist/hooks/runtime/judge.js +7 -1
  80. package/dist/hooks/runtime/judge.js.map +1 -1
  81. package/dist/hooks/runtime/pre-compact.js +12 -6
  82. package/dist/hooks/runtime/pre-compact.js.map +1 -1
  83. package/dist/hooks/runtime/resolve.js +90 -8
  84. package/dist/hooks/runtime/resolve.js.map +1 -1
  85. package/dist/hooks/runtime/retry-policy.js +57 -0
  86. package/dist/hooks/runtime/retry-policy.js.map +1 -0
  87. package/dist/hooks/runtime/review-sessions.js +74 -0
  88. package/dist/hooks/runtime/review-sessions.js.map +1 -0
  89. package/dist/hooks/runtime/script-resolve.js +695 -0
  90. package/dist/hooks/runtime/script-resolve.js.map +1 -0
  91. package/dist/hooks/runtime/session-activation.js +5 -1
  92. package/dist/hooks/runtime/session-activation.js.map +1 -1
  93. package/dist/hooks/runtime/session-review.js +163 -45
  94. package/dist/hooks/runtime/session-review.js.map +1 -1
  95. package/dist/hooks/runtime/session-start.js +2 -1
  96. package/dist/hooks/runtime/session-start.js.map +1 -1
  97. package/dist/hooks/runtime/session-writes.js +232 -0
  98. package/dist/hooks/runtime/session-writes.js.map +1 -0
  99. package/dist/hooks/runtime/sniff.js +26 -7
  100. package/dist/hooks/runtime/sniff.js.map +1 -1
  101. package/dist/hooks/runtime/target-resolve.js +336 -0
  102. package/dist/hooks/runtime/target-resolve.js.map +1 -0
  103. package/dist/hooks/runtime/unmapped-tools.js +93 -0
  104. package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
  105. package/dist/hooks/runtime/usage-block.js +4 -0
  106. package/dist/hooks/runtime/usage-block.js.map +1 -1
  107. package/dist/hooks/runtime/user-prompt-submit.js +2 -1
  108. package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
  109. package/dist/hooks/runtime/wire-text.js +41 -0
  110. package/dist/hooks/runtime/wire-text.js.map +1 -0
  111. package/dist/hooks/runtime/work-dir.js +84 -0
  112. package/dist/hooks/runtime/work-dir.js.map +1 -0
  113. package/dist/index.js +45 -12
  114. package/dist/index.js.map +1 -1
  115. package/dist/install.js +174 -11
  116. package/dist/install.js.map +1 -1
  117. package/dist/link.js +74 -48
  118. package/dist/link.js.map +1 -1
  119. package/dist/login.js +77 -11
  120. package/dist/login.js.map +1 -1
  121. package/dist/mcp/auth/agent-sign-ins.js +174 -0
  122. package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
  123. package/dist/mcp/auth/callback.js +200 -0
  124. package/dist/mcp/auth/callback.js.map +1 -0
  125. package/dist/mcp/auth/connect.js +392 -0
  126. package/dist/mcp/auth/connect.js.map +1 -0
  127. package/dist/mcp/auth/discovery.js +399 -0
  128. package/dist/mcp/auth/discovery.js.map +1 -0
  129. package/dist/mcp/auth/oauth.js +326 -0
  130. package/dist/mcp/auth/oauth.js.map +1 -0
  131. package/dist/mcp/auth/probe.js +128 -0
  132. package/dist/mcp/auth/probe.js.map +1 -0
  133. package/dist/mcp/auth/secrets.js +446 -0
  134. package/dist/mcp/auth/secrets.js.map +1 -0
  135. package/dist/mcp/auth/store.js +260 -0
  136. package/dist/mcp/auth/store.js.map +1 -0
  137. package/dist/mcp/auth/tokenSource.js +55 -0
  138. package/dist/mcp/auth/tokenSource.js.map +1 -0
  139. package/dist/mcp/config/atomicWrite.js +98 -0
  140. package/dist/mcp/config/atomicWrite.js.map +1 -0
  141. package/dist/mcp/config/clients.js +171 -0
  142. package/dist/mcp/config/clients.js.map +1 -0
  143. package/dist/mcp/config/cybedefend-servers.js +48 -0
  144. package/dist/mcp/config/cybedefend-servers.js.map +1 -0
  145. package/dist/mcp/config/discover.js +114 -0
  146. package/dist/mcp/config/discover.js.map +1 -0
  147. package/dist/mcp/config/hook.js +123 -0
  148. package/dist/mcp/config/hook.js.map +1 -0
  149. package/dist/mcp/config/index.js +11 -0
  150. package/dist/mcp/config/index.js.map +1 -0
  151. package/dist/mcp/config/install.js +132 -0
  152. package/dist/mcp/config/install.js.map +1 -0
  153. package/dist/mcp/config/installed.js +37 -0
  154. package/dist/mcp/config/installed.js.map +1 -0
  155. package/dist/mcp/config/jsonEdit.js +119 -0
  156. package/dist/mcp/config/jsonEdit.js.map +1 -0
  157. package/dist/mcp/config/lock.js +102 -0
  158. package/dist/mcp/config/lock.js.map +1 -0
  159. package/dist/mcp/config/proxyBinary.js +211 -0
  160. package/dist/mcp/config/proxyBinary.js.map +1 -0
  161. package/dist/mcp/config/rewrite.js +388 -0
  162. package/dist/mcp/config/rewrite.js.map +1 -0
  163. package/dist/mcp/config/types.js +34 -0
  164. package/dist/mcp/config/types.js.map +1 -0
  165. package/dist/mcp/proxy/audit.js +83 -0
  166. package/dist/mcp/proxy/audit.js.map +1 -0
  167. package/dist/mcp/proxy/authInterceptor.js +119 -0
  168. package/dist/mcp/proxy/authInterceptor.js.map +1 -0
  169. package/dist/mcp/proxy/expand.js +93 -0
  170. package/dist/mcp/proxy/expand.js.map +1 -0
  171. package/dist/mcp/proxy/inject.js +397 -0
  172. package/dist/mcp/proxy/inject.js.map +1 -0
  173. package/dist/mcp/proxy/intercept.js +37 -0
  174. package/dist/mcp/proxy/intercept.js.map +1 -0
  175. package/dist/mcp/proxy/jsonrpc.js +153 -0
  176. package/dist/mcp/proxy/jsonrpc.js.map +1 -0
  177. package/dist/mcp/proxy/log.js +109 -0
  178. package/dist/mcp/proxy/log.js.map +1 -0
  179. package/dist/mcp/proxy/paths.js +53 -0
  180. package/dist/mcp/proxy/paths.js.map +1 -0
  181. package/dist/mcp/proxy/proxy.js +510 -0
  182. package/dist/mcp/proxy/proxy.js.map +1 -0
  183. package/dist/mcp/proxy/spec.js +203 -0
  184. package/dist/mcp/proxy/spec.js.map +1 -0
  185. package/dist/mcp/proxy/sse.js +77 -0
  186. package/dist/mcp/proxy/sse.js.map +1 -0
  187. package/dist/mcp/proxy/upstream.js +2 -0
  188. package/dist/mcp/proxy/upstream.js.map +1 -0
  189. package/dist/mcp/proxy/upstreamHttp.js +499 -0
  190. package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
  191. package/dist/mcp/proxy/upstreamStdio.js +188 -0
  192. package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
  193. package/dist/mcp/proxy/upstreams.js +143 -0
  194. package/dist/mcp/proxy/upstreams.js.map +1 -0
  195. package/dist/mcp/proxy/verifyInterceptor.js +559 -0
  196. package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
  197. package/dist/mcp/proxy/windowsCommand.js +99 -0
  198. package/dist/mcp/proxy/windowsCommand.js.map +1 -0
  199. package/dist/mcp/scan/args.js +228 -0
  200. package/dist/mcp/scan/args.js.map +1 -0
  201. package/dist/mcp/scan/results.js +253 -0
  202. package/dist/mcp/scan/results.js.map +1 -0
  203. package/dist/mcp/scan/types.js +40 -0
  204. package/dist/mcp/scan/types.js.map +1 -0
  205. package/dist/mcp/verify/api.js +475 -0
  206. package/dist/mcp/verify/api.js.map +1 -0
  207. package/dist/mcp/verify/backend.js +143 -0
  208. package/dist/mcp/verify/backend.js.map +1 -0
  209. package/dist/mcp/verify/cache.js +182 -0
  210. package/dist/mcp/verify/cache.js.map +1 -0
  211. package/dist/mcp/verify/contract-errors.js +198 -0
  212. package/dist/mcp/verify/contract-errors.js.map +1 -0
  213. package/dist/mcp/verify/digest.js +91 -0
  214. package/dist/mcp/verify/digest.js.map +1 -0
  215. package/dist/mcp/verify/factory.js +56 -0
  216. package/dist/mcp/verify/factory.js.map +1 -0
  217. package/dist/mcp/verify/identity.js +173 -0
  218. package/dist/mcp/verify/identity.js.map +1 -0
  219. package/dist/mcp/verify/policy-store.js +63 -0
  220. package/dist/mcp/verify/policy-store.js.map +1 -0
  221. package/dist/mcp/verify/policy.js +158 -0
  222. package/dist/mcp/verify/policy.js.map +1 -0
  223. package/dist/mcp/verify/pool.js +27 -0
  224. package/dist/mcp/verify/pool.js.map +1 -0
  225. package/dist/mcp/verify/token-refresh.js +44 -0
  226. package/dist/mcp/verify/token-refresh.js.map +1 -0
  227. package/dist/mcp/verify/token.js +67 -0
  228. package/dist/mcp/verify/token.js.map +1 -0
  229. package/dist/mcp/verify/verifier.js +1009 -0
  230. package/dist/mcp/verify/verifier.js.map +1 -0
  231. package/dist/org-gate.js +1 -1
  232. package/dist/org-gate.js.map +1 -1
  233. package/dist/org.js +146 -0
  234. package/dist/org.js.map +1 -0
  235. package/dist/project-create.js +129 -0
  236. package/dist/project-create.js.map +1 -0
  237. package/dist/prompts.js +21 -0
  238. package/dist/prompts.js.map +1 -1
  239. package/dist/re2.wasm +0 -0
  240. package/dist/status.js +61 -0
  241. package/dist/status.js.map +1 -1
  242. package/dist/uninstall.js +49 -4
  243. package/dist/uninstall.js.map +1 -1
  244. package/dist/utils.js +62 -6
  245. package/dist/utils.js.map +1 -1
  246. package/package.json +10 -3
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Level 1 — per-session write memory for the Action Guards.
3
+ *
4
+ * A file the agent WROTE earlier this session and then EXECUTES later
5
+ * (`bash cleanup.sh`, `./x`, `python x.py`, `npm run cleanup`) is a strong
6
+ * write-then-run signal: the Write hook never reads file CONTENT, so a script
7
+ * the agent authored is invisible to the regex evaluator until it runs. This
8
+ * module keeps a small on-disk memory of the paths an agent has written in a
9
+ * session so the guard can flag executing one.
10
+ *
11
+ * It CONSUMES write-target paths (e.g. the output of `shell-writes.ts` for a
12
+ * Bash envelope, or the single path of an Edit/Write/MultiEdit envelope). It
13
+ * does NOT extract them itself.
14
+ *
15
+ * Mirrors the repo's on-disk JSON tracker pattern (see `judge-cache.ts` and
16
+ * `defaultSessionTracker` in `user-prompt-submit.ts`):
17
+ * - stored under `CYBEDEFEND_BASELINE_DIR` / `~/.cybedefend`;
18
+ * - best-effort — a missing / corrupt file reads as empty, and every FS/JSON
19
+ * error is swallowed (never throws, never blocks the hot path);
20
+ * - bounded — at most `maxPaths` newest paths per session, and entries older
21
+ * than `ttlMs` are pruned on read and on write;
22
+ * - atomic-ish write (temp file + rename).
23
+ *
24
+ * Keyed by `sessionId`. No `sessionId` → `record` is a no-op and reads are
25
+ * empty / false (we can't attribute a write to a conversation we can't name).
26
+ */
27
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync, } from 'node:fs';
28
+ import { homedir } from 'node:os';
29
+ import { join, posix } from 'node:path';
30
+ /** Default cap on remembered paths per session — newest kept, oldest dropped. */
31
+ export const DEFAULT_MAX_PATHS = 500;
32
+ /** Default time-to-live for a remembered write: a few hours (one work session). */
33
+ export const DEFAULT_SESSION_WRITES_TTL_MS = 4 * 60 * 60 * 1000;
34
+ /** Basename of the on-disk store. */
35
+ export const SESSION_WRITES_FILE = 'guards-session-writes.json';
36
+ /** A lock older than this was left by a dead hook and is broken. */
37
+ const LOCK_STALE_MS = 2_000;
38
+ /** Longest a hook waits for the lock before writing without it (as before). */
39
+ const LOCK_WAIT_MS = 500;
40
+ function sleepMs(ms) {
41
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
42
+ }
43
+ /**
44
+ * Run `fn` holding an exclusive lock: a directory, since `mkdir` is atomic.
45
+ * Concurrent hooks (parallel tool calls, several sessions) each read, modify
46
+ * and rewrite the whole file, so without it the last rename erases the others.
47
+ */
48
+ function withLock(lockDir, fn) {
49
+ const deadline = Date.now() + LOCK_WAIT_MS;
50
+ let held = false;
51
+ while (!held) {
52
+ try {
53
+ mkdirSync(lockDir);
54
+ held = true;
55
+ }
56
+ catch {
57
+ try {
58
+ if (Date.now() - statSync(lockDir).mtimeMs > LOCK_STALE_MS) {
59
+ rmdirSync(lockDir);
60
+ continue;
61
+ }
62
+ }
63
+ catch {
64
+ continue; // released meanwhile: try again at once
65
+ }
66
+ if (Date.now() >= deadline)
67
+ break;
68
+ sleepMs(2);
69
+ }
70
+ }
71
+ try {
72
+ fn();
73
+ }
74
+ finally {
75
+ if (held) {
76
+ try {
77
+ rmdirSync(lockDir);
78
+ }
79
+ catch {
80
+ /* best-effort */
81
+ }
82
+ }
83
+ }
84
+ }
85
+ /** Default store directory — read at call time so env changes are honoured. */
86
+ function defaultDir() {
87
+ return process.env.CYBEDEFEND_BASELINE_DIR ?? join(homedir(), '.cybedefend');
88
+ }
89
+ /**
90
+ * Lexical normalization: resolve `.` / `..`, collapse duplicate slashes, drop a
91
+ * trailing slash. Posix semantics (agents and the guard deal in posix-style
92
+ * absolute paths on the host the hook runs on). Relative paths are normalized
93
+ * lexically but kept relative — the caller is expected to pass absolute paths.
94
+ */
95
+ function normalizePath(p) {
96
+ if (typeof p !== 'string')
97
+ return '';
98
+ const trimmed = p.trim();
99
+ if (trimmed.length === 0)
100
+ return '';
101
+ let n = posix.normalize(trimmed);
102
+ if (n.length > 1 && n.endsWith('/'))
103
+ n = n.slice(0, -1);
104
+ return n;
105
+ }
106
+ function isUsable(p) {
107
+ return typeof p === 'string' && p.trim().length > 0;
108
+ }
109
+ /** Narrow unknown JSON into a well-typed store, discarding anything malformed. */
110
+ function coerce(raw) {
111
+ if (typeof raw !== 'object' || raw === null)
112
+ return {};
113
+ const out = {};
114
+ for (const [sessionId, value] of Object.entries(raw)) {
115
+ if (typeof value !== 'object' || value === null)
116
+ continue;
117
+ const paths = value.paths;
118
+ if (!Array.isArray(paths))
119
+ continue;
120
+ const entries = [];
121
+ for (const item of paths) {
122
+ if (typeof item !== 'object' || item === null)
123
+ continue;
124
+ const path = item.path;
125
+ const at = item.at;
126
+ if (typeof path === 'string' && typeof at === 'number' && Number.isFinite(at)) {
127
+ entries.push({ path, at });
128
+ }
129
+ }
130
+ out[sessionId] = { paths: entries };
131
+ }
132
+ return out;
133
+ }
134
+ export function makeSessionWritesStore(opts = {}) {
135
+ const dir = opts.dir ?? defaultDir();
136
+ const file = join(dir, SESSION_WRITES_FILE);
137
+ const now = opts.now ?? (() => Date.now());
138
+ const maxPaths = typeof opts.maxPaths === 'number' && opts.maxPaths > 0
139
+ ? Math.floor(opts.maxPaths)
140
+ : DEFAULT_MAX_PATHS;
141
+ const ttlMs = typeof opts.ttlMs === 'number' && opts.ttlMs > 0
142
+ ? opts.ttlMs
143
+ : DEFAULT_SESSION_WRITES_TTL_MS;
144
+ function readAll() {
145
+ if (!existsSync(file))
146
+ return {};
147
+ try {
148
+ return coerce(JSON.parse(readFileSync(file, 'utf8')));
149
+ }
150
+ catch {
151
+ return {}; // missing / corrupt → treat as empty, never throw
152
+ }
153
+ }
154
+ /** Drop expired entries (and emptied sessions). Returns a fresh store. */
155
+ function pruneExpired(all, t) {
156
+ const out = {};
157
+ for (const [sessionId, entry] of Object.entries(all)) {
158
+ const live = entry.paths.filter((e) => t - e.at <= ttlMs);
159
+ if (live.length > 0)
160
+ out[sessionId] = { paths: live };
161
+ }
162
+ return out;
163
+ }
164
+ function writeAll(all) {
165
+ try {
166
+ if (!existsSync(dir))
167
+ mkdirSync(dir, { recursive: true });
168
+ const tmp = `${file}.${process.pid}.tmp`;
169
+ writeFileSync(tmp, JSON.stringify(all), 'utf8');
170
+ renameSync(tmp, file);
171
+ }
172
+ catch {
173
+ // Best-effort — never block the hook on an I/O error.
174
+ }
175
+ }
176
+ function record(sessionId, paths) {
177
+ if (!isUsable(sessionId))
178
+ return; // no session → nothing to attribute
179
+ if (!Array.isArray(paths))
180
+ return;
181
+ const usable = paths.filter(isUsable);
182
+ if (usable.length === 0)
183
+ return;
184
+ try {
185
+ if (!existsSync(dir))
186
+ mkdirSync(dir, { recursive: true });
187
+ withLock(`${file}.lock`, () => {
188
+ const t = now();
189
+ const all = pruneExpired(readAll(), t);
190
+ const entry = all[sessionId] ?? { paths: [] };
191
+ for (const given of usable) {
192
+ const norm = normalizePath(given);
193
+ // Dedupe by normalized form, keeping the freshest occurrence last
194
+ // (so slicing from the end keeps the newest).
195
+ entry.paths = entry.paths.filter((e) => normalizePath(e.path) !== norm);
196
+ entry.paths.push({ path: given, at: t });
197
+ }
198
+ if (entry.paths.length > maxPaths) {
199
+ entry.paths = entry.paths.slice(-maxPaths); // keep newest
200
+ }
201
+ all[sessionId] = entry;
202
+ writeAll(all);
203
+ });
204
+ }
205
+ catch {
206
+ // Best-effort — swallow everything.
207
+ }
208
+ }
209
+ /** Live (non-expired) entries for a session, without persisting the prune. */
210
+ function liveEntries(sessionId) {
211
+ const t = now();
212
+ const entry = readAll()[sessionId];
213
+ if (!entry)
214
+ return [];
215
+ return entry.paths.filter((e) => t - e.at <= ttlMs);
216
+ }
217
+ function wasWritten(sessionId, path) {
218
+ if (!isUsable(sessionId) || !isUsable(path))
219
+ return false;
220
+ const norm = normalizePath(path);
221
+ if (norm.length === 0)
222
+ return false;
223
+ return liveEntries(sessionId).some((e) => normalizePath(e.path) === norm);
224
+ }
225
+ function writtenThisSession(sessionId) {
226
+ if (!isUsable(sessionId))
227
+ return [];
228
+ return liveEntries(sessionId).map((e) => e.path);
229
+ }
230
+ return { record, wasWritten, writtenThisSession };
231
+ }
232
+ //# sourceMappingURL=session-writes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-writes.js","sourceRoot":"","sources":["../../../src/hooks/runtime/session-writes.ts"],"names":[],"mappings":"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"}
@@ -7,10 +7,12 @@
7
7
  *
8
8
  * Schema cheat sheet:
9
9
  * - Windsurf: `.agent_action_name` + `.tool_info.{file_path, edits}`
10
- * - VS Code: `.hookEventName` (camelCase field) + `.tool_input.…`
10
+ * - VS Code: `.hook_event_name` with PascalCase value + an ISO-8601
11
+ * `.timestamp` (both its harnesses); older builds sent a
12
+ * camelCase `.hookEventName` field
11
13
  * - Cursor: `.hook_event_name` with **camelCase** value (`preToolUse`)
12
- * - Codex: `.hook_event_name` with PascalCase value + `tool_name`
13
- * `apply_patch` carrying a unified diff in `tool_input.patch`
14
+ * - Codex: `.hook_event_name` with PascalCase value + `turn_id`, or
15
+ * `tool_name` `apply_patch` carrying a unified diff
14
16
  * - Claude Code: `.hook_event_name` with PascalCase value (default fallback)
15
17
  */
16
18
  import { homedir } from 'node:os';
@@ -104,11 +106,23 @@ export function sniff(input) {
104
106
  // commands, so tool_name alone cannot tell them apart there — Codex's
105
107
  // envelope carries a `turn_id` (its documented extension), Claude
106
108
  // Code's does not (observed on Codex 0.142 / Claude Code 2.1.267).
109
+ //
110
+ // VS Code Copilot, configured with PascalCase events (our
111
+ // `~/.copilot/hooks/cybedefend.json`), sends this same snake_case shape
112
+ // and even Claude tool names — but always with an ISO-8601
113
+ // `timestamp`, a field neither Claude Code nor Codex sends. Its
114
+ // output contract differs (see emit.ts), so it must not pass as
115
+ // Claude Code.
107
116
  const toolName = typeof input.tool_name === 'string' ? input.tool_name : '';
108
- client =
109
- toolName === 'apply_patch' || 'turn_id' in input
110
- ? 'codex'
111
- : 'claude-code';
117
+ if (toolName === 'apply_patch' || 'turn_id' in input) {
118
+ client = 'codex';
119
+ }
120
+ else if (hasIsoTimestamp(input)) {
121
+ client = 'vscode';
122
+ }
123
+ else {
124
+ client = 'claude-code';
125
+ }
112
126
  }
113
127
  return extractToolInputShape(input, client);
114
128
  }
@@ -122,6 +136,11 @@ export function sniff(input) {
122
136
  raw: input,
123
137
  };
124
138
  }
139
+ /** VS Code's snake_case payloads carry `timestamp` as an ISO-8601 string. */
140
+ function hasIsoTimestamp(input) {
141
+ return (typeof input.timestamp === 'string' &&
142
+ /^\d{4}-\d{2}-\d{2}T/.test(input.timestamp));
143
+ }
125
144
  /**
126
145
  * Shared extraction for the Claude-Code-shaped families (Cursor / Codex /
127
146
  * Claude Code / VS Code Copilot). Pulls `tool_name` + `tool_input.*`,
@@ -1 +1 @@
1
- {"version":3,"file":"sniff.js","sourceRoot":"","sources":["../../../src/hooks/runtime/sniff.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sniff.js","sourceRoot":"","sources":["../../../src/hooks/runtime/sniff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Level 0 — disk resolution of ONE raw target path into the set of REAL
3
+ * candidate paths it may resolve to.
4
+ *
5
+ * Why this exists (hardening SPEC, "Measured gaps"): the Action Guards
6
+ * evaluator matches a file path as a LITERAL STRING. That is blind to two
7
+ * trivially evasive shapes:
8
+ *
9
+ * - a shell glob — `cat .en?`, `cat config/*.json` — never literally equals
10
+ * `.env`, so the sensitive-file rule does not fire;
11
+ * - a symlink — `app.py -> /proc/self/environ`, or a link whose name is
12
+ * innocuous but whose target is `.env` — reads the secret while the rule
13
+ * only ever sees `app.py`.
14
+ *
15
+ * The PreToolUse hook runs LOCALLY on the developer machine, so disk access is
16
+ * allowed here. This module turns a raw path (as written in a tool envelope or
17
+ * a command argument) into every real path it may touch, so the integrator can
18
+ * run the file rules against each one:
19
+ *
20
+ * - a leading `~` / `~/` is expanded to `homeDir`;
21
+ * - glob metacharacters (`*`, `?`, `[…]`) are expanded against the REAL
22
+ * filesystem relative to `cwd`, BOUNDED (at most `maxMatches` results and a
23
+ * capped number of directory reads); on the cap it stops and returns what
24
+ * it found plus the original (lexically-normalized) pattern;
25
+ * - every existing result is `realpath()`-ed to follow symlinks; BOTH the
26
+ * lexical path and its realpath target are returned when they differ, so a
27
+ * symlink to `.env` yields the `.env` realpath too;
28
+ * - a path that does NOT exist still contributes its lexically-resolved form
29
+ * (absolute, cwd-joined, `~`-expanded, `.`/`..` collapsed) so rules still
30
+ * fire on a not-yet-existing target;
31
+ * - the lexically-normalized input itself is ALWAYS included (deduped), so
32
+ * the caller never loses the original.
33
+ *
34
+ * Design rules (match the rest of the hardening work):
35
+ * - NEVER THROWS. A symlink loop, a permission error, an ENOENT — all are
36
+ * swallowed and the function returns best-effort. Blindness must fail
37
+ * toward "return more candidates", never toward a silent allow.
38
+ * - BOUNDED TIME. No unbounded recursion: the directory walk is an explicit
39
+ * queue with a readdir budget, a depth cap, and a visited-realpath set that
40
+ * breaks symlink cycles.
41
+ * - INJECTABLE FS. All disk access goes through a minimal `FsLike` passed via
42
+ * `opts.fs` (defaulting to `node:fs`), so tests drive a fake in-memory FS
43
+ * and never touch the real disk.
44
+ */
45
+ import { existsSync as nodeExistsSync, readdirSync as nodeReaddirSync, realpathSync as nodeRealpathSync, statSync as nodeStatSync, lstatSync as nodeLstatSync, } from 'node:fs';
46
+ import { matchGlob } from '../../guards-evaluator/glob.js';
47
+ /** Default: bind `node:fs` into the narrow `FsLike` shape. */
48
+ const DEFAULT_FS = {
49
+ existsSync: (p) => nodeExistsSync(p),
50
+ readdirSync: (p) => nodeReaddirSync(p),
51
+ realpathSync: (p) => nodeRealpathSync(p),
52
+ statSync: (p) => nodeStatSync(p),
53
+ lstatSync: (p) => nodeLstatSync(p),
54
+ };
55
+ const DEFAULT_MAX_MATCHES = 64;
56
+ /** Max directories descended through for a `**` segment. */
57
+ const MAX_DIR_DEPTH = 16;
58
+ // ---------------------------------------------------------------------------
59
+ // Pure path helpers (no I/O)
60
+ // ---------------------------------------------------------------------------
61
+ /** Windows backslashes → forward slashes, consistent with glob.ts. */
62
+ function normSep(s) {
63
+ return s.replace(/\\/g, '/');
64
+ }
65
+ /** Expand a leading `~` / `~/` only; a `~` elsewhere is left untouched. */
66
+ function expandTilde(p, homeDir) {
67
+ if (p === '~')
68
+ return homeDir;
69
+ if (p.startsWith('~/'))
70
+ return homeDir + p.slice(1);
71
+ return p;
72
+ }
73
+ function isAbsolute(p) {
74
+ return p.startsWith('/');
75
+ }
76
+ /**
77
+ * Collapse `.`, `..`, empty and duplicate segments WITHOUT touching glob
78
+ * metacharacters (they live inside a segment, never equal `.`/`..`/'').
79
+ * Assumes `/`-separated input.
80
+ */
81
+ function collapse(p) {
82
+ const abs = isAbsolute(p);
83
+ const out = [];
84
+ for (const part of p.split('/')) {
85
+ if (part === '' || part === '.')
86
+ continue;
87
+ if (part === '..') {
88
+ if (out.length > 0 && out[out.length - 1] !== '..')
89
+ out.pop();
90
+ else if (!abs)
91
+ out.push('..');
92
+ // For an absolute path, a leading `..` is dropped (cannot go above root).
93
+ continue;
94
+ }
95
+ out.push(part);
96
+ }
97
+ const joined = out.join('/');
98
+ if (abs)
99
+ return '/' + joined;
100
+ return joined === '' ? '.' : joined;
101
+ }
102
+ /**
103
+ * Absolute, `~`-expanded, cwd-joined, `.`/`..`-collapsed form of `raw`. Glob
104
+ * metacharacters survive untouched (so it doubles as the absolute pattern for
105
+ * glob expansion).
106
+ */
107
+ function lexicalResolve(raw, cwd, homeDir) {
108
+ let p = expandTilde(normSep(raw), homeDir);
109
+ if (!isAbsolute(p)) {
110
+ const base = cwd === '' ? '/' : cwd;
111
+ p = base + '/' + p;
112
+ }
113
+ return collapse(p);
114
+ }
115
+ function joinPath(dir, name) {
116
+ return dir === '/' ? '/' + name : dir + '/' + name;
117
+ }
118
+ /**
119
+ * A pattern is a glob only for `*` / `?`, or a BALANCED `[…]` character class.
120
+ * A lone `[` with no closing `]` (and no `*`/`?`) is a literal path.
121
+ */
122
+ function hasGlobMeta(p) {
123
+ return /[*?]/.test(p) || /\[[^/\]]*\]/.test(p);
124
+ }
125
+ function dedupe(xs) {
126
+ return [...new Set(xs.filter((x) => x.length > 0))];
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Safe FS wrappers — every one swallows errors (never throw on the hot path)
130
+ // ---------------------------------------------------------------------------
131
+ function safeExists(fs, p) {
132
+ try {
133
+ return fs.existsSync(p);
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ function safeReaddir(fs, p) {
140
+ try {
141
+ const r = fs.readdirSync(p);
142
+ return Array.isArray(r) ? r : [];
143
+ }
144
+ catch {
145
+ return [];
146
+ }
147
+ }
148
+ function safeIsDir(fs, p) {
149
+ try {
150
+ return fs.statSync(p).isDirectory();
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ function safeIsSymlink(fs, p) {
157
+ try {
158
+ return fs.lstatSync(p).isSymbolicLink();
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ /** Realpath, normalized; on any failure returns the input unchanged. */
165
+ function safeRealpath(fs, p) {
166
+ try {
167
+ const r = fs.realpathSync(p);
168
+ return typeof r === 'string' && r.length > 0 ? normSep(r) : p;
169
+ }
170
+ catch {
171
+ return p;
172
+ }
173
+ }
174
+ /**
175
+ * Push `lexPath` and, when it exists (or is a symlink) and its realpath
176
+ * differs, the realpath target too.
177
+ */
178
+ function pushWithReal(out, fs, lexPath) {
179
+ out.push(lexPath);
180
+ if (safeExists(fs, lexPath) || safeIsSymlink(fs, lexPath)) {
181
+ const real = safeRealpath(fs, lexPath);
182
+ if (real !== lexPath)
183
+ out.push(real);
184
+ }
185
+ }
186
+ /**
187
+ * All directories reachable from `starts` (inclusive of `starts` themselves),
188
+ * for a `**` segment. Explicit BFS — bounded by the readdir budget, a depth
189
+ * cap, and a visited-realpath set that breaks symlink cycles.
190
+ */
191
+ function collectDescendants(fs, starts, state) {
192
+ const result = [];
193
+ const visited = new Set();
194
+ const queue = starts.map((d) => ({
195
+ dir: d,
196
+ depth: 0,
197
+ }));
198
+ while (queue.length > 0) {
199
+ if (state.readdirBudget <= 0) {
200
+ state.capped = true;
201
+ break;
202
+ }
203
+ const { dir, depth } = queue.shift();
204
+ const canonical = safeRealpath(fs, dir);
205
+ if (visited.has(canonical))
206
+ continue; // cycle / already seen
207
+ visited.add(canonical);
208
+ result.push(dir);
209
+ if (depth >= MAX_DIR_DEPTH)
210
+ continue;
211
+ state.readdirBudget--;
212
+ for (const name of safeReaddir(fs, dir)) {
213
+ const child = joinPath(dir, name);
214
+ if (safeIsDir(fs, child))
215
+ queue.push({ dir: child, depth: depth + 1 });
216
+ }
217
+ }
218
+ return result;
219
+ }
220
+ /**
221
+ * Expand an absolute `/`-separated glob pattern into existing matches, walking
222
+ * one path segment at a time. Bounded: stops at `maxMatches` matches or once
223
+ * the readdir budget is spent, flagging `capped`.
224
+ */
225
+ function expandGlob(fs, absPattern, homeDir, maxMatches) {
226
+ const matches = [];
227
+ // Budget scales with the match cap but stays bounded for tiny caps.
228
+ const state = {
229
+ readdirBudget: Math.max(256, maxMatches * 8),
230
+ capped: false,
231
+ };
232
+ const addMatch = (p) => {
233
+ if (matches.length >= maxMatches) {
234
+ state.capped = true;
235
+ return;
236
+ }
237
+ matches.push(p);
238
+ };
239
+ const segs = absPattern.split('/').filter((s) => s.length > 0);
240
+ let frontier = ['/'];
241
+ for (let s = 0; s < segs.length; s++) {
242
+ if (state.capped || state.readdirBudget <= 0) {
243
+ state.capped = true;
244
+ break;
245
+ }
246
+ const seg = segs[s];
247
+ const isLast = s === segs.length - 1;
248
+ if (seg === '**') {
249
+ const dirs = collectDescendants(fs, frontier, state);
250
+ if (isLast)
251
+ for (const d of dirs)
252
+ addMatch(d);
253
+ frontier = dirs;
254
+ continue;
255
+ }
256
+ const next = [];
257
+ const glob = hasGlobMeta(seg);
258
+ for (const dir of frontier) {
259
+ if (state.capped || state.readdirBudget <= 0) {
260
+ state.capped = true;
261
+ break;
262
+ }
263
+ if (glob) {
264
+ state.readdirBudget--;
265
+ for (const name of safeReaddir(fs, dir)) {
266
+ if (!matchGlob(seg, name, { homeDir }))
267
+ continue;
268
+ const child = joinPath(dir, name);
269
+ if (isLast) {
270
+ addMatch(child);
271
+ if (state.capped)
272
+ break;
273
+ }
274
+ else if (safeIsDir(fs, child)) {
275
+ next.push(child);
276
+ }
277
+ }
278
+ }
279
+ else {
280
+ // A literal segment: no readdir needed, just probe the one path.
281
+ const child = joinPath(dir, seg);
282
+ if (isLast) {
283
+ if (safeExists(fs, child) || safeIsSymlink(fs, child))
284
+ addMatch(child);
285
+ }
286
+ else if (safeIsDir(fs, child)) {
287
+ next.push(child);
288
+ }
289
+ }
290
+ }
291
+ frontier = next;
292
+ }
293
+ return { matches, capped: state.capped };
294
+ }
295
+ // ---------------------------------------------------------------------------
296
+ // Public entry point
297
+ // ---------------------------------------------------------------------------
298
+ /**
299
+ * Resolve ONE raw path into every real candidate path it may touch. Pure of
300
+ * exceptions; bounded in time. The first element is always the
301
+ * lexically-normalized input; it is followed by glob matches and symlink
302
+ * realpath targets, deduped in first-seen order.
303
+ */
304
+ export function resolveCandidatePaths(rawPath, opts) {
305
+ const out = [];
306
+ try {
307
+ if (typeof rawPath !== 'string' || rawPath.length === 0)
308
+ return [];
309
+ const fs = opts.fs ?? DEFAULT_FS;
310
+ const homeDir = normSep(opts.homeDir ?? '');
311
+ const cwd = normSep(opts.cwd ?? '');
312
+ const maxMatches = typeof opts.maxMatches === 'number' && opts.maxMatches > 0
313
+ ? Math.floor(opts.maxMatches)
314
+ : DEFAULT_MAX_MATCHES;
315
+ // Always include the lexically-normalized input (also the glob pattern
316
+ // itself when the raw path globs). This is the "plus the original pattern"
317
+ // the SPEC requires on a cap, and the caller's guaranteed fallback.
318
+ const lexInput = lexicalResolve(rawPath, cwd, homeDir);
319
+ out.push(lexInput);
320
+ if (hasGlobMeta(rawPath)) {
321
+ const { matches } = expandGlob(fs, lexInput, homeDir, maxMatches);
322
+ for (const m of matches)
323
+ pushWithReal(out, fs, m);
324
+ }
325
+ else {
326
+ // Non-glob: add the realpath target when the lexical path is a symlink
327
+ // (or exists and its canonical form differs).
328
+ pushWithReal(out, fs, lexInput);
329
+ }
330
+ }
331
+ catch {
332
+ // Never throw: return whatever was gathered (at least the lexical input).
333
+ }
334
+ return dedupe(out);
335
+ }
336
+ //# sourceMappingURL=target-resolve.js.map