@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.
- package/README.md +67 -2
- package/dist/api/gateway-request.js +49 -0
- package/dist/api/gateway-request.js.map +1 -0
- package/dist/api/projects.js +67 -0
- package/dist/api/projects.js.map +1 -1
- package/dist/api/repository-link.js +157 -0
- package/dist/api/repository-link.js.map +1 -0
- package/dist/auth/auth-store.js +107 -25
- package/dist/auth/auth-store.js.map +1 -1
- package/dist/auth/auth.js +21 -0
- package/dist/auth/auth.js.map +1 -1
- package/dist/clients/cursor.js +5 -1
- package/dist/clients/cursor.js.map +1 -1
- package/dist/clients/vscode.js +38 -29
- package/dist/clients/vscode.js.map +1 -1
- package/dist/commands/mcp/allow.js +219 -0
- package/dist/commands/mcp/allow.js.map +1 -0
- package/dist/commands/mcp/connect.js +337 -0
- package/dist/commands/mcp/connect.js.map +1 -0
- package/dist/commands/mcp/index.js +170 -0
- package/dist/commands/mcp/index.js.map +1 -0
- package/dist/commands/mcp/proxy.js +264 -0
- package/dist/commands/mcp/proxy.js.map +1 -0
- package/dist/commands/mcp/scan.js +349 -0
- package/dist/commands/mcp/scan.js.map +1 -0
- package/dist/commands/mcp/sync.js +215 -0
- package/dist/commands/mcp/sync.js.map +1 -0
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/diagnostics.js +78 -12
- package/dist/diagnostics.js.map +1 -1
- package/dist/doctor.js +19 -0
- package/dist/doctor.js.map +1 -1
- package/dist/guards-evaluator/analysis-types.js +27 -0
- package/dist/guards-evaluator/analysis-types.js.map +1 -0
- package/dist/guards-evaluator/command-analysis.js +572 -0
- package/dist/guards-evaluator/command-analysis.js.map +1 -0
- package/dist/guards-evaluator/command-view.js +586 -0
- package/dist/guards-evaluator/command-view.js.map +1 -0
- package/dist/guards-evaluator/env.js +11 -0
- package/dist/guards-evaluator/env.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +91 -41
- package/dist/guards-evaluator/evaluate.js.map +1 -1
- package/dist/guards-evaluator/index.js +2 -1
- package/dist/guards-evaluator/index.js.map +1 -1
- package/dist/guards-evaluator/inline-code.js +259 -0
- package/dist/guards-evaluator/inline-code.js.map +1 -0
- package/dist/guards-evaluator/normalize.js +716 -0
- package/dist/guards-evaluator/normalize.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +13 -5
- package/dist/guards-evaluator/preprocess.js.map +1 -1
- package/dist/guards-evaluator/redact.js +19 -0
- package/dist/guards-evaluator/redact.js.map +1 -1
- package/dist/guards-evaluator/regex.js +25 -3
- package/dist/guards-evaluator/regex.js.map +1 -1
- package/dist/guards-evaluator/safe-commands.js +155 -0
- package/dist/guards-evaluator/safe-commands.js.map +1 -0
- package/dist/guards-evaluator/triage.js +315 -0
- package/dist/guards-evaluator/triage.js.map +1 -0
- package/dist/hook-runner.js +24679 -5079
- package/dist/hooks/install.js +38 -0
- package/dist/hooks/install.js.map +1 -1
- package/dist/hooks/runtime/config.js +38 -0
- package/dist/hooks/runtime/config.js.map +1 -1
- package/dist/hooks/runtime/emit.js +94 -61
- package/dist/hooks/runtime/emit.js.map +1 -1
- package/dist/hooks/runtime/fetch-rules.js +22 -7
- package/dist/hooks/runtime/fetch-rules.js.map +1 -1
- package/dist/hooks/runtime/gateway-http-error.js +34 -0
- package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +139 -17
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
- package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
- package/dist/hooks/runtime/judge-cache.js +6 -0
- package/dist/hooks/runtime/judge-cache.js.map +1 -1
- package/dist/hooks/runtime/judge-triage.js +193 -0
- package/dist/hooks/runtime/judge-triage.js.map +1 -0
- package/dist/hooks/runtime/judge.js +7 -1
- package/dist/hooks/runtime/judge.js.map +1 -1
- package/dist/hooks/runtime/pre-compact.js +12 -6
- package/dist/hooks/runtime/pre-compact.js.map +1 -1
- package/dist/hooks/runtime/resolve.js +90 -8
- package/dist/hooks/runtime/resolve.js.map +1 -1
- package/dist/hooks/runtime/retry-policy.js +57 -0
- package/dist/hooks/runtime/retry-policy.js.map +1 -0
- package/dist/hooks/runtime/review-sessions.js +74 -0
- package/dist/hooks/runtime/review-sessions.js.map +1 -0
- package/dist/hooks/runtime/script-resolve.js +695 -0
- package/dist/hooks/runtime/script-resolve.js.map +1 -0
- package/dist/hooks/runtime/session-activation.js +5 -1
- package/dist/hooks/runtime/session-activation.js.map +1 -1
- package/dist/hooks/runtime/session-review.js +163 -45
- package/dist/hooks/runtime/session-review.js.map +1 -1
- package/dist/hooks/runtime/session-start.js +2 -1
- package/dist/hooks/runtime/session-start.js.map +1 -1
- package/dist/hooks/runtime/session-writes.js +232 -0
- package/dist/hooks/runtime/session-writes.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +26 -7
- package/dist/hooks/runtime/sniff.js.map +1 -1
- package/dist/hooks/runtime/target-resolve.js +336 -0
- package/dist/hooks/runtime/target-resolve.js.map +1 -0
- package/dist/hooks/runtime/unmapped-tools.js +93 -0
- package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
- package/dist/hooks/runtime/usage-block.js +4 -0
- package/dist/hooks/runtime/usage-block.js.map +1 -1
- package/dist/hooks/runtime/user-prompt-submit.js +2 -1
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
- package/dist/hooks/runtime/wire-text.js +41 -0
- package/dist/hooks/runtime/wire-text.js.map +1 -0
- package/dist/hooks/runtime/work-dir.js +84 -0
- package/dist/hooks/runtime/work-dir.js.map +1 -0
- package/dist/index.js +45 -12
- package/dist/index.js.map +1 -1
- package/dist/install.js +174 -11
- package/dist/install.js.map +1 -1
- package/dist/link.js +74 -48
- package/dist/link.js.map +1 -1
- package/dist/login.js +77 -11
- package/dist/login.js.map +1 -1
- package/dist/mcp/auth/agent-sign-ins.js +174 -0
- package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
- package/dist/mcp/auth/callback.js +200 -0
- package/dist/mcp/auth/callback.js.map +1 -0
- package/dist/mcp/auth/connect.js +392 -0
- package/dist/mcp/auth/connect.js.map +1 -0
- package/dist/mcp/auth/discovery.js +399 -0
- package/dist/mcp/auth/discovery.js.map +1 -0
- package/dist/mcp/auth/oauth.js +326 -0
- package/dist/mcp/auth/oauth.js.map +1 -0
- package/dist/mcp/auth/probe.js +128 -0
- package/dist/mcp/auth/probe.js.map +1 -0
- package/dist/mcp/auth/secrets.js +446 -0
- package/dist/mcp/auth/secrets.js.map +1 -0
- package/dist/mcp/auth/store.js +260 -0
- package/dist/mcp/auth/store.js.map +1 -0
- package/dist/mcp/auth/tokenSource.js +55 -0
- package/dist/mcp/auth/tokenSource.js.map +1 -0
- package/dist/mcp/config/atomicWrite.js +98 -0
- package/dist/mcp/config/atomicWrite.js.map +1 -0
- package/dist/mcp/config/clients.js +171 -0
- package/dist/mcp/config/clients.js.map +1 -0
- package/dist/mcp/config/cybedefend-servers.js +48 -0
- package/dist/mcp/config/cybedefend-servers.js.map +1 -0
- package/dist/mcp/config/discover.js +114 -0
- package/dist/mcp/config/discover.js.map +1 -0
- package/dist/mcp/config/hook.js +123 -0
- package/dist/mcp/config/hook.js.map +1 -0
- package/dist/mcp/config/index.js +11 -0
- package/dist/mcp/config/index.js.map +1 -0
- package/dist/mcp/config/install.js +132 -0
- package/dist/mcp/config/install.js.map +1 -0
- package/dist/mcp/config/installed.js +37 -0
- package/dist/mcp/config/installed.js.map +1 -0
- package/dist/mcp/config/jsonEdit.js +119 -0
- package/dist/mcp/config/jsonEdit.js.map +1 -0
- package/dist/mcp/config/lock.js +102 -0
- package/dist/mcp/config/lock.js.map +1 -0
- package/dist/mcp/config/proxyBinary.js +211 -0
- package/dist/mcp/config/proxyBinary.js.map +1 -0
- package/dist/mcp/config/rewrite.js +388 -0
- package/dist/mcp/config/rewrite.js.map +1 -0
- package/dist/mcp/config/types.js +34 -0
- package/dist/mcp/config/types.js.map +1 -0
- package/dist/mcp/proxy/audit.js +83 -0
- package/dist/mcp/proxy/audit.js.map +1 -0
- package/dist/mcp/proxy/authInterceptor.js +119 -0
- package/dist/mcp/proxy/authInterceptor.js.map +1 -0
- package/dist/mcp/proxy/expand.js +93 -0
- package/dist/mcp/proxy/expand.js.map +1 -0
- package/dist/mcp/proxy/inject.js +397 -0
- package/dist/mcp/proxy/inject.js.map +1 -0
- package/dist/mcp/proxy/intercept.js +37 -0
- package/dist/mcp/proxy/intercept.js.map +1 -0
- package/dist/mcp/proxy/jsonrpc.js +153 -0
- package/dist/mcp/proxy/jsonrpc.js.map +1 -0
- package/dist/mcp/proxy/log.js +109 -0
- package/dist/mcp/proxy/log.js.map +1 -0
- package/dist/mcp/proxy/paths.js +53 -0
- package/dist/mcp/proxy/paths.js.map +1 -0
- package/dist/mcp/proxy/proxy.js +510 -0
- package/dist/mcp/proxy/proxy.js.map +1 -0
- package/dist/mcp/proxy/spec.js +203 -0
- package/dist/mcp/proxy/spec.js.map +1 -0
- package/dist/mcp/proxy/sse.js +77 -0
- package/dist/mcp/proxy/sse.js.map +1 -0
- package/dist/mcp/proxy/upstream.js +2 -0
- package/dist/mcp/proxy/upstream.js.map +1 -0
- package/dist/mcp/proxy/upstreamHttp.js +499 -0
- package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
- package/dist/mcp/proxy/upstreamStdio.js +188 -0
- package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
- package/dist/mcp/proxy/upstreams.js +143 -0
- package/dist/mcp/proxy/upstreams.js.map +1 -0
- package/dist/mcp/proxy/verifyInterceptor.js +559 -0
- package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
- package/dist/mcp/proxy/windowsCommand.js +99 -0
- package/dist/mcp/proxy/windowsCommand.js.map +1 -0
- package/dist/mcp/scan/args.js +228 -0
- package/dist/mcp/scan/args.js.map +1 -0
- package/dist/mcp/scan/results.js +253 -0
- package/dist/mcp/scan/results.js.map +1 -0
- package/dist/mcp/scan/types.js +40 -0
- package/dist/mcp/scan/types.js.map +1 -0
- package/dist/mcp/verify/api.js +475 -0
- package/dist/mcp/verify/api.js.map +1 -0
- package/dist/mcp/verify/backend.js +143 -0
- package/dist/mcp/verify/backend.js.map +1 -0
- package/dist/mcp/verify/cache.js +182 -0
- package/dist/mcp/verify/cache.js.map +1 -0
- package/dist/mcp/verify/contract-errors.js +198 -0
- package/dist/mcp/verify/contract-errors.js.map +1 -0
- package/dist/mcp/verify/digest.js +91 -0
- package/dist/mcp/verify/digest.js.map +1 -0
- package/dist/mcp/verify/factory.js +56 -0
- package/dist/mcp/verify/factory.js.map +1 -0
- package/dist/mcp/verify/identity.js +173 -0
- package/dist/mcp/verify/identity.js.map +1 -0
- package/dist/mcp/verify/policy-store.js +63 -0
- package/dist/mcp/verify/policy-store.js.map +1 -0
- package/dist/mcp/verify/policy.js +158 -0
- package/dist/mcp/verify/policy.js.map +1 -0
- package/dist/mcp/verify/pool.js +27 -0
- package/dist/mcp/verify/pool.js.map +1 -0
- package/dist/mcp/verify/token-refresh.js +44 -0
- package/dist/mcp/verify/token-refresh.js.map +1 -0
- package/dist/mcp/verify/token.js +67 -0
- package/dist/mcp/verify/token.js.map +1 -0
- package/dist/mcp/verify/verifier.js +1009 -0
- package/dist/mcp/verify/verifier.js.map +1 -0
- package/dist/org-gate.js +1 -1
- package/dist/org-gate.js.map +1 -1
- package/dist/org.js +146 -0
- package/dist/org.js.map +1 -0
- package/dist/project-create.js +129 -0
- package/dist/project-create.js.map +1 -0
- package/dist/prompts.js +21 -0
- package/dist/prompts.js.map +1 -1
- package/dist/re2.wasm +0 -0
- package/dist/status.js +61 -0
- package/dist/status.js.map +1 -1
- package/dist/uninstall.js +49 -4
- package/dist/uninstall.js.map +1 -1
- package/dist/utils.js +62 -6
- package/dist/utils.js.map +1 -1
- 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: `.
|
|
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 + `
|
|
13
|
-
* `apply_patch` carrying a unified diff
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"sniff.js","sourceRoot":"","sources":["../../../src/hooks/runtime/sniff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AAE/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAG7D;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,MAAc;IAC5D,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IACxC,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,OAAO,WAAW,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAC7E,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,WAAW,GAAG,2CAA2C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3E,IAAI,WAAW;YAAE,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,+DAA+D;AAC/D,SAAS,uBAAuB,CAC9B,KAAiD;IAEjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SAClE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,IAAI,CAAC,SAAS,CAAC,CAAC;AACrB,CAAC;AAED,yCAAyC;AACzC,SAAS,iBAAiB,CACxB,KAAiD;IAEjD,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,KAA8B;IAClD,wEAAwE;IACxE,IAAI,OAAO,KAAK,CAAC,iBAAiB,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,KAAK,CAAC,iBAAiB,CAAC;QACvC,MAAM,QAAQ,GAAI,KAAK,CAAC,SAAqC,IAAI,EAAE,CAAC;QACpE,MAAM,QAAQ,GACZ,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,MAAM,UAAU,GAAG,iBAAiB,CAClC,QAAQ,CAAC,KAAmD,CAC7D,CAAC;QACF,IAAI,QAAQ,GAAG,MAAM,CAAC;QACtB,IAAI,MAAM,KAAK,gBAAgB,IAAI,MAAM,KAAK,iBAAiB,EAAE,CAAC;YAChE,QAAQ,GAAG,OAAO,CAAC;QACrB,CAAC;aAAM,IAAI,MAAM,KAAK,eAAe,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;YACrE,QAAQ,GAAG,MAAM,CAAC;QACpB,CAAC;QACD,OAAO;YACL,MAAM,EAAE,UAAU;YAClB,QAAQ;YACR,QAAQ;YACR,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;YACrC,UAAU;YACV,GAAG,EAAE,KAAK;SACX,CAAC;IACJ,CAAC;IAED,wEAAwE;IACxE,mEAAmE;IACnE,4DAA4D;IAC5D,IAAI,eAAe,IAAI,KAAK,EAAE,CAAC;QAC7B,OAAO,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED,wEAAwE;IACxE,IAAI,iBAAiB,IAAI,KAAK,EAAE,CAAC;QAC/B,MAAM,GAAG,GACP,OAAO,KAAK,CAAC,eAAe,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,IAAI,MAAgB,CAAC;QACrB,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,qDAAqD;YACrD,MAAM,GAAG,QAAQ,CAAC;QACpB,CAAC;aAAM,CAAC;YACN,iEAAiE;YACjE,oEAAoE;YACpE,sEAAsE;YACtE,kEAAkE;YAClE,mEAAmE;YACnE,EAAE;YACF,0DAA0D;YAC1D,wEAAwE;YACxE,2DAA2D;YAC3D,gEAAgE;YAChE,gEAAgE;YAChE,eAAe;YACf,MAAM,QAAQ,GACZ,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,IAAI,QAAQ,KAAK,aAAa,IAAI,SAAS,IAAI,KAAK,EAAE,CAAC;gBACrD,MAAM,GAAG,OAAO,CAAC;YACnB,CAAC;iBAAM,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClC,MAAM,GAAG,QAAQ,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,aAAa,CAAC;YACzB,CAAC;QACH,CAAC;QACD,OAAO,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED,wEAAwE;IACxE,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE,EAAE;QACb,UAAU,EAAE,EAAE;QACd,GAAG,EAAE,KAAK;KACX,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,SAAS,eAAe,CAAC,KAA8B;IACrD,OAAO,CACL,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ;QACnC,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAC5C,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,qBAAqB,CAC5B,KAA8B,EAC9B,MAAgB;IAEhB,MAAM,QAAQ,GACZ,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7D,MAAM,SAAS,GAAI,KAAK,CAAC,UAAsC,IAAI,EAAE,CAAC;IACtE,IAAI,QAAQ,GACV,OAAO,SAAS,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IACrE,IAAI,SAAS,GAAa,EAAE,CAAC;IAC7B,IAAI,UAAU,GAAG,EAAE,CAAC;IAEpB,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,uEAAuE;YACvE,oEAAoE;YACpE,oEAAoE;YACpE,kEAAkE;YAClE,MAAM,OAAO,GACX,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,UAAU,GAAG,OAAO,CAAC;YACrB,MAAM,GAAG,GACP,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC;gBACnD,CAAC,CAAC,KAAK,CAAC,GAAG;gBACX,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACpB,SAAS,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACtD,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,CAC3B,CAAC;YACF,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9B,MAAM;QACR,CAAC;QACD,KAAK,OAAO;YACV,UAAU;gBACR,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM;QACR,KAAK,MAAM;YACT,UAAU;gBACR,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACvE,MAAM;QACR,KAAK,WAAW;YACd,UAAU,GAAG,uBAAuB,CAClC,SAAS,CAAC,KAAmD,CAC9D,CAAC;YACF,MAAM;QACR,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,qEAAqE;YACrE,iEAAiE;YACjE,sEAAsE;YACtE,mEAAmE;YACnE,sDAAsD;YACtD,MAAM,KAAK,GACT,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ;gBACjC,CAAC,CAAC,SAAS,CAAC,KAAK;gBACjB,CAAC,CAAC,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ;oBACrC,CAAC,CAAC,SAAS,CAAC,OAAO;oBACnB,CAAC,CAAC,EAAE,CAAC;YACX,UAAU,GAAG,KAAK,CAAC;YACnB,IAAI,CAAC,QAAQ;gBAAE,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YACnD,MAAM;QACR,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,yEAAyE;IACzE,uEAAuE;IACvE,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1D,IAAI,QAAQ;YAAE,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjE,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,GAAa,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ;QAAE,SAAS,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/D,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AAC3E,CAAC"}
|
|
@@ -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
|