@synkro-sh/cli 1.4.68 → 1.4.69
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/dist/bootstrap.js +234 -47
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -364,20 +364,20 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
364
364
|
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
365
365
|
timeout: 15,
|
|
366
366
|
failClosed: false,
|
|
367
|
-
matcher: "Shell|Bash|Read|Grep|Glob",
|
|
367
|
+
matcher: "Shell|Bash|Read|ReadFile|Grep|Glob|terminal|run_terminal_cmd|execute_command|read_file|grep_search|file_search|list_dir|codebase_search|delete_file",
|
|
368
368
|
[SYNKRO_MARKER2]: true
|
|
369
369
|
});
|
|
370
370
|
pushCcHook(h, "preToolUse", config.editPrecheckScriptPath, {
|
|
371
371
|
timeout: 15,
|
|
372
|
-
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
|
|
372
|
+
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
|
|
373
373
|
});
|
|
374
374
|
pushCcHook(h, "preToolUse", config.cwePrecheckScriptPath, {
|
|
375
375
|
timeout: 15,
|
|
376
|
-
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
|
|
376
|
+
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
|
|
377
377
|
});
|
|
378
378
|
pushCcHook(h, "preToolUse", config.cvePrecheckScriptPath, {
|
|
379
379
|
timeout: 10,
|
|
380
|
-
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
|
|
380
|
+
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
|
|
381
381
|
});
|
|
382
382
|
pushCcHook(h, "preToolUse", config.agentJudgeScriptPath, {
|
|
383
383
|
timeout: 15,
|
|
@@ -396,7 +396,7 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
396
396
|
});
|
|
397
397
|
pushCcHook(h, "postToolUse", config.bashFollowupScriptPath, {
|
|
398
398
|
timeout: 10,
|
|
399
|
-
matcher: "Shell|Bash"
|
|
399
|
+
matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command|delete_file"
|
|
400
400
|
});
|
|
401
401
|
writeHooksFileAtomic(hooksJsonPath, file);
|
|
402
402
|
}
|
|
@@ -589,13 +589,76 @@ function inspectMcpConfig() {
|
|
|
589
589
|
}
|
|
590
590
|
return { installed: true, configPath: CC_CONFIG_PATH, url: entry.url };
|
|
591
591
|
}
|
|
592
|
-
|
|
592
|
+
function readCursorMcpJson() {
|
|
593
|
+
if (!existsSync3(CURSOR_MCP_PATH)) return {};
|
|
594
|
+
try {
|
|
595
|
+
const raw = readFileSync3(CURSOR_MCP_PATH, "utf-8");
|
|
596
|
+
return JSON.parse(raw);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
throw new Error(`Failed to parse ${CURSOR_MCP_PATH}: ${err.message}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function writeCursorMcpJsonAtomic(config) {
|
|
602
|
+
mkdirSync3(dirname3(CURSOR_MCP_PATH), { recursive: true });
|
|
603
|
+
const tmpPath = `${CURSOR_MCP_PATH}.synkro.tmp`;
|
|
604
|
+
writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
605
|
+
renameSync3(tmpPath, CURSOR_MCP_PATH);
|
|
606
|
+
}
|
|
607
|
+
function installCursorMcpConfig(opts) {
|
|
608
|
+
const config = readCursorMcpJson();
|
|
609
|
+
config.mcpServers = config.mcpServers ?? {};
|
|
610
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
611
|
+
if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
|
|
612
|
+
}
|
|
613
|
+
if (opts.local) {
|
|
614
|
+
const url2 = "http://127.0.0.1:8931/";
|
|
615
|
+
const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
|
|
616
|
+
let localToken = "";
|
|
617
|
+
try {
|
|
618
|
+
localToken = readFileSync3(tokenPath, "utf-8").trim();
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
622
|
+
url: url2,
|
|
623
|
+
...localToken ? { headers: { Authorization: `Bearer ${localToken}` } } : {},
|
|
624
|
+
[SYNKRO_MARKER3]: true
|
|
625
|
+
};
|
|
626
|
+
writeCursorMcpJsonAtomic(config);
|
|
627
|
+
return { path: CURSOR_MCP_PATH, url: url2 };
|
|
628
|
+
}
|
|
629
|
+
const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
|
|
630
|
+
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
631
|
+
url,
|
|
632
|
+
headers: { Authorization: `Bearer ${opts.bearerToken}` },
|
|
633
|
+
[SYNKRO_MARKER3]: true
|
|
634
|
+
};
|
|
635
|
+
writeCursorMcpJsonAtomic(config);
|
|
636
|
+
return { path: CURSOR_MCP_PATH, url };
|
|
637
|
+
}
|
|
638
|
+
function uninstallCursorMcpConfig() {
|
|
639
|
+
if (!existsSync3(CURSOR_MCP_PATH)) return false;
|
|
640
|
+
const config = readCursorMcpJson();
|
|
641
|
+
if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
|
|
642
|
+
let removed = false;
|
|
643
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
644
|
+
if (entry?.[SYNKRO_MARKER3] === true) {
|
|
645
|
+
delete config.mcpServers[name];
|
|
646
|
+
removed = true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (!removed) return false;
|
|
650
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
651
|
+
writeCursorMcpJsonAtomic(config);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH, CURSOR_MCP_PATH;
|
|
593
655
|
var init_mcpConfig = __esm({
|
|
594
656
|
"cli/installer/mcpConfig.ts"() {
|
|
595
657
|
"use strict";
|
|
596
658
|
SYNKRO_MARKER3 = "__synkro_managed__";
|
|
597
659
|
SYNKRO_SERVER_NAME = "synkro-guardrails";
|
|
598
660
|
CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
|
|
661
|
+
CURSOR_MCP_PATH = join2(homedir3(), ".cursor", "mcp.json");
|
|
599
662
|
}
|
|
600
663
|
});
|
|
601
664
|
|
|
@@ -1778,6 +1841,17 @@ function cursorHookExit(): never {
|
|
|
1778
1841
|
|
|
1779
1842
|
export function outputJson(obj: any): void {
|
|
1780
1843
|
if (isCursorHookFormat()) {
|
|
1844
|
+
if (obj?.permission === 'allow') {
|
|
1845
|
+
const u = typeof obj.user_message === 'string' ? obj.user_message : '';
|
|
1846
|
+
const a = typeof obj.agent_message === 'string' ? obj.agent_message : u;
|
|
1847
|
+
if (u || a) {
|
|
1848
|
+
if (!cursorHookExited) {
|
|
1849
|
+
cursorHookExited = true;
|
|
1850
|
+
process.stdout.write(JSON.stringify({ permission: 'allow' }) + '\\n');
|
|
1851
|
+
}
|
|
1852
|
+
cursorHookExit();
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1781
1855
|
const hso = obj?.hookSpecificOutput;
|
|
1782
1856
|
const sys = typeof obj?.systemMessage === 'string' ? obj.systemMessage : '';
|
|
1783
1857
|
if (hso?.permissionDecision === 'deny') {
|
|
@@ -1792,11 +1866,12 @@ export function outputJson(obj: any): void {
|
|
|
1792
1866
|
}
|
|
1793
1867
|
cursorHookExit();
|
|
1794
1868
|
}
|
|
1795
|
-
const
|
|
1869
|
+
const addCtx = typeof hso?.additionalContext === 'string' ? hso.additionalContext : '';
|
|
1870
|
+
const ctx = sys || addCtx;
|
|
1796
1871
|
if (ctx) {
|
|
1797
1872
|
if (!cursorHookExited) {
|
|
1798
1873
|
cursorHookExited = true;
|
|
1799
|
-
process.stdout.write(JSON.stringify({
|
|
1874
|
+
process.stdout.write(JSON.stringify({ permission: 'allow' }) + '\\n');
|
|
1800
1875
|
}
|
|
1801
1876
|
cursorHookExit();
|
|
1802
1877
|
}
|
|
@@ -1913,7 +1988,7 @@ async function main() {
|
|
|
1913
1988
|
try {
|
|
1914
1989
|
gradeResp = await localGrade('edit', graderPrompt);
|
|
1915
1990
|
} catch {
|
|
1916
|
-
|
|
1991
|
+
outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 local grader unavailable, skipped' });
|
|
1917
1992
|
return;
|
|
1918
1993
|
}
|
|
1919
1994
|
|
|
@@ -1947,7 +2022,7 @@ async function main() {
|
|
|
1947
2022
|
rulesChecked: config.rules, violatedRules,
|
|
1948
2023
|
ccModel: transcript.ccModel,
|
|
1949
2024
|
});
|
|
1950
|
-
outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 warning: ' + guardReason });
|
|
2025
|
+
outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 warning: ' + guardReason, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge (audit). ' + guardReason } });
|
|
1951
2026
|
return;
|
|
1952
2027
|
}
|
|
1953
2028
|
|
|
@@ -1958,7 +2033,8 @@ async function main() {
|
|
|
1958
2033
|
rulesChecked: config.rules, violatedRules: [],
|
|
1959
2034
|
ccModel: transcript.ccModel,
|
|
1960
2035
|
});
|
|
1961
|
-
|
|
2036
|
+
const passLine = tagStr + ' editGuard ' + fileShort + ' \\u2192 pass: ' + (verdict.reason || 'no policy violations detected');
|
|
2037
|
+
outputJson({ systemMessage: passLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge. ' + (verdict.reason || 'no policy violations detected') } });
|
|
1962
2038
|
return;
|
|
1963
2039
|
}
|
|
1964
2040
|
|
|
@@ -2195,6 +2271,13 @@ async function main() {
|
|
|
2195
2271
|
}, config.captureDepth);
|
|
2196
2272
|
}
|
|
2197
2273
|
|
|
2274
|
+
dispatchCapture(jwt, 'cwe', 'block', verdict.severity || 'high', verdict.category || 'security',
|
|
2275
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2276
|
+
command: 'edit ' + filePath,
|
|
2277
|
+
reasoning: denyDetail,
|
|
2278
|
+
violatedRules: activeCweIds,
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2198
2281
|
outputJson({
|
|
2199
2282
|
systemMessage: cweMsg,
|
|
2200
2283
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
@@ -2210,7 +2293,14 @@ async function main() {
|
|
|
2210
2293
|
status: 'resolved',
|
|
2211
2294
|
}, config.captureDepth);
|
|
2212
2295
|
|
|
2213
|
-
|
|
2296
|
+
dispatchCapture(jwt, 'cwe', 'pass', 'audit', 'clean',
|
|
2297
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2298
|
+
command: 'edit ' + filePath,
|
|
2299
|
+
reasoning: verdict.reason || 'no CWE weaknesses detected',
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
const cleanMsg = cweTag + ' ' + fileShort + ' \\u2192 clean' + (verdict.reason ? ' (' + verdict.reason + ')' : '');
|
|
2303
|
+
outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
|
|
2214
2304
|
return;
|
|
2215
2305
|
}
|
|
2216
2306
|
|
|
@@ -2229,7 +2319,7 @@ main();
|
|
|
2229
2319
|
import {
|
|
2230
2320
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
2231
2321
|
reconstructContent, readStdin, findNearestDeps, log,
|
|
2232
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
|
|
2322
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
2233
2323
|
} from './_synkro-common.ts';
|
|
2234
2324
|
import { basename } from 'node:path';
|
|
2235
2325
|
|
|
@@ -2262,6 +2352,7 @@ async function main() {
|
|
|
2262
2352
|
const toolInput = payload.tool_input || {};
|
|
2263
2353
|
const sessionId = hookSessionId(payload);
|
|
2264
2354
|
const cwd = payload.cwd || '';
|
|
2355
|
+
const gitRepo = detectRepo(cwd || '.');
|
|
2265
2356
|
|
|
2266
2357
|
const filePath = toolInput.file_path || toolInput.notebook_path || toolInput.path || '';
|
|
2267
2358
|
if (!filePath) { outputEmpty(); return; }
|
|
@@ -2356,6 +2447,16 @@ async function main() {
|
|
|
2356
2447
|
const cveMsg = cveTag + ' ' + fileShort + ' \\u2192 ' + count + ' ' + label;
|
|
2357
2448
|
const ctx = 'CVE: ' + top3 + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 upgrade the vulnerable dependencies yourself.';
|
|
2358
2449
|
|
|
2450
|
+
const cveIds = findings.slice(0, 10).map((f: any) =>
|
|
2451
|
+
(f.aliases || []).find((a: string) => a.startsWith('CVE-')) || f.id || 'unknown'
|
|
2452
|
+
);
|
|
2453
|
+
dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
|
|
2454
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2455
|
+
command: 'edit ' + filePath,
|
|
2456
|
+
reasoning: top3,
|
|
2457
|
+
violatedRules: cveIds,
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2359
2460
|
outputJson({
|
|
2360
2461
|
systemMessage: cveMsg,
|
|
2361
2462
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
@@ -2382,7 +2483,7 @@ import {
|
|
|
2382
2483
|
} from './_synkro-common.ts';
|
|
2383
2484
|
|
|
2384
2485
|
const TOP_NPM_PKGS = new Set([
|
|
2385
|
-
'express','react','lodash','
|
|
2486
|
+
'express','react','lodash','chalk','commander','debug','dotenv','webpack',
|
|
2386
2487
|
'typescript','moment','uuid','cors','body-parser','mongoose','jsonwebtoken','bcrypt',
|
|
2387
2488
|
'nodemon','eslint','prettier','jest','mocha','chai','sinon','supertest','request',
|
|
2388
2489
|
'async','bluebird','underscore','ramda','rxjs','socket.io','redis','pg','mysql',
|
|
@@ -2472,6 +2573,7 @@ async function main() {
|
|
|
2472
2573
|
const permissionMode = payload.permission_mode || '';
|
|
2473
2574
|
const transcriptPath = payload.transcript_path || '';
|
|
2474
2575
|
const gitRepo = detectRepo(cwd || '.');
|
|
2576
|
+
const transcript = extractTranscript(transcriptPath);
|
|
2475
2577
|
|
|
2476
2578
|
let command = '';
|
|
2477
2579
|
switch (toolName) {
|
|
@@ -2651,6 +2753,15 @@ async function main() {
|
|
|
2651
2753
|
}, config.captureDepth);
|
|
2652
2754
|
}
|
|
2653
2755
|
|
|
2756
|
+
const cveIds = findings.map((f: any) => f.cve || f.id || f.package);
|
|
2757
|
+
dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
|
|
2758
|
+
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
2759
|
+
command,
|
|
2760
|
+
reasoning: top3,
|
|
2761
|
+
violatedRules: cveIds,
|
|
2762
|
+
ccModel: transcript.ccModel,
|
|
2763
|
+
});
|
|
2764
|
+
|
|
2654
2765
|
outputJson({
|
|
2655
2766
|
systemMessage: cveMsg,
|
|
2656
2767
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
@@ -2671,7 +2782,6 @@ async function main() {
|
|
|
2671
2782
|
}
|
|
2672
2783
|
}
|
|
2673
2784
|
|
|
2674
|
-
const transcript = extractTranscript(transcriptPath);
|
|
2675
2785
|
const lastPrompt = readLastPrompt();
|
|
2676
2786
|
|
|
2677
2787
|
const config = await loadConfig(jwt);
|
|
@@ -2698,7 +2808,7 @@ async function main() {
|
|
|
2698
2808
|
try {
|
|
2699
2809
|
gradeResp = await localGrade('bash', graderPrompt);
|
|
2700
2810
|
} catch {
|
|
2701
|
-
|
|
2811
|
+
outputJson({ systemMessage: tagStr + ' bashGuard \\u2192 local grader unavailable, skipped' });
|
|
2702
2812
|
return;
|
|
2703
2813
|
}
|
|
2704
2814
|
|
|
@@ -2879,7 +2989,7 @@ async function main() {
|
|
|
2879
2989
|
try {
|
|
2880
2990
|
gradeResp = await localGrade('bash', graderPrompt);
|
|
2881
2991
|
} catch {
|
|
2882
|
-
|
|
2992
|
+
outputJson({ systemMessage: tagStr + ' agentGuard \\u2192 local grader unavailable, skipped' });
|
|
2883
2993
|
return;
|
|
2884
2994
|
}
|
|
2885
2995
|
|
|
@@ -3069,7 +3179,7 @@ async function main() {
|
|
|
3069
3179
|
try {
|
|
3070
3180
|
gradeResp = await localGrade('plan', graderPrompt);
|
|
3071
3181
|
} catch {
|
|
3072
|
-
|
|
3182
|
+
outputJson({ systemMessage: tagStr + ' planReview \\u2192 local grader unavailable, skipped' });
|
|
3073
3183
|
return;
|
|
3074
3184
|
}
|
|
3075
3185
|
|
|
@@ -3080,7 +3190,8 @@ async function main() {
|
|
|
3080
3190
|
if (!verdict.ok) {
|
|
3081
3191
|
const reviewMsg = (verdict.ruleId ? '(first: ' + verdict.ruleId + ') ' : '') + (verdict.reason || 'check org rules during implementation');
|
|
3082
3192
|
appendReviewToPlan(planFile, '\\u26a0\\ufe0f Advisory \\u2014 ' + reviewMsg);
|
|
3083
|
-
|
|
3193
|
+
const advLine = tagStr + ' planReview \\u2192 ' + reviewMsg;
|
|
3194
|
+
outputJson({ systemMessage: advLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge (advisory). ' + reviewMsg } });
|
|
3084
3195
|
dispatchCapture(jwt, 'plan_review', 'advisory', verdict.severity || 'medium', verdict.category || 'general',
|
|
3085
3196
|
'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
|
|
3086
3197
|
command: planContent, reasoning: verdict.reason || 'check org rules',
|
|
@@ -3089,7 +3200,8 @@ async function main() {
|
|
|
3089
3200
|
} else {
|
|
3090
3201
|
const reviewMsg = verdict.reason || 'no relevant org rules for this plan';
|
|
3091
3202
|
appendReviewToPlan(planFile, '\\u2705 Clean \\u2014 ' + reviewMsg);
|
|
3092
|
-
|
|
3203
|
+
const cleanLine = tagStr + ' planReview \\u2192 clean: ' + reviewMsg;
|
|
3204
|
+
outputJson({ systemMessage: cleanLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge. ' + reviewMsg } });
|
|
3093
3205
|
dispatchCapture(jwt, 'plan_review', 'clean', 'audit', verdict.category || 'general',
|
|
3094
3206
|
'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
|
|
3095
3207
|
command: planContent, reasoning: reviewMsg,
|
|
@@ -3559,6 +3671,27 @@ import {
|
|
|
3559
3671
|
extractTranscript, readLastPrompt, log, GATEWAY_URL,
|
|
3560
3672
|
type Rule,
|
|
3561
3673
|
} from './_synkro-common.ts';
|
|
3674
|
+
import { createHash } from 'node:crypto';
|
|
3675
|
+
import { existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3676
|
+
|
|
3677
|
+
const DEDUP_DIR = process.env.HOME + '/.synkro/.dedup';
|
|
3678
|
+
const DEDUP_TTL_MS = 3000;
|
|
3679
|
+
|
|
3680
|
+
function isDuplicate(command: string, sessionId: string): boolean {
|
|
3681
|
+
const hash = createHash('md5').update(sessionId + ':' + command).digest('hex').slice(0, 12);
|
|
3682
|
+
const marker = DEDUP_DIR + '/' + hash;
|
|
3683
|
+
try {
|
|
3684
|
+
if (existsSync(marker)) {
|
|
3685
|
+
const age = Date.now() - statSync(marker).mtimeMs;
|
|
3686
|
+
if (age < DEDUP_TTL_MS) return true;
|
|
3687
|
+
}
|
|
3688
|
+
} catch {}
|
|
3689
|
+
try {
|
|
3690
|
+
mkdirSync(DEDUP_DIR, { recursive: true });
|
|
3691
|
+
writeFileSync(marker, '', { flag: 'w' });
|
|
3692
|
+
} catch {}
|
|
3693
|
+
return false;
|
|
3694
|
+
}
|
|
3562
3695
|
|
|
3563
3696
|
// Cursor beforeShellExecution timeout is 15s; stay under it (JWT refresh + grade).
|
|
3564
3697
|
const CURSOR_GRADE_TIMEOUT_MS = 7500;
|
|
@@ -3583,7 +3716,11 @@ function finishWith(payload: Record<string, unknown>): never {
|
|
|
3583
3716
|
process.on('SIGTERM', () => finishAllow());
|
|
3584
3717
|
|
|
3585
3718
|
const SHELL_TOOL_NAMES = new Set(['Bash', 'Shell', 'terminal', 'run_terminal_cmd', 'execute_command']);
|
|
3586
|
-
const
|
|
3719
|
+
const READ_TOOL_NAMES = new Set(['Read', 'ReadFile', 'read_file']);
|
|
3720
|
+
const SEARCH_TOOL_NAMES = new Set(['Grep', 'grep_search', 'codebase_search', 'file_search']);
|
|
3721
|
+
const DIR_TOOL_NAMES = new Set(['Glob', 'list_dir']);
|
|
3722
|
+
const DELETE_TOOL_NAMES = new Set(['delete_file']);
|
|
3723
|
+
const BASH_PRE_TOOL_NAMES = new Set([...SHELL_TOOL_NAMES, ...READ_TOOL_NAMES, ...SEARCH_TOOL_NAMES, ...DIR_TOOL_NAMES, ...DELETE_TOOL_NAMES]);
|
|
3587
3724
|
|
|
3588
3725
|
function extractCommand(payload: Record<string, unknown>): { command: string; toolName: string } {
|
|
3589
3726
|
const direct = typeof payload.command === 'string' ? payload.command : '';
|
|
@@ -3597,23 +3734,16 @@ function extractCommand(payload: Record<string, unknown>): { command: string; to
|
|
|
3597
3734
|
: {};
|
|
3598
3735
|
|
|
3599
3736
|
let command = '';
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
break;
|
|
3611
|
-
case 'Grep':
|
|
3612
|
-
command = "grep -r '" + String(toolInput.pattern ?? '') + "' " + String(toolInput.path ?? '.');
|
|
3613
|
-
break;
|
|
3614
|
-
case 'Glob':
|
|
3615
|
-
command = "find . -name '" + String(toolInput.pattern ?? '') + "'";
|
|
3616
|
-
break;
|
|
3737
|
+
if (SHELL_TOOL_NAMES.has(toolName)) {
|
|
3738
|
+
command = String(toolInput.command ?? '');
|
|
3739
|
+
} else if (READ_TOOL_NAMES.has(toolName)) {
|
|
3740
|
+
command = 'cat ' + String(toolInput.file_path ?? toolInput.path ?? '');
|
|
3741
|
+
} else if (SEARCH_TOOL_NAMES.has(toolName)) {
|
|
3742
|
+
command = "grep -r '" + String(toolInput.pattern ?? toolInput.query ?? '') + "' " + String(toolInput.path ?? '.');
|
|
3743
|
+
} else if (DIR_TOOL_NAMES.has(toolName)) {
|
|
3744
|
+
command = "find . -name '" + String(toolInput.pattern ?? toolInput.relative_workspace_path ?? '') + "'";
|
|
3745
|
+
} else if (DELETE_TOOL_NAMES.has(toolName)) {
|
|
3746
|
+
command = 'rm ' + String(toolInput.target_file ?? toolInput.file_path ?? toolInput.path ?? '');
|
|
3617
3747
|
}
|
|
3618
3748
|
return { command, toolName: toolName || 'Bash' };
|
|
3619
3749
|
}
|
|
@@ -3629,7 +3759,16 @@ async function main() {
|
|
|
3629
3759
|
|
|
3630
3760
|
const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
|
|
3631
3761
|
const sessionId = String(payload.conversation_id ?? payload.session_id ?? '');
|
|
3762
|
+
|
|
3763
|
+
if (isDuplicate(command, sessionId)) {
|
|
3764
|
+
log('bashGuard skip (dedup): ' + command.slice(0, 80));
|
|
3765
|
+
finishAllow();
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3632
3768
|
const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
|
|
3769
|
+
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
3770
|
+
const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
|
|
3771
|
+
const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : 'cursor';
|
|
3633
3772
|
const repo = detectRepo(cwd || '.');
|
|
3634
3773
|
|
|
3635
3774
|
const cmdShort = command.slice(0, 80);
|
|
@@ -3669,7 +3808,7 @@ async function main() {
|
|
|
3669
3808
|
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS);
|
|
3670
3809
|
} catch (e) {
|
|
3671
3810
|
log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
|
|
3672
|
-
|
|
3811
|
+
finishWith({ permission: 'allow' });
|
|
3673
3812
|
}
|
|
3674
3813
|
|
|
3675
3814
|
const verdict = parseVerdict(gradeResp);
|
|
@@ -3680,9 +3819,10 @@ async function main() {
|
|
|
3680
3819
|
|
|
3681
3820
|
if (mode !== 'audit') {
|
|
3682
3821
|
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3683
|
-
'Bash',
|
|
3822
|
+
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
3684
3823
|
command, reasoning: guardReason,
|
|
3685
3824
|
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3825
|
+
ccModel: model,
|
|
3686
3826
|
});
|
|
3687
3827
|
finishWith({
|
|
3688
3828
|
permission: 'deny',
|
|
@@ -3692,20 +3832,25 @@ async function main() {
|
|
|
3692
3832
|
}
|
|
3693
3833
|
|
|
3694
3834
|
dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
3695
|
-
'Bash',
|
|
3835
|
+
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
3696
3836
|
command, reasoning: guardReason,
|
|
3697
3837
|
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3838
|
+
ccModel: model,
|
|
3698
3839
|
});
|
|
3840
|
+
log('bashGuard ' + cmdShort + ' \u2192 audit warning');
|
|
3841
|
+
finishWith({ permission: 'allow' });
|
|
3699
3842
|
} else {
|
|
3700
3843
|
dispatchCapture(jwt, 'bash', 'pass', 'audit', verdict.category || 'clean',
|
|
3701
|
-
'Bash',
|
|
3844
|
+
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
3702
3845
|
command, reasoning: verdict.reason || 'no policy violations detected',
|
|
3703
3846
|
rulesChecked: config.rules, violatedRules: [],
|
|
3847
|
+
ccModel: model,
|
|
3704
3848
|
});
|
|
3705
3849
|
}
|
|
3706
3850
|
|
|
3707
|
-
|
|
3708
|
-
|
|
3851
|
+
const passReason = verdict.reason || 'no policy violations detected';
|
|
3852
|
+
log('bashGuard ' + cmdShort + ' \u2192 pass: ' + passReason);
|
|
3853
|
+
finishWith({ permission: 'allow' });
|
|
3709
3854
|
}
|
|
3710
3855
|
|
|
3711
3856
|
const body: Record<string, any> = {
|
|
@@ -3730,7 +3875,15 @@ async function main() {
|
|
|
3730
3875
|
}
|
|
3731
3876
|
|
|
3732
3877
|
if (resp.hook_response) {
|
|
3733
|
-
|
|
3878
|
+
const hr = resp.hook_response as Record<string, unknown>;
|
|
3879
|
+
if (hr.permission === 'allow') {
|
|
3880
|
+
const um = String(hr.user_message || '');
|
|
3881
|
+
const am = String(hr.agent_message || um);
|
|
3882
|
+
if (um || am) {
|
|
3883
|
+
finishWith({ permission: 'allow' });
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
finishWith(hr);
|
|
3734
3887
|
}
|
|
3735
3888
|
log('bashGuard ' + cmdShort + ' \u2192 pass (no hook_response)');
|
|
3736
3889
|
finishAllow();
|
|
@@ -6972,7 +7125,7 @@ function writeConfigEnv(opts) {
|
|
|
6972
7125
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6973
7126
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6974
7127
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6975
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
7128
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.69")}`
|
|
6976
7129
|
];
|
|
6977
7130
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6978
7131
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7494,6 +7647,36 @@ async function installCommand(opts = {}) {
|
|
|
7494
7647
|
}
|
|
7495
7648
|
}
|
|
7496
7649
|
}
|
|
7650
|
+
if (hasCursor && !opts.noMcp) {
|
|
7651
|
+
try {
|
|
7652
|
+
if (useLocalMcp) {
|
|
7653
|
+
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
|
|
7654
|
+
console.log(`Registered local MCP guardrails server in ${mcp.path}`);
|
|
7655
|
+
console.log(` url: ${mcp.url}`);
|
|
7656
|
+
} else {
|
|
7657
|
+
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
7658
|
+
method: "POST",
|
|
7659
|
+
headers: {
|
|
7660
|
+
"Authorization": `Bearer ${token}`,
|
|
7661
|
+
"Content-Type": "application/json"
|
|
7662
|
+
},
|
|
7663
|
+
body: "{}"
|
|
7664
|
+
});
|
|
7665
|
+
if (!mintResp.ok) {
|
|
7666
|
+
const errText = await mintResp.text().catch(() => "");
|
|
7667
|
+
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
7668
|
+
}
|
|
7669
|
+
const minted = await mintResp.json();
|
|
7670
|
+
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
7671
|
+
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
7672
|
+
console.log(` url: ${mcp.url}`);
|
|
7673
|
+
}
|
|
7674
|
+
console.log();
|
|
7675
|
+
} catch (err) {
|
|
7676
|
+
console.warn(` \u26A0 Cursor MCP registration failed: ${err.message}`);
|
|
7677
|
+
console.log();
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7497
7680
|
const priorLocalFlag = (() => {
|
|
7498
7681
|
try {
|
|
7499
7682
|
const content = readFileSync10(CONFIG_PATH3, "utf-8");
|
|
@@ -9072,7 +9255,11 @@ function disconnectCommand(args2 = []) {
|
|
|
9072
9255
|
}
|
|
9073
9256
|
if (sawClaudeCode) {
|
|
9074
9257
|
const mcpRemoved = uninstallMcpConfig();
|
|
9075
|
-
console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails
|
|
9258
|
+
console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (CC): ${mcpRemoved ? "removed from ~/.claude.json" : "no entry found"}`);
|
|
9259
|
+
}
|
|
9260
|
+
{
|
|
9261
|
+
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
9262
|
+
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
9076
9263
|
}
|
|
9077
9264
|
if (purge) {
|
|
9078
9265
|
if (existsSync14(SYNKRO_DIR5)) {
|