@cybedefend/vibedefend 1.1.1
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/LICENSE +77 -0
- package/README.md +120 -0
- package/bin/vibedefend.js +19 -0
- package/dist/auth/auth-store.js +170 -0
- package/dist/auth/auth-store.js.map +1 -0
- package/dist/auth/auth.js +125 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/callback-server.js +216 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/pkce.js +31 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/token-exchange.js +83 -0
- package/dist/auth/token-exchange.js.map +1 -0
- package/dist/clients/claude-code.js +170 -0
- package/dist/clients/claude-code.js.map +1 -0
- package/dist/clients/codex.js +378 -0
- package/dist/clients/codex.js.map +1 -0
- package/dist/clients/cursor-guards-rules.js +94 -0
- package/dist/clients/cursor-guards-rules.js.map +1 -0
- package/dist/clients/cursor.js +172 -0
- package/dist/clients/cursor.js.map +1 -0
- package/dist/clients/detect.js +86 -0
- package/dist/clients/detect.js.map +1 -0
- package/dist/clients/registry.js +41 -0
- package/dist/clients/registry.js.map +1 -0
- package/dist/clients/types.js +2 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/clients/vscode.js +187 -0
- package/dist/clients/vscode.js.map +1 -0
- package/dist/clients/windsurf.js +151 -0
- package/dist/clients/windsurf.js.map +1 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/custom-regions.js +112 -0
- package/dist/custom-regions.js.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/doctor.js +125 -0
- package/dist/doctor.js.map +1 -0
- package/dist/guards-evaluator/bucketing.js +83 -0
- package/dist/guards-evaluator/bucketing.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +272 -0
- package/dist/guards-evaluator/evaluate.js.map +1 -0
- package/dist/guards-evaluator/glob.js +148 -0
- package/dist/guards-evaluator/glob.js.map +1 -0
- package/dist/guards-evaluator/index.js +9 -0
- package/dist/guards-evaluator/index.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +174 -0
- package/dist/guards-evaluator/preprocess.js.map +1 -0
- package/dist/guards-evaluator/redact.js +111 -0
- package/dist/guards-evaluator/redact.js.map +1 -0
- package/dist/guards-evaluator/regex.js +125 -0
- package/dist/guards-evaluator/regex.js.map +1 -0
- package/dist/guards-evaluator/types.js +2 -0
- package/dist/guards-evaluator/types.js.map +1 -0
- package/dist/guards-evaluator/validation.js +115 -0
- package/dist/guards-evaluator/validation.js.map +1 -0
- package/dist/hook-runner.js +6680 -0
- package/dist/hooks/install.js +169 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/runtime/api.js +167 -0
- package/dist/hooks/runtime/api.js.map +1 -0
- package/dist/hooks/runtime/config.js +60 -0
- package/dist/hooks/runtime/config.js.map +1 -0
- package/dist/hooks/runtime/emit.js +45 -0
- package/dist/hooks/runtime/emit.js.map +1 -0
- package/dist/hooks/runtime/fetch-rules.js +154 -0
- package/dist/hooks/runtime/fetch-rules.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +217 -0
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -0
- package/dist/hooks/runtime/guard-violations-buffer.js +105 -0
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -0
- package/dist/hooks/runtime/pre-compact.js +41 -0
- package/dist/hooks/runtime/pre-compact.js.map +1 -0
- package/dist/hooks/runtime/resolve.js +206 -0
- package/dist/hooks/runtime/resolve.js.map +1 -0
- package/dist/hooks/runtime/session-review.js +198 -0
- package/dist/hooks/runtime/session-review.js.map +1 -0
- package/dist/hooks/runtime/session-start.js +101 -0
- package/dist/hooks/runtime/session-start.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +112 -0
- package/dist/hooks/runtime/sniff.js.map +1 -0
- package/dist/hooks/runtime/types.js +22 -0
- package/dist/hooks/runtime/types.js.map +1 -0
- package/dist/hooks/runtime/user-prompt-submit.js +154 -0
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/install.js +183 -0
- package/dist/install.js.map +1 -0
- package/dist/login.js +335 -0
- package/dist/login.js.map +1 -0
- package/dist/prompts.js +134 -0
- package/dist/prompts.js.map +1 -0
- package/dist/self-update.js +177 -0
- package/dist/self-update.js.map +1 -0
- package/dist/status.js +58 -0
- package/dist/status.js.map +1 -0
- package/dist/utils.js +84 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ETag-aware cache for Action Guard effective rules.
|
|
3
|
+
*
|
|
4
|
+
* Rules are fetched from the gateway endpoint
|
|
5
|
+
* GET /project/<id>/action-guards/effective-rules
|
|
6
|
+
* and cached to `~/.cybedefend/guards-cache.json` with a 60-second TTL.
|
|
7
|
+
*
|
|
8
|
+
* On each invocation:
|
|
9
|
+
* 1. Read the cache file.
|
|
10
|
+
* 2. If fresh (age < staleSec), return cached rules immediately.
|
|
11
|
+
* 3. If stale, re-fetch with `If-None-Match: <etag>`.
|
|
12
|
+
* 4. On 304 Not Modified, touch the cache (extend TTL) and return
|
|
13
|
+
* cached rules.
|
|
14
|
+
* 5. On 200, persist new rules + new ETag.
|
|
15
|
+
*
|
|
16
|
+
* Fail-closed semantics:
|
|
17
|
+
* - If the fetch fails AND there are no cached rules (cold start or
|
|
18
|
+
* cache for a different project), `refreshIfStale` returns
|
|
19
|
+
* `{ kind: 'unavailable', reason: '...' }`. Callers MUST block the
|
|
20
|
+
* tool call rather than silently allow everything.
|
|
21
|
+
* - If the fetch fails but cached rules are present (rolling session
|
|
22
|
+
* with a transient auth/network hiccup), the cache returns
|
|
23
|
+
* `{ kind: 'rules', rules: [...] }` and emits a stderr warning so
|
|
24
|
+
* the operator is aware of the degraded state.
|
|
25
|
+
* - Cache read/write failures are still swallowed — I/O errors must
|
|
26
|
+
* never interrupt legitimate work.
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
29
|
+
import { dirname } from 'node:path';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
export const DEFAULT_CACHE_FILE = join(homedir(), '.cybedefend', 'guards-cache.json');
|
|
33
|
+
export const DEFAULT_STALE_SEC = 60;
|
|
34
|
+
// ─── Category-id → evaluator category string ─────────────────────────────────
|
|
35
|
+
const CATEGORY_ID_MAP = {
|
|
36
|
+
'gc-filesystem': 'filesystem',
|
|
37
|
+
'gc-shell': 'shell',
|
|
38
|
+
'gc-network': 'network',
|
|
39
|
+
'gc-git': 'git',
|
|
40
|
+
'gc-process': 'process',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Transform a raw snake_case rule object returned by the gateway API
|
|
44
|
+
* into the camelCase `Rule` shape expected by the bundled guards-evaluator.
|
|
45
|
+
*
|
|
46
|
+
* The API returns fields like `file_path_globs`, `category_id`,
|
|
47
|
+
* `default_action`, etc. The evaluator's `Rule` interface uses `filePathGlobs`,
|
|
48
|
+
* `category`, etc. Passing the raw payload straight to `evaluate()` makes every
|
|
49
|
+
* matcher field resolve to `undefined`, so nothing ever matches and the verdict
|
|
50
|
+
* is always `allow`.
|
|
51
|
+
*
|
|
52
|
+
* Mirrors mcp-service's `snakeToCamelRule` in
|
|
53
|
+
* `mcp-service/src/modules/gateway-client/gateway-client.service.ts`.
|
|
54
|
+
*/
|
|
55
|
+
export function snakeToCamelRule(raw) {
|
|
56
|
+
const rawCategoryId = raw['category_id'];
|
|
57
|
+
const category = rawCategoryId
|
|
58
|
+
? (CATEGORY_ID_MAP[rawCategoryId] ?? rawCategoryId)
|
|
59
|
+
: raw['category'];
|
|
60
|
+
return {
|
|
61
|
+
id: raw['id'],
|
|
62
|
+
category: category,
|
|
63
|
+
...(rawCategoryId ? { categoryId: rawCategoryId } : {}),
|
|
64
|
+
builtinKey: (raw['builtin_key'] ?? raw['builtinKey']),
|
|
65
|
+
name: raw['name'],
|
|
66
|
+
description: raw['description'],
|
|
67
|
+
severity: raw['severity'],
|
|
68
|
+
action: raw['action'],
|
|
69
|
+
subcategory: raw['subcategory'],
|
|
70
|
+
active: raw['active'],
|
|
71
|
+
version: raw['version'],
|
|
72
|
+
tags: raw['tags'],
|
|
73
|
+
isBuiltin: (raw['is_builtin'] ?? raw['isBuiltin']),
|
|
74
|
+
toolMatch: (raw['tool_match'] ?? raw['toolMatch']),
|
|
75
|
+
filePathGlobs: (raw['file_path_globs'] ?? raw['filePathGlobs']),
|
|
76
|
+
filePathExcludes: (raw['file_path_excludes'] ?? raw['filePathExcludes']),
|
|
77
|
+
commandRegex: (raw['command_regex'] ?? raw['commandRegex']),
|
|
78
|
+
commandArgGlobs: (raw['command_arg_globs'] ?? raw['commandArgGlobs']),
|
|
79
|
+
envNameGlobs: (raw['env_name_globs'] ?? raw['envNameGlobs']),
|
|
80
|
+
httpHostGlobs: (raw['http_host_globs'] ?? raw['httpHostGlobs']),
|
|
81
|
+
httpHostExcludes: (raw['http_host_excludes'] ?? raw['httpHostExcludes']),
|
|
82
|
+
httpMethod: (raw['http_method'] ?? raw['httpMethod']),
|
|
83
|
+
httpPathGlobs: (raw['http_path_globs'] ?? raw['httpPathGlobs']),
|
|
84
|
+
gitRemoteGlobs: (raw['git_remote_globs'] ?? raw['gitRemoteGlobs']),
|
|
85
|
+
gitBranchGlobs: (raw['git_branch_globs'] ?? raw['gitBranchGlobs']),
|
|
86
|
+
gitSubcommand: (raw['git_subcommand'] ?? raw['gitSubcommand']),
|
|
87
|
+
denyMessage: (raw['deny_message'] ?? raw['denyMessage']),
|
|
88
|
+
warnMessage: (raw['warn_message'] ?? raw['warnMessage']),
|
|
89
|
+
defaultAction: (raw['default_action'] ?? raw['defaultAction']),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create an injectable guard rules cache (testable via custom cacheFile and fetchFn).
|
|
94
|
+
*/
|
|
95
|
+
export function makeGuardRulesCache(opts) {
|
|
96
|
+
const cacheFile = opts.cacheFile ?? DEFAULT_CACHE_FILE;
|
|
97
|
+
const staleSec = opts.staleSec ?? DEFAULT_STALE_SEC;
|
|
98
|
+
const fetchFn = opts.fetchFn;
|
|
99
|
+
async function refreshIfStale(ctx) {
|
|
100
|
+
const entry = readCache(ctx.projectId);
|
|
101
|
+
const ageSec = entry
|
|
102
|
+
? (Date.now() - entry.fetchedAt) / 1000
|
|
103
|
+
: Infinity;
|
|
104
|
+
if (entry && ageSec < staleSec) {
|
|
105
|
+
// Cache is fresh — return immediately
|
|
106
|
+
return { kind: 'rules', rules: entry.rules };
|
|
107
|
+
}
|
|
108
|
+
// Fetch (with ETag for conditional request if we have one)
|
|
109
|
+
try {
|
|
110
|
+
const result = await fetchFn({
|
|
111
|
+
...ctx,
|
|
112
|
+
etag: entry?.etag ?? null,
|
|
113
|
+
});
|
|
114
|
+
if (result.notModified && entry) {
|
|
115
|
+
// 304 — touch the TTL and return original rules
|
|
116
|
+
writeCache({
|
|
117
|
+
...entry,
|
|
118
|
+
fetchedAt: Date.now(),
|
|
119
|
+
});
|
|
120
|
+
return { kind: 'rules', rules: entry.rules };
|
|
121
|
+
}
|
|
122
|
+
// 200 — persist fresh rules (transform snake_case → camelCase)
|
|
123
|
+
const transformedRules = result.rules.map((r) => snakeToCamelRule(r));
|
|
124
|
+
const fresh = {
|
|
125
|
+
projectId: ctx.projectId,
|
|
126
|
+
rules: transformedRules,
|
|
127
|
+
etag: result.etag,
|
|
128
|
+
fetchedAt: Date.now(),
|
|
129
|
+
};
|
|
130
|
+
writeCache(fresh);
|
|
131
|
+
return { kind: 'rules', rules: fresh.rules };
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
135
|
+
if (entry?.rules?.length) {
|
|
136
|
+
// We have something to work with — use cached rules and emit a warning
|
|
137
|
+
// so the operator is aware that there is an auth/network issue brewing.
|
|
138
|
+
const ageSec = Math.round((Date.now() - entry.fetchedAt) / 1000);
|
|
139
|
+
process.stderr.write(`[VibeDefend] Action Guards: rule refresh failed (${msg}). ` +
|
|
140
|
+
`Using cached rules (age=${ageSec}s).\n`);
|
|
141
|
+
return { kind: 'rules', rules: entry.rules };
|
|
142
|
+
}
|
|
143
|
+
// Cold start with no cached rules — fail-closed.
|
|
144
|
+
return {
|
|
145
|
+
kind: 'unavailable',
|
|
146
|
+
reason: `fetch failed and no cached rules available: ${msg}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function readCache(projectId) {
|
|
151
|
+
if (!existsSync(cacheFile))
|
|
152
|
+
return null;
|
|
153
|
+
try {
|
|
154
|
+
const raw = readFileSync(cacheFile, 'utf8');
|
|
155
|
+
const data = JSON.parse(raw);
|
|
156
|
+
// Validate that the cached entry is for this project
|
|
157
|
+
if (data.projectId !== projectId)
|
|
158
|
+
return null;
|
|
159
|
+
return data;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function writeCache(entry) {
|
|
166
|
+
try {
|
|
167
|
+
ensureDirFor(cacheFile);
|
|
168
|
+
writeFileSync(cacheFile, JSON.stringify(entry), 'utf8');
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Best-effort — silent failure
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { refreshIfStale };
|
|
175
|
+
}
|
|
176
|
+
function ensureDirFor(filePath) {
|
|
177
|
+
const dir = dirname(filePath);
|
|
178
|
+
if (!existsSync(dir)) {
|
|
179
|
+
mkdirSync(dir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// ─── Gateway fetch implementation (used by the hook runner) ──────────────────
|
|
183
|
+
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
184
|
+
/**
|
|
185
|
+
* Fetch effective action guard rules from the gateway.
|
|
186
|
+
*
|
|
187
|
+
* Uses the existing `fetch` API. Returns the full result or throws on error
|
|
188
|
+
* (the cache layer catches and falls through to cached/empty rules).
|
|
189
|
+
*/
|
|
190
|
+
export async function fetchEffectiveRulesFromGateway(ctx, opts = {}) {
|
|
191
|
+
const f = opts.fetchImpl ?? globalThis.fetch;
|
|
192
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
193
|
+
const url = `${ctx.apiBaseResolved}/project/${encodeURIComponent(ctx.projectId)}/action-guards/effective-rules`;
|
|
194
|
+
const headers = {
|
|
195
|
+
Authorization: `Bearer ${ctx.token}`,
|
|
196
|
+
};
|
|
197
|
+
if (ctx.etag) {
|
|
198
|
+
headers['If-None-Match'] = ctx.etag;
|
|
199
|
+
}
|
|
200
|
+
const res = await f(url, {
|
|
201
|
+
method: 'GET',
|
|
202
|
+
headers,
|
|
203
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
204
|
+
});
|
|
205
|
+
if (res.status === 304) {
|
|
206
|
+
return { rules: [], etag: ctx.etag ?? null, notModified: true };
|
|
207
|
+
}
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
throw new Error(`HTTP ${res.status} from effective-rules endpoint`);
|
|
210
|
+
}
|
|
211
|
+
const body = (await res.json());
|
|
212
|
+
const data = body.data ?? body;
|
|
213
|
+
const rules = Array.isArray(data.rules) ? data.rules : [];
|
|
214
|
+
const etag = res.headers.get('ETag');
|
|
215
|
+
return { rules, etag, notModified: false };
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=guard-rules-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-rules-cache.js","sourceRoot":"","sources":["../../../src/hooks/runtime/guard-rules-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CACpC,OAAO,EAAE,EACT,aAAa,EACb,mBAAmB,CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAgDpC,gFAAgF;AAEhF,MAAM,eAAe,GAA2B;IAC9C,eAAe,EAAE,YAAY;IAC7B,UAAU,EAAE,OAAO;IACnB,YAAY,EAAE,SAAS;IACvB,QAAQ,EAAE,KAAK;IACf,YAAY,EAAE,SAAS;CACxB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAA4B;IAC3D,MAAM,aAAa,GAAG,GAAG,CAAC,aAAa,CAAuB,CAAC;IAC/D,MAAM,QAAQ,GAAG,aAAa;QAC5B,CAAC,CAAC,CAAC,eAAe,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC;QACnD,CAAC,CAAE,GAAG,CAAC,UAAU,CAAY,CAAC;IAEhC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,IAAI,CAAW;QACvB,QAAQ,EAAE,QAA4B;QACtC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,UAAU,EAAE,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC,CAAuB;QAC3E,IAAI,EAAE,GAAG,CAAC,MAAM,CAAuB;QACvC,WAAW,EAAE,GAAG,CAAC,aAAa,CAAuB;QACrD,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAqB;QAC7C,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAmB;QACvC,WAAW,EAAE,GAAG,CAAC,aAAa,CAAwB;QACtD,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAwB;QAC5C,OAAO,EAAE,GAAG,CAAC,SAAS,CAAuB;QAC7C,IAAI,EAAE,GAAG,CAAC,MAAM,CAAyB;QACzC,SAAS,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,CAAwB;QACzE,SAAS,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,CAAyB;QAC1E,aAAa,EAAE,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC,CAAyB;QACvF,gBAAgB,EAAE,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAyB;QAChG,YAAY,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,GAAG,CAAC,cAAc,CAAC,CAAuB;QACjF,eAAe,EAAE,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,GAAG,CAAC,iBAAiB,CAAC,CAAyB;QAC7F,YAAY,EAAE,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,cAAc,CAAC,CAAyB;QACpF,aAAa,EAAE,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC,CAAyB;QACvF,gBAAgB,EAAE,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAyB;QAChG,UAAU,EAAE,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC,CAAyB;QAC7E,aAAa,EAAE,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC,CAAyB;QACvF,cAAc,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAyB;QAC1F,cAAc,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAyB;QAC1F,aAAa,EAAE,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC,CAAyB;QACtF,WAAW,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,aAAa,CAAC,CAAuB;QAC9E,WAAW,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,aAAa,CAAC,CAAuB;QAC9E,aAAa,EAAE,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC,CAAgC;KAC9F,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAA4B;IAE5B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAE7B,KAAK,UAAU,cAAc,CAAC,GAI7B;QACC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,KAAK;YAClB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI;YACvC,CAAC,CAAC,QAAQ,CAAC;QAEb,IAAI,KAAK,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC;YAC/B,sCAAsC;YACtC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;QAC/C,CAAC;QAED,2DAA2D;QAC3D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;gBAC3B,GAAG,GAAG;gBACN,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,IAAI;aAC1B,CAAC,CAAC;YAEH,IAAI,MAAM,CAAC,WAAW,IAAI,KAAK,EAAE,CAAC;gBAChC,gDAAgD;gBAChD,UAAU,CAAC;oBACT,GAAG,KAAK;oBACR,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACtB,CAAC,CAAC;gBACH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/C,CAAC;YAED,+DAA+D;YAC/D,MAAM,gBAAgB,GAAI,MAAM,CAAC,KAAmD,CAAC,GAAG,CACtF,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAC3B,CAAC;YACF,MAAM,KAAK,GAAe;gBACxB,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,KAAK,EAAE,gBAAgB;gBACvB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;aACtB,CAAC;YACF,UAAU,CAAC,KAAK,CAAC,CAAC;YAClB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;QAC/C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAEvD,IAAI,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;gBACzB,uEAAuE;gBACvE,wEAAwE;gBACxE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;gBACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,oDAAoD,GAAG,KAAK;oBAC5D,2BAA2B,MAAM,OAAO,CACzC,CAAC;gBACF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/C,CAAC;YAED,iDAAiD;YACjD,OAAO;gBACL,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,+CAA+C,GAAG,EAAE;aAC7D,CAAC;QACJ,CAAC;IACH,CAAC;IAED,SAAS,SAAS,CAAC,SAAiB;QAClC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,IAAI,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAe,CAAC;YAC3C,qDAAqD;YACrD,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC;YAC9C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,SAAS,UAAU,CAAC,KAAiB;QACnC,IAAI,CAAC;YACH,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,+BAA+B;QACjC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAC5B,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB;IACpC,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED,gFAAgF;AAEhF,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,GAAa,EACb,OAAoE,EAAE;IAEtE,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,UAAU,CAAC,KAAK,CAAC;IAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACvD,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,eAAe,YAAY,kBAAkB,CAAC,GAAG,CAAC,SAAS,CAAC,gCAAgC,CAAC;IAEhH,MAAM,OAAO,GAA2B;QACtC,aAAa,EAAE,UAAU,GAAG,CAAC,KAAK,EAAE;KACrC,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACb,OAAO,CAAC,eAAe,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;IACtC,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,EAAE;QACvB,MAAM,EAAE,KAAK;QACb,OAAO;QACP,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;KACvC,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvB,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAClE,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,gCAAgC,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;IAC3D,MAAM,IAAI,GAAI,IAAI,CAAC,IAAgC,IAAI,IAAI,CAAC;IAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,KAAgB,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAErC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard violations buffer.
|
|
3
|
+
*
|
|
4
|
+
* Violations (deny/warn outcomes) are appended as JSON lines to a local
|
|
5
|
+
* buffer file (`~/.cybedefend/guards-violations-buffer.jsonl`) and flushed
|
|
6
|
+
* to the gateway on demand (typically on hook process exit via
|
|
7
|
+
* `process.on('beforeExit')`).
|
|
8
|
+
*
|
|
9
|
+
* Why JSONL + append semantics?
|
|
10
|
+
* - Append to a file is atomic at the OS level for small writes.
|
|
11
|
+
* - If the flush crashes mid-batch the file is left intact (preserving
|
|
12
|
+
* violations for the next flush attempt).
|
|
13
|
+
* - The hook NEVER blocks on the audit write — `bufferViolation` is
|
|
14
|
+
* synchronous (sync appendFileSync with minimal data).
|
|
15
|
+
*
|
|
16
|
+
* Violation semantics:
|
|
17
|
+
* - `deny` → outcome = 'denied'
|
|
18
|
+
* - `warn` → outcome = 'warned'
|
|
19
|
+
* - `allow` → no violation recorded
|
|
20
|
+
*/
|
|
21
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, } from 'node:fs';
|
|
22
|
+
import { dirname } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
export const DEFAULT_BUFFER_FILE = join(homedir(), '.cybedefend', 'guards-violations-buffer.jsonl');
|
|
26
|
+
/**
|
|
27
|
+
* Create an injectable violations buffer (testable via custom bufferFile).
|
|
28
|
+
*
|
|
29
|
+
* Production code uses the default file under `~/.cybedefend/`.
|
|
30
|
+
* Tests inject a temp dir path to avoid touching the real FS.
|
|
31
|
+
*/
|
|
32
|
+
export function makeViolationsBuffer(opts = {}) {
|
|
33
|
+
const bufferFile = opts.bufferFile ?? DEFAULT_BUFFER_FILE;
|
|
34
|
+
function bufferViolation(ctx, v) {
|
|
35
|
+
const record = {
|
|
36
|
+
...v,
|
|
37
|
+
projectId: ctx.projectId,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
ensureDirFor(bufferFile);
|
|
42
|
+
appendFileSync(bufferFile, JSON.stringify(record) + '\n', 'utf8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Best-effort — never block the hook on an I/O error.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function flushViolations(ctx, postFn) {
|
|
49
|
+
if (!existsSync(bufferFile))
|
|
50
|
+
return;
|
|
51
|
+
let content;
|
|
52
|
+
try {
|
|
53
|
+
content = readFileSync(bufferFile, 'utf8').trim();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return; // can't read — skip
|
|
57
|
+
}
|
|
58
|
+
if (!content)
|
|
59
|
+
return;
|
|
60
|
+
const records = [];
|
|
61
|
+
for (const line of content.split('\n')) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (!trimmed)
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
records.push(JSON.parse(trimmed));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Corrupt line — skip it
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (records.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
const BATCH_SIZE = 100;
|
|
75
|
+
try {
|
|
76
|
+
for (let i = 0; i < records.length; i += BATCH_SIZE) {
|
|
77
|
+
const batch = records.slice(i, i + BATCH_SIZE);
|
|
78
|
+
await postFn(ctx, batch);
|
|
79
|
+
}
|
|
80
|
+
// Truncate on success
|
|
81
|
+
try {
|
|
82
|
+
writeFileSync(bufferFile, '', 'utf8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Best-effort truncate
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// POST failed — preserve buffer for next flush attempt
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { bufferViolation, flushViolations };
|
|
93
|
+
}
|
|
94
|
+
/** Ensure the parent directory of a file exists. */
|
|
95
|
+
function ensureDirFor(filePath) {
|
|
96
|
+
const dir = dirname(filePath);
|
|
97
|
+
if (!existsSync(dir)) {
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ─── Default singleton (used by the hook runner) ─────────────────────────────
|
|
102
|
+
const _defaultBuffer = makeViolationsBuffer();
|
|
103
|
+
export const bufferViolation = _defaultBuffer.bufferViolation.bind(_defaultBuffer);
|
|
104
|
+
export const flushViolations = _defaultBuffer.flushViolations.bind(_defaultBuffer);
|
|
105
|
+
//# sourceMappingURL=guard-violations-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-violations-buffer.js","sourceRoot":"","sources":["../../../src/hooks/runtime/guard-violations-buffer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PreCompact hook — same gap-analysis nudge as Stop, but fires right
|
|
3
|
+
* before Claude Code summarises the conversation context. Catches long
|
|
4
|
+
* sessions where the user keeps replying and Stop never fires.
|
|
5
|
+
*
|
|
6
|
+
* Only Claude Code, Cursor, and VS Code Copilot expose this event;
|
|
7
|
+
* Windsurf and Codex don't have an equivalent (those clients' adapters
|
|
8
|
+
* simply don't wire this subcommand).
|
|
9
|
+
*/
|
|
10
|
+
import { sniff } from './sniff.js';
|
|
11
|
+
import { emit } from './emit.js';
|
|
12
|
+
import { loadRuntimeConfig, readStdinJson } from './config.js';
|
|
13
|
+
export async function preCompactHook(deps = {}) {
|
|
14
|
+
const readStdin = deps.readStdin ?? readStdinJson;
|
|
15
|
+
const loadConfig = deps.loadConfig ?? loadRuntimeConfig;
|
|
16
|
+
const emitFn = deps.emitFn ?? emit;
|
|
17
|
+
const stdin = await readStdin();
|
|
18
|
+
const event = sniff(stdin);
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
if (!config)
|
|
21
|
+
return;
|
|
22
|
+
if (!config.hooks.enableSessionReview)
|
|
23
|
+
return;
|
|
24
|
+
if (process.env.CYBEDEFEND_DRY_RUN === '1') {
|
|
25
|
+
emitFn(`## 🛡️ CybeDefend PreCompact (dry-run, client=${event.client})\n`, event.client);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const body = [
|
|
29
|
+
'## 🛡️ CybeDefend PreCompact gap analysis',
|
|
30
|
+
'',
|
|
31
|
+
'Context is about to be compacted — this session has been substantial.',
|
|
32
|
+
'Before the fine-grained tool history is summarised away, run a gap analysis:',
|
|
33
|
+
'1. Re-read the original user request (still uncompacted now).',
|
|
34
|
+
'2. List the CybeDefend rules returned during the session.',
|
|
35
|
+
'3. For uncovered concerns, ask the user via AskUserQuestion whether to propose;',
|
|
36
|
+
' on "yes", call `cybe_rules_report_missing` with confirmed_by_user: true.',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n');
|
|
39
|
+
emitFn(body, event.client);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=pre-compact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-compact.js","sourceRoot":"","sources":["../../../src/hooks/runtime/pre-compact.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAQ/D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAuB,EAAE;IAC5D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,aAAa,CAAC;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,iBAAiB,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;IAEnC,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IAE3B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB;QAAE,OAAO;IAE9C,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,GAAG,EAAE,CAAC;QAC3C,MAAM,CACJ,iDAAiD,KAAK,CAAC,MAAM,KAAK,EAClE,KAAK,CAAC,MAAM,CACb,CAAC;QACF,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG;QACX,2CAA2C;QAC3C,EAAE;QACF,uEAAuE;QACvE,8EAA8E;QAC9E,+DAA+D;QAC/D,2DAA2D;QAC3D,iFAAiF;QACjF,6EAA6E;QAC7E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the project ID + bearer token at hook-invocation time.
|
|
3
|
+
*
|
|
4
|
+
* Both are needed for every API call. Lookup order matches the bash
|
|
5
|
+
* hooks we replaced, so existing users keep the same behaviour:
|
|
6
|
+
*
|
|
7
|
+
* projectId: $CYBEDEFEND_PROJECT_ID → .cybedefend/config.json:projectId
|
|
8
|
+
* token: $CYBEDEFEND_TOKEN → OS keychain (macOS Keychain /
|
|
9
|
+
* Linux libsecret) for the MCP
|
|
10
|
+
* server name we were installed
|
|
11
|
+
* against
|
|
12
|
+
*
|
|
13
|
+
* Windows: we shell out to PowerShell's CredentialManager via `cmdkey`
|
|
14
|
+
* in V1.1; for V1 Windows users provide $CYBEDEFEND_TOKEN explicitly.
|
|
15
|
+
* (Claude Code on Windows doesn't write to a system keychain yet anyway.)
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { getValidAccessToken, LoginRequiredError } from '../../auth/auth.js';
|
|
20
|
+
export function resolveProjectId(cwd = process.cwd()) {
|
|
21
|
+
const envProjectId = process.env.CYBEDEFEND_PROJECT_ID;
|
|
22
|
+
if (envProjectId && envProjectId.length > 0) {
|
|
23
|
+
return { projectId: envProjectId, apiBase: null };
|
|
24
|
+
}
|
|
25
|
+
const configPath = `${cwd}/.cybedefend/config.json`;
|
|
26
|
+
if (existsSync(configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
29
|
+
const cfg = JSON.parse(raw);
|
|
30
|
+
return {
|
|
31
|
+
projectId: cfg.projectId ?? null,
|
|
32
|
+
apiBase: cfg.apiBase ?? null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Malformed JSON — silently fall through to "no projectId" so the
|
|
37
|
+
// hook early-exits without spamming the agent's context.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { projectId: null, apiBase: null };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the bearer token. Returns null if not found.
|
|
44
|
+
*
|
|
45
|
+
* macOS: Claude Code stores MCP OAuth tokens in the Keychain under the
|
|
46
|
+
* service name "Claude Code-credentials". We shell out to `security`
|
|
47
|
+
* (ships with macOS, no install) to extract the JSON blob, then parse
|
|
48
|
+
* for the entry whose `serverName` matches our MCP name.
|
|
49
|
+
*
|
|
50
|
+
* macOS (Codex): Codex stores MCP OAuth tokens in the Keychain under
|
|
51
|
+
* the service name "Codex MCP Credentials". The account is
|
|
52
|
+
* "<mcpName>|<hash>" and the password is a JSON blob with an
|
|
53
|
+
* `access_token` field. We try this keychain after the Claude Code one.
|
|
54
|
+
*
|
|
55
|
+
* Linux: `secret-tool lookup service Claude-Code account <mcpName>`
|
|
56
|
+
* (requires libsecret-tools; ships with most desktop distros).
|
|
57
|
+
*
|
|
58
|
+
* Windows: env var only for V1. Future improvement: `cmdkey /list` →
|
|
59
|
+
* parse, or via a small PowerShell snippet.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveToken(mcpName) {
|
|
62
|
+
const envToken = process.env.CYBEDEFEND_TOKEN;
|
|
63
|
+
if (envToken && envToken.length > 0)
|
|
64
|
+
return envToken;
|
|
65
|
+
if (process.platform === 'darwin') {
|
|
66
|
+
return resolveTokenMacOS(mcpName) ?? resolveTokenCodexMacOS(mcpName);
|
|
67
|
+
}
|
|
68
|
+
if (process.platform === 'linux')
|
|
69
|
+
return resolveTokenLinux(mcpName);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function resolveTokenMacOS(mcpName) {
|
|
73
|
+
let blob;
|
|
74
|
+
try {
|
|
75
|
+
blob = execFileSync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'], {
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
78
|
+
timeout: 2000,
|
|
79
|
+
}).trim();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(blob);
|
|
86
|
+
const oauth = data.mcpOAuth ?? {};
|
|
87
|
+
for (const entry of Object.values(oauth)) {
|
|
88
|
+
if (entry?.serverName === mcpName && entry.accessToken) {
|
|
89
|
+
return entry.accessToken;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Blob isn't JSON — Claude Code stores raw token for some setups.
|
|
95
|
+
// Use it as-is.
|
|
96
|
+
return blob || null;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Codex stores MCP OAuth tokens in the macOS Keychain under service
|
|
102
|
+
* "Codex MCP Credentials". The account is "<mcpName>|<hash>" — we use
|
|
103
|
+
* a prefix match on the service and iterate all matching entries via
|
|
104
|
+
* `security find-generic-password -s "Codex MCP Credentials"`.
|
|
105
|
+
*
|
|
106
|
+
* The password blob is a JSON object with an `access_token` field (the
|
|
107
|
+
* same structure Codex writes when `codex mcp login <name>` completes).
|
|
108
|
+
* Fall back to returning the raw blob as a token if JSON parsing fails
|
|
109
|
+
* (some Codex versions write the raw token string directly).
|
|
110
|
+
*/
|
|
111
|
+
function resolveTokenCodexMacOS(mcpName) {
|
|
112
|
+
// `security find-generic-password` with `-s` returns the FIRST matching
|
|
113
|
+
// entry. We use `-a <account-prefix>` to narrow to our MCP name, but the
|
|
114
|
+
// account has a `|<hash>` suffix we don't know. Instead we enumerate all
|
|
115
|
+
// entries for the Codex service and filter by mcpName prefix.
|
|
116
|
+
let blob;
|
|
117
|
+
try {
|
|
118
|
+
blob = execFileSync('security', ['find-generic-password', '-s', 'Codex MCP Credentials', '-a', mcpName, '-w'], {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
121
|
+
timeout: 2000,
|
|
122
|
+
}).trim();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Exact account match failed (account has a hash suffix). Try without
|
|
126
|
+
// the -a filter and scan all entries for this service by iterating.
|
|
127
|
+
// `security find-generic-password` without `-a` returns the first entry;
|
|
128
|
+
// to iterate all we'd need `security dump-keychain` (requires trust), so
|
|
129
|
+
// we try the service-only lookup and accept the first entry returned.
|
|
130
|
+
try {
|
|
131
|
+
blob = execFileSync('security', ['find-generic-password', '-s', 'Codex MCP Credentials', '-w'], {
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
134
|
+
timeout: 2000,
|
|
135
|
+
}).trim();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!blob)
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
const data = JSON.parse(blob);
|
|
145
|
+
return data.access_token ?? data.accessToken ?? data.token ?? null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Raw token string stored directly.
|
|
149
|
+
return blob || null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function resolveTokenLinux(mcpName) {
|
|
153
|
+
try {
|
|
154
|
+
const token = execFileSync('secret-tool', ['lookup', 'service', 'Claude-Code', 'account', mcpName], {
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
157
|
+
timeout: 2000,
|
|
158
|
+
}).trim();
|
|
159
|
+
return token.length > 0 ? token : null;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ─── Async token resolution with transparent refresh ─────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Async variant of `resolveToken` that transparently refreshes the access_token
|
|
168
|
+
* when near expiry using the stored refresh_token.
|
|
169
|
+
*
|
|
170
|
+
* Falls back to the synchronous `resolveToken` path (legacy keychain lookup) if
|
|
171
|
+
* the new OAuth store has no entry for this MCP name — this ensures backward
|
|
172
|
+
* compatibility during the migration period.
|
|
173
|
+
*
|
|
174
|
+
* Returns null when:
|
|
175
|
+
* - No credentials of any kind are stored (CYBEDEFEND_TOKEN env, keychain,
|
|
176
|
+
* or OAuth store), OR
|
|
177
|
+
* - The refresh_token was rejected by Logto (LoginRequiredError) — in this
|
|
178
|
+
* case local credentials are already cleared; the caller should trigger
|
|
179
|
+
* the UNVERIFIED banner.
|
|
180
|
+
*
|
|
181
|
+
* Never throws — all errors are converted to null so hook callers remain
|
|
182
|
+
* fail-open.
|
|
183
|
+
*/
|
|
184
|
+
export async function resolveTokenWithRefresh(mcpName) {
|
|
185
|
+
// 1. Env var takes highest precedence (CI / scripted use).
|
|
186
|
+
const envToken = process.env.CYBEDEFEND_TOKEN;
|
|
187
|
+
if (envToken && envToken.length > 0)
|
|
188
|
+
return envToken;
|
|
189
|
+
// 2. Try the new OAuth-store path (includes transparent refresh).
|
|
190
|
+
try {
|
|
191
|
+
const token = await getValidAccessToken(mcpName);
|
|
192
|
+
if (token)
|
|
193
|
+
return token;
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (e instanceof LoginRequiredError) {
|
|
197
|
+
// Refresh token rejected — caller triggers UNVERIFIED banner.
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// Transient error (network/DNS) — fall through to the legacy path below
|
|
201
|
+
// so a stale-but-unexpired legacy token can still be used.
|
|
202
|
+
}
|
|
203
|
+
// 3. Fall back to the synchronous legacy keychain lookup (pre-PKCE tokens).
|
|
204
|
+
return resolveToken(mcpName);
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/hooks/runtime/resolve.ts"],"names":[],"mappings":"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"}
|