@synkro-sh/cli 1.6.14 → 1.6.16
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 +246 -99
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -116,6 +116,17 @@ function installCCHooks(settingsPath, config) {
|
|
|
116
116
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd ?? [];
|
|
117
117
|
settings.hooks.SessionStart = settings.hooks.SessionStart ?? [];
|
|
118
118
|
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit ?? [];
|
|
119
|
+
settings.hooks.PreToolUse.push({
|
|
120
|
+
matcher: "Bash",
|
|
121
|
+
hooks: [
|
|
122
|
+
{
|
|
123
|
+
type: "command",
|
|
124
|
+
command: config.installScanScriptPath,
|
|
125
|
+
timeout: 8
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
[SYNKRO_MARKER]: true
|
|
129
|
+
});
|
|
119
130
|
settings.hooks.PreToolUse.push({
|
|
120
131
|
matcher: "Bash|Read|Grep|Glob",
|
|
121
132
|
hooks: [
|
|
@@ -319,6 +330,12 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
319
330
|
pushCcHook(h, "beforeSubmitPrompt", config.userPromptSubmitScriptPath, { timeout: 5 });
|
|
320
331
|
pushCcHook(h, "stop", config.transcriptSyncScriptPath, { timeout: 3 });
|
|
321
332
|
h.beforeShellExecution = h.beforeShellExecution ?? [];
|
|
333
|
+
h.beforeShellExecution.push({
|
|
334
|
+
command: cursorCcCmd(config.installScanScriptPath),
|
|
335
|
+
timeout: 8,
|
|
336
|
+
failClosed: false,
|
|
337
|
+
[SYNKRO_MARKER2]: true
|
|
338
|
+
});
|
|
322
339
|
h.beforeShellExecution.push({
|
|
323
340
|
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
324
341
|
timeout: 15,
|
|
@@ -327,6 +344,13 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
327
344
|
});
|
|
328
345
|
pushCcHook(h, "afterShellExecution", config.bashFollowupScriptPath, { timeout: 10 });
|
|
329
346
|
h.preToolUse = h.preToolUse ?? [];
|
|
347
|
+
h.preToolUse.push({
|
|
348
|
+
command: cursorCcCmd(config.installScanScriptPath),
|
|
349
|
+
timeout: 8,
|
|
350
|
+
failClosed: false,
|
|
351
|
+
matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command",
|
|
352
|
+
[SYNKRO_MARKER2]: true
|
|
353
|
+
});
|
|
330
354
|
h.preToolUse.push({
|
|
331
355
|
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
332
356
|
timeout: 15,
|
|
@@ -727,7 +751,7 @@ synkro_post_with_retry() {
|
|
|
727
751
|
});
|
|
728
752
|
|
|
729
753
|
// cli/installer/hookScriptsTs.ts
|
|
730
|
-
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS;
|
|
754
|
+
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS;
|
|
731
755
|
var init_hookScriptsTs = __esm({
|
|
732
756
|
"cli/installer/hookScriptsTs.ts"() {
|
|
733
757
|
"use strict";
|
|
@@ -975,12 +999,19 @@ export function normalizeMode(m?: string): 'ask' | 'fix' {
|
|
|
975
999
|
|
|
976
1000
|
// \u2500\u2500\u2500 Config Loading \u2500\u2500\u2500
|
|
977
1001
|
|
|
1002
|
+
export interface RuleExample {
|
|
1003
|
+
text: string;
|
|
1004
|
+
verdict: 'violation' | 'ok';
|
|
1005
|
+
note?: string;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
978
1008
|
export interface Rule {
|
|
979
1009
|
rule_id: string;
|
|
980
1010
|
text: string;
|
|
981
1011
|
severity: string;
|
|
982
1012
|
category: string;
|
|
983
1013
|
mode: string;
|
|
1014
|
+
examples?: RuleExample[];
|
|
984
1015
|
}
|
|
985
1016
|
|
|
986
1017
|
export interface HookConfig {
|
|
@@ -1032,6 +1063,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1032
1063
|
severity: r.severity || '',
|
|
1033
1064
|
category: r.category || '',
|
|
1034
1065
|
mode: normalizeMode(r.mode),
|
|
1066
|
+
examples: Array.isArray(r.examples) ? r.examples : undefined,
|
|
1035
1067
|
}));
|
|
1036
1068
|
}
|
|
1037
1069
|
config.silent = raw.silent === true;
|
|
@@ -1072,6 +1104,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1072
1104
|
severity: r.severity || '',
|
|
1073
1105
|
category: r.category || '',
|
|
1074
1106
|
mode: normalizeMode(r.mode),
|
|
1107
|
+
examples: Array.isArray(r.examples) ? r.examples : undefined,
|
|
1075
1108
|
}));
|
|
1076
1109
|
}
|
|
1077
1110
|
} catch {}
|
|
@@ -1322,9 +1355,15 @@ export async function runInstallScan(command: string, jwt: string): Promise<Inst
|
|
|
1322
1355
|
const summary = scanResp?.summary || '';
|
|
1323
1356
|
const scannedLabel = pkgResults.map((p: any) => p.name + '@' + p.version).join(', ');
|
|
1324
1357
|
if (action === 'block') {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1358
|
+
// Every critical/high signal (uncapped) + the true CVE total. The grader
|
|
1359
|
+
// only sees what we put in blockContext \u2014 so the real count must be
|
|
1360
|
+
// STATED here; a bare preview lets it under-report (the count is data,
|
|
1361
|
+
// not something the grader should infer from a truncated list).
|
|
1362
|
+
const highSignals = pkgResults
|
|
1363
|
+
.flatMap((p: any) => (p.signals || []).filter((s: any) => s.severity === 'critical' || s.severity === 'high'));
|
|
1364
|
+
const cveCount = pkgResults
|
|
1365
|
+
.flatMap((p: any) => (p.signals || []))
|
|
1366
|
+
.filter((s: any) => s.type === 'cve').length;
|
|
1328
1367
|
const findings: InstallScanResult['findings'] = [];
|
|
1329
1368
|
for (const p of pkgResults) {
|
|
1330
1369
|
for (const s of (p.signals || [])) {
|
|
@@ -1337,10 +1376,20 @@ export async function runInstallScan(command: string, jwt: string): Promise<Inst
|
|
|
1337
1376
|
}
|
|
1338
1377
|
}
|
|
1339
1378
|
}
|
|
1340
|
-
|
|
1379
|
+
// Preview the top 5 detail lines; the headline carries the true total.
|
|
1380
|
+
const blockSignals = highSignals.slice(0, 5);
|
|
1381
|
+
const headline = cveCount > 0
|
|
1382
|
+
? cveCount + ' known CVE' + (cveCount === 1 ? '' : 's') + ' found in ' + (scannedLabel || 'the requested install') + '.\\n'
|
|
1383
|
+
: '';
|
|
1384
|
+
const preview = blockSignals.map((s: any) => s.detail).join('\\n') || summary;
|
|
1385
|
+
const more = highSignals.length > blockSignals.length
|
|
1386
|
+
? '\\n(+' + (highSignals.length - blockSignals.length) + ' more critical/high findings not shown)'
|
|
1387
|
+
: '';
|
|
1341
1388
|
return {
|
|
1342
1389
|
scanned: true, action: 'block',
|
|
1343
|
-
blockContext:
|
|
1390
|
+
blockContext: headline + preview + more
|
|
1391
|
+
+ '\\nReport the CVE count and fix version exactly as stated above \u2014 do not estimate.'
|
|
1392
|
+
+ '\\nDo NOT install packages with security risks. Use a patched version or a different package.',
|
|
1344
1393
|
summary, scannedLabel, findings,
|
|
1345
1394
|
violatedIds: blockSignals.map((s: any) => s.type + ':' + (s.detail || '').slice(0, 40)),
|
|
1346
1395
|
};
|
|
@@ -2255,6 +2304,8 @@ import {
|
|
|
2255
2304
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2256
2305
|
import { basename, dirname, join } from 'node:path';
|
|
2257
2306
|
|
|
2307
|
+
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
2308
|
+
|
|
2258
2309
|
async function main() {
|
|
2259
2310
|
setupCursorHookSignals();
|
|
2260
2311
|
try {
|
|
@@ -2346,7 +2397,7 @@ async function main() {
|
|
|
2346
2397
|
|
|
2347
2398
|
let gradeResp: string;
|
|
2348
2399
|
try {
|
|
2349
|
-
gradeResp = await localGrade('edit', graderPrompt);
|
|
2400
|
+
gradeResp = await localGrade('edit', graderPrompt, undefined, agentKind);
|
|
2350
2401
|
} catch (err) {
|
|
2351
2402
|
logGraderUnavailable('editGuard', fileShort, (err as Error).message || String(err));
|
|
2352
2403
|
outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 local grader unavailable, skipped' });
|
|
@@ -2467,6 +2518,7 @@ import {
|
|
|
2467
2518
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
2468
2519
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2469
2520
|
|
|
2521
|
+
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
2470
2522
|
|
|
2471
2523
|
interface PackageCapability {
|
|
2472
2524
|
name: string;
|
|
@@ -2727,8 +2779,8 @@ async function main() {
|
|
|
2727
2779
|
const chunk2 = cweContent.slice(mid - OVERLAP);
|
|
2728
2780
|
try {
|
|
2729
2781
|
const [resp1, resp2] = await Promise.all([
|
|
2730
|
-
localGradeCwe(buildCwePrompt(chunk1)),
|
|
2731
|
-
localGradeCwe(buildCwePrompt(chunk2)),
|
|
2782
|
+
localGradeCwe(buildCwePrompt(chunk1), agentKind),
|
|
2783
|
+
localGradeCwe(buildCwePrompt(chunk2), agentKind),
|
|
2732
2784
|
]);
|
|
2733
2785
|
gradeResponses = [resp1, resp2];
|
|
2734
2786
|
} catch (gradeErr: any) {
|
|
@@ -2739,7 +2791,7 @@ async function main() {
|
|
|
2739
2791
|
}
|
|
2740
2792
|
} else {
|
|
2741
2793
|
try {
|
|
2742
|
-
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent))];
|
|
2794
|
+
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), agentKind)];
|
|
2743
2795
|
} catch (gradeErr: any) {
|
|
2744
2796
|
const reason = gradeErr?.message || String(gradeErr);
|
|
2745
2797
|
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
@@ -3100,6 +3152,96 @@ async function main() {
|
|
|
3100
3152
|
}
|
|
3101
3153
|
}
|
|
3102
3154
|
|
|
3155
|
+
main();
|
|
3156
|
+
`;
|
|
3157
|
+
INSTALL_SCAN_TS = `#!/usr/bin/env bun
|
|
3158
|
+
import {
|
|
3159
|
+
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
3160
|
+
readStdin, runInstallScan, dispatchFinding, dispatchCapture, hashCommand,
|
|
3161
|
+
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
|
|
3162
|
+
} from './_synkro-common.ts';
|
|
3163
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
3164
|
+
import { join } from 'node:path';
|
|
3165
|
+
|
|
3166
|
+
const SCAN_CACHE_DIR = (process.env.HOME || '/tmp') + '/.synkro/.scan-cache';
|
|
3167
|
+
|
|
3168
|
+
async function main() {
|
|
3169
|
+
setupCursorHookSignals();
|
|
3170
|
+
try {
|
|
3171
|
+
const input = await readStdin();
|
|
3172
|
+
if (!input.trim()) { outputEmpty(); return; }
|
|
3173
|
+
|
|
3174
|
+
const payload = JSON.parse(input);
|
|
3175
|
+
const toolName = payload.tool_name || '';
|
|
3176
|
+
if (!isShellTool(toolName)) { outputEmpty(); return; }
|
|
3177
|
+
|
|
3178
|
+
const toolInput = payload.tool_input || {};
|
|
3179
|
+
const command = typeof payload.command === 'string' ? payload.command : (toolInput.command || '');
|
|
3180
|
+
if (!command) { outputEmpty(); return; }
|
|
3181
|
+
|
|
3182
|
+
let jwt = loadJwt();
|
|
3183
|
+
if (!jwt) { outputEmpty(); return; }
|
|
3184
|
+
jwt = await ensureFreshJwt(jwt);
|
|
3185
|
+
|
|
3186
|
+
const scan = await runInstallScan(command, jwt);
|
|
3187
|
+
|
|
3188
|
+
if (scan.scanned) {
|
|
3189
|
+
try {
|
|
3190
|
+
mkdirSync(SCAN_CACHE_DIR, { recursive: true });
|
|
3191
|
+
writeFileSync(join(SCAN_CACHE_DIR, hashCommand(command)), JSON.stringify(scan), 'utf-8');
|
|
3192
|
+
} catch {}
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
if (!scan.scanned) { outputEmpty(); return; }
|
|
3196
|
+
|
|
3197
|
+
const sessionId = hookSessionId(payload);
|
|
3198
|
+
const cwd = payload.cwd || '';
|
|
3199
|
+
const repo = detectRepo(cwd);
|
|
3200
|
+
const config = await loadConfig(jwt);
|
|
3201
|
+
const rt = await route(config);
|
|
3202
|
+
const tagStr = tag(rt, config);
|
|
3203
|
+
|
|
3204
|
+
if (scan.action === 'block') {
|
|
3205
|
+
for (const f of scan.findings) {
|
|
3206
|
+
dispatchFinding(jwt, {
|
|
3207
|
+
session_id: sessionId,
|
|
3208
|
+
file_path: command,
|
|
3209
|
+
finding_type: 'cve' as const,
|
|
3210
|
+
finding_id: f.advisoryId + ':' + f.name,
|
|
3211
|
+
severity: f.severity,
|
|
3212
|
+
status: 'open',
|
|
3213
|
+
detail: f.detail,
|
|
3214
|
+
package_name: f.name,
|
|
3215
|
+
package_version: f.version,
|
|
3216
|
+
}, config.captureDepth);
|
|
3217
|
+
}
|
|
3218
|
+
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
3219
|
+
'Bash', repo, sessionId, config.captureDepth, {
|
|
3220
|
+
command, reasoning: scan.blockContext.slice(0, 200),
|
|
3221
|
+
violatedRules: scan.violatedIds,
|
|
3222
|
+
});
|
|
3223
|
+
outputJson({
|
|
3224
|
+
systemMessage: '[synkro:installScan] ' + scan.summary,
|
|
3225
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '[synkro:installScan] ' + scan.summary },
|
|
3226
|
+
});
|
|
3227
|
+
} else if (scan.action === 'warn') {
|
|
3228
|
+
outputJson({
|
|
3229
|
+
systemMessage: '[synkro:installScan] ' + scan.summary,
|
|
3230
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '[synkro:installScan] ' + scan.summary },
|
|
3231
|
+
});
|
|
3232
|
+
} else {
|
|
3233
|
+
const label = scan.scannedLabel || command.slice(0, 80);
|
|
3234
|
+
outputJson({
|
|
3235
|
+
systemMessage: '[synkro:installScan] ' + label + ' \\u2192 clean',
|
|
3236
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '[synkro:installScan] ' + label + ' \\u2192 clean' },
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
} catch (err) {
|
|
3240
|
+
process.stderr.write('[synkro] installScan error: ' + String(err) + '\\n');
|
|
3241
|
+
outputEmpty();
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3103
3245
|
main();
|
|
3104
3246
|
`;
|
|
3105
3247
|
BASH_JUDGE_TS = String.raw`#!/usr/bin/env bun
|
|
@@ -3110,9 +3252,43 @@ import {
|
|
|
3110
3252
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3111
3253
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
3112
3254
|
logGraderUnavailable, filterRules, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
3113
|
-
|
|
3255
|
+
hashCommand,
|
|
3114
3256
|
type HookConfig, type Rule,
|
|
3115
3257
|
} from './_synkro-common.ts';
|
|
3258
|
+
import { createHash } from 'node:crypto';
|
|
3259
|
+
import { existsSync, statSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
3260
|
+
import { join } from 'node:path';
|
|
3261
|
+
|
|
3262
|
+
const SCAN_CACHE_DIR = (process.env.HOME || '/tmp') + '/.synkro/.scan-cache';
|
|
3263
|
+
|
|
3264
|
+
function readCachedScan(command: string): any | null {
|
|
3265
|
+
try {
|
|
3266
|
+
const path = join(SCAN_CACHE_DIR, hashCommand(command));
|
|
3267
|
+
if (!existsSync(path)) return null;
|
|
3268
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
3269
|
+
unlinkSync(path);
|
|
3270
|
+
return data;
|
|
3271
|
+
} catch { return null; }
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
const DEDUP_DIR = process.env.HOME + '/.synkro/.dedup';
|
|
3275
|
+
const DEDUP_TTL_MS = 3000;
|
|
3276
|
+
|
|
3277
|
+
function isDuplicate(command: string, sessionId: string): boolean {
|
|
3278
|
+
const hash = createHash('md5').update(sessionId + ':' + command).digest('hex').slice(0, 12);
|
|
3279
|
+
const marker = DEDUP_DIR + '/' + hash;
|
|
3280
|
+
try {
|
|
3281
|
+
if (existsSync(marker)) {
|
|
3282
|
+
const age = Date.now() - statSync(marker).mtimeMs;
|
|
3283
|
+
if (age < DEDUP_TTL_MS) return true;
|
|
3284
|
+
}
|
|
3285
|
+
} catch {}
|
|
3286
|
+
try {
|
|
3287
|
+
mkdirSync(DEDUP_DIR, { recursive: true });
|
|
3288
|
+
writeFileSync(marker, '', { flag: 'w' });
|
|
3289
|
+
} catch {}
|
|
3290
|
+
return false;
|
|
3291
|
+
}
|
|
3116
3292
|
|
|
3117
3293
|
async function main() {
|
|
3118
3294
|
setupCursorHookSignals();
|
|
@@ -3150,6 +3326,12 @@ async function main() {
|
|
|
3150
3326
|
}
|
|
3151
3327
|
if (!command) { outputEmpty(); return; }
|
|
3152
3328
|
|
|
3329
|
+
if (isDuplicate(command, sessionId)) {
|
|
3330
|
+
log('bashGuard skip (dedup): ' + command.slice(0, 80));
|
|
3331
|
+
outputEmpty();
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3153
3335
|
const cmdShort = command.slice(0, 80);
|
|
3154
3336
|
log('bashGuard checking: ' + cmdShort);
|
|
3155
3337
|
|
|
@@ -3195,45 +3377,20 @@ async function main() {
|
|
|
3195
3377
|
return;
|
|
3196
3378
|
}
|
|
3197
3379
|
|
|
3198
|
-
// ─── Install protection:
|
|
3199
|
-
//
|
|
3200
|
-
//
|
|
3201
|
-
//
|
|
3202
|
-
//
|
|
3203
|
-
let installScanMsg = '';
|
|
3380
|
+
// ─── Install protection: read cached scan from the install-scan hook ───
|
|
3381
|
+
// The install-scan hook (INSTALL_SCAN_TS) runs before this hook, calls
|
|
3382
|
+
// runInstallScan(), outputs its own system message, and caches the result.
|
|
3383
|
+
// We just read the cache to feed scanConcern into the grader prompt so
|
|
3384
|
+
// the consent-carryover flow works.
|
|
3204
3385
|
let scanConcern = '';
|
|
3205
3386
|
let scanBlockContext = '';
|
|
3206
3387
|
if (toolName === 'Bash') {
|
|
3207
|
-
const scan =
|
|
3208
|
-
if (scan.action === 'block') {
|
|
3209
|
-
|
|
3210
|
-
dispatchFinding(jwt, {
|
|
3211
|
-
session_id: sessionId,
|
|
3212
|
-
file_path: command,
|
|
3213
|
-
finding_type: 'cve' as const,
|
|
3214
|
-
finding_id: f.advisoryId + ':' + f.name,
|
|
3215
|
-
severity: f.severity,
|
|
3216
|
-
status: 'open',
|
|
3217
|
-
detail: f.detail,
|
|
3218
|
-
package_name: f.name,
|
|
3219
|
-
package_version: f.version,
|
|
3220
|
-
}, config.captureDepth);
|
|
3221
|
-
}
|
|
3222
|
-
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
3223
|
-
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
3224
|
-
command,
|
|
3225
|
-
reasoning: scan.blockContext.slice(0, 200),
|
|
3226
|
-
violatedRules: scan.violatedIds,
|
|
3227
|
-
ccModel: transcript.ccModel,
|
|
3228
|
-
});
|
|
3229
|
-
scanBlockContext = scan.blockContext;
|
|
3388
|
+
const scan = readCachedScan(command);
|
|
3389
|
+
if (scan && scan.action === 'block') {
|
|
3390
|
+
scanBlockContext = scan.blockContext || '';
|
|
3230
3391
|
scanConcern = 'PACKAGE SCANNER FLAG (authoritative — do NOT re-evaluate whether the vulnerability is real): '
|
|
3231
|
-
+
|
|
3392
|
+
+ scanBlockContext
|
|
3232
3393
|
+ ' For this concern you MUST return ok=false with rule_id "SYNKRO_PKGSCAN", rule_mode "ask", and the reason above — UNLESS the user has explicitly consented in this conversation to installing this despite the warning, in which case return ok=true.';
|
|
3233
|
-
} else if (scan.scanned && scan.action === 'warn') {
|
|
3234
|
-
installScanMsg = '[synkro:installScan] ' + scan.summary;
|
|
3235
|
-
} else if (scan.scanned) {
|
|
3236
|
-
installScanMsg = '[synkro:installScan] ' + (scan.scannedLabel || cmdShort) + ' → clean';
|
|
3237
3394
|
}
|
|
3238
3395
|
}
|
|
3239
3396
|
|
|
@@ -3289,10 +3446,9 @@ async function main() {
|
|
|
3289
3446
|
const blockMsg = mode === 'fix'
|
|
3290
3447
|
? tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before retrying — do not ask the user.'
|
|
3291
3448
|
: tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3292
|
-
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
|
|
3293
3449
|
outputJson({
|
|
3294
|
-
systemMessage:
|
|
3295
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext:
|
|
3450
|
+
systemMessage: blockMsg,
|
|
3451
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: blockMsg },
|
|
3296
3452
|
});
|
|
3297
3453
|
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3298
3454
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
@@ -3301,8 +3457,10 @@ async function main() {
|
|
|
3301
3457
|
});
|
|
3302
3458
|
} else {
|
|
3303
3459
|
const reason = tagStr + ' bashGuard → pass: ' + (verdict.reason || 'no policy violations detected');
|
|
3304
|
-
|
|
3305
|
-
|
|
3460
|
+
outputJson({
|
|
3461
|
+
systemMessage: reason,
|
|
3462
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: reason },
|
|
3463
|
+
});
|
|
3306
3464
|
dispatchCapture(jwt, 'bash', 'pass', 'clean', verdict.category || 'trivial_utility',
|
|
3307
3465
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3308
3466
|
command, reasoning: verdict.reason || 'no policy violations detected',
|
|
@@ -3354,30 +3512,16 @@ async function main() {
|
|
|
3354
3512
|
|
|
3355
3513
|
if (!resp) {
|
|
3356
3514
|
log('bashGuard ' + cmdShort + ' → error (timeout)');
|
|
3357
|
-
|
|
3358
|
-
outputJson({ systemMessage: installScanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: installScanMsg } });
|
|
3359
|
-
} else { outputEmpty(); }
|
|
3515
|
+
outputEmpty();
|
|
3360
3516
|
return;
|
|
3361
3517
|
}
|
|
3362
3518
|
|
|
3363
3519
|
if (!resp.hook_response || typeof resp.hook_response !== 'object') {
|
|
3364
3520
|
log('bashGuard ' + cmdShort + ' → pass (no hook_response)');
|
|
3365
|
-
|
|
3366
|
-
outputJson({ systemMessage: installScanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: installScanMsg } });
|
|
3367
|
-
} else { outputEmpty(); }
|
|
3521
|
+
outputEmpty();
|
|
3368
3522
|
return;
|
|
3369
3523
|
}
|
|
3370
3524
|
|
|
3371
|
-
if (installScanMsg) {
|
|
3372
|
-
const existing = resp.hook_response.systemMessage || '';
|
|
3373
|
-
resp.hook_response.systemMessage = installScanMsg + (existing ? '\\n' + existing : '');
|
|
3374
|
-
if (resp.hook_response.hookSpecificOutput) {
|
|
3375
|
-
const existingCtx = resp.hook_response.hookSpecificOutput.additionalContext || '';
|
|
3376
|
-
resp.hook_response.hookSpecificOutput.additionalContext = installScanMsg + (existingCtx ? '\\n' + existingCtx : '');
|
|
3377
|
-
} else {
|
|
3378
|
-
resp.hook_response.hookSpecificOutput = { hookEventName: 'PreToolUse', additionalContext: resp.hook_response.systemMessage };
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
3525
|
outputJson(resp.hook_response);
|
|
3382
3526
|
} catch (err) {
|
|
3383
3527
|
process.stderr.write('[synkro] bashGuard error: ' + String(err) + '\\n');
|
|
@@ -3397,6 +3541,8 @@ import {
|
|
|
3397
3541
|
type HookConfig, type Rule,
|
|
3398
3542
|
} from './_synkro-common.ts';
|
|
3399
3543
|
|
|
3544
|
+
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
3545
|
+
|
|
3400
3546
|
async function main() {
|
|
3401
3547
|
setupCursorHookSignals();
|
|
3402
3548
|
try {
|
|
@@ -3466,7 +3612,7 @@ async function main() {
|
|
|
3466
3612
|
|
|
3467
3613
|
let gradeResp: string;
|
|
3468
3614
|
try {
|
|
3469
|
-
gradeResp = await localGrade('bash', graderPrompt);
|
|
3615
|
+
gradeResp = await localGrade('bash', graderPrompt, undefined, agentKind);
|
|
3470
3616
|
} catch (err) {
|
|
3471
3617
|
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), (err as Error).message || String(err));
|
|
3472
3618
|
outputJson({ systemMessage: tagStr + ' agentGuard \u2192 local grader unavailable, skipped' });
|
|
@@ -3565,6 +3711,8 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from '
|
|
|
3565
3711
|
import { join } from 'node:path';
|
|
3566
3712
|
import { homedir } from 'node:os';
|
|
3567
3713
|
|
|
3714
|
+
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
3715
|
+
|
|
3568
3716
|
function findLatestPlanInDir(plansDir: string): string | null {
|
|
3569
3717
|
if (!existsSync(plansDir)) return null;
|
|
3570
3718
|
try {
|
|
@@ -3656,7 +3804,7 @@ async function main() {
|
|
|
3656
3804
|
|
|
3657
3805
|
let gradeResp: string;
|
|
3658
3806
|
try {
|
|
3659
|
-
gradeResp = await localGrade('plan', graderPrompt);
|
|
3807
|
+
gradeResp = await localGrade('plan', graderPrompt, undefined, agentKind);
|
|
3660
3808
|
} catch {
|
|
3661
3809
|
outputJson({ systemMessage: tagStr + ' planReview \u2192 local grader unavailable, skipped' });
|
|
3662
3810
|
return;
|
|
@@ -4147,13 +4295,26 @@ main();
|
|
|
4147
4295
|
import {
|
|
4148
4296
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4149
4297
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules,
|
|
4150
|
-
isSafeInRepoRead,
|
|
4298
|
+
isSafeInRepoRead, postWithRetry, readStdin, hashCommand,
|
|
4151
4299
|
extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
|
|
4152
4300
|
appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
|
|
4153
4301
|
type Rule,
|
|
4154
4302
|
} from './_synkro-common.ts';
|
|
4155
4303
|
import { createHash } from 'node:crypto';
|
|
4156
|
-
import { existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4304
|
+
import { existsSync, statSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
4305
|
+
import { join } from 'node:path';
|
|
4306
|
+
|
|
4307
|
+
const SCAN_CACHE_DIR = (process.env.HOME || '/tmp') + '/.synkro/.scan-cache';
|
|
4308
|
+
|
|
4309
|
+
function readCachedScan(command: string): any | null {
|
|
4310
|
+
try {
|
|
4311
|
+
const path = join(SCAN_CACHE_DIR, hashCommand(command));
|
|
4312
|
+
if (!existsSync(path)) return null;
|
|
4313
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
4314
|
+
unlinkSync(path);
|
|
4315
|
+
return data;
|
|
4316
|
+
} catch { return null; }
|
|
4317
|
+
}
|
|
4157
4318
|
|
|
4158
4319
|
const DEDUP_DIR = process.env.HOME + '/.synkro/.dedup';
|
|
4159
4320
|
const DEDUP_TTL_MS = 3000;
|
|
@@ -4282,33 +4443,16 @@ async function main() {
|
|
|
4282
4443
|
const rt = await route(config);
|
|
4283
4444
|
const tagStr = tag(rt, config);
|
|
4284
4445
|
|
|
4285
|
-
// Install protection \u2014
|
|
4286
|
-
// A block is handed to the grader as an authoritative concern so the
|
|
4287
|
-
// normal ask + consent-carryover flow can let the user override it.
|
|
4446
|
+
// Install protection \u2014 read cached scan from the install-scan hook.
|
|
4288
4447
|
let scanConcern = '';
|
|
4289
4448
|
let scanBlockContext = '';
|
|
4290
4449
|
if (SHELL_TOOL_NAMES.has(toolName)) {
|
|
4291
|
-
const scan =
|
|
4292
|
-
if (scan.action === 'block') {
|
|
4293
|
-
|
|
4294
|
-
dispatchFinding(jwt, {
|
|
4295
|
-
session_id: sessionId, file_path: command,
|
|
4296
|
-
finding_type: 'cve' as const, finding_id: f.advisoryId + ':' + f.name,
|
|
4297
|
-
severity: f.severity, status: 'open', detail: f.detail,
|
|
4298
|
-
package_name: f.name, package_version: f.version,
|
|
4299
|
-
}, config.captureDepth);
|
|
4300
|
-
}
|
|
4301
|
-
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
4302
|
-
'Bash', repo, sessionId, config.captureDepth, {
|
|
4303
|
-
command, reasoning: scan.blockContext.slice(0, 200),
|
|
4304
|
-
violatedRules: scan.violatedIds, ccModel: model,
|
|
4305
|
-
});
|
|
4306
|
-
scanBlockContext = scan.blockContext;
|
|
4450
|
+
const scan = readCachedScan(command);
|
|
4451
|
+
if (scan && scan.action === 'block') {
|
|
4452
|
+
scanBlockContext = scan.blockContext || '';
|
|
4307
4453
|
scanConcern = 'PACKAGE SCANNER FLAG (authoritative \u2014 do NOT re-evaluate whether the vulnerability is real): '
|
|
4308
|
-
+
|
|
4454
|
+
+ scanBlockContext
|
|
4309
4455
|
+ ' For this concern you MUST return ok=false with rule_id "SYNKRO_PKGSCAN", rule_mode "ask", and the reason above \u2014 UNLESS the user has explicitly consented in this conversation to installing this despite the warning, in which case return ok=true.';
|
|
4310
|
-
} else if (scan.scanned && scan.action === 'warn') {
|
|
4311
|
-
log('bashGuard installScan warn: ' + scan.summary);
|
|
4312
4456
|
}
|
|
4313
4457
|
}
|
|
4314
4458
|
|
|
@@ -5664,8 +5808,6 @@ async function dockerInstall(opts = {}) {
|
|
|
5664
5808
|
`127.0.0.1:${HOST_GRADER_PORT}:8929`,
|
|
5665
5809
|
"-p",
|
|
5666
5810
|
`127.0.0.1:${HOST_CWE_PORT}:8930`,
|
|
5667
|
-
"-p",
|
|
5668
|
-
`127.0.0.1:${HOST_PG_PORT}:5433`,
|
|
5669
5811
|
"-v",
|
|
5670
5812
|
`${PGDATA_PATH}:/data/pgdata`,
|
|
5671
5813
|
"-v",
|
|
@@ -5894,7 +6036,7 @@ function checkPgdata() {
|
|
|
5894
6036
|
if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
|
|
5895
6037
|
return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
|
|
5896
6038
|
}
|
|
5897
|
-
var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT,
|
|
6039
|
+
var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
|
|
5898
6040
|
var init_dockerInstall = __esm({
|
|
5899
6041
|
"cli/local-cc/dockerInstall.ts"() {
|
|
5900
6042
|
"use strict";
|
|
@@ -5908,7 +6050,6 @@ var init_dockerInstall = __esm({
|
|
|
5908
6050
|
HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
|
|
5909
6051
|
HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
|
|
5910
6052
|
HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
|
|
5911
|
-
HOST_PG_PORT = parseInt(process.env.SYNKRO_HOST_PG_PORT || "15433", 10);
|
|
5912
6053
|
CONTAINER_NAME = "synkro-server";
|
|
5913
6054
|
DEFAULT_IMAGE = "ghcr.io/synkro-sh/synkro-server:latest";
|
|
5914
6055
|
DockerInstallError = class extends Error {
|
|
@@ -6424,6 +6565,7 @@ function writeHookScripts() {
|
|
|
6424
6565
|
const userPromptSubmitScriptPath = join8(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
6425
6566
|
const commonScriptPath = join8(HOOKS_DIR, "_synkro-common.ts");
|
|
6426
6567
|
const commonBashScriptPath = join8(HOOKS_DIR, "_synkro-common.sh");
|
|
6568
|
+
const installScanScriptPath = join8(HOOKS_DIR, "cc-install-scan.ts");
|
|
6427
6569
|
const cursorBashJudgePath = join8(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
6428
6570
|
const cursorEditCapturePath = join8(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
6429
6571
|
const mcpStdioProxyPath = join8(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
@@ -6440,6 +6582,7 @@ function writeHookScripts() {
|
|
|
6440
6582
|
writeFileSync7(userPromptSubmitScriptPath, USER_PROMPT_SUBMIT_TS, "utf-8");
|
|
6441
6583
|
writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
|
|
6442
6584
|
writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
|
|
6585
|
+
writeFileSync7(installScanScriptPath, INSTALL_SCAN_TS, "utf-8");
|
|
6443
6586
|
writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
|
|
6444
6587
|
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
6445
6588
|
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
@@ -6456,6 +6599,7 @@ function writeHookScripts() {
|
|
|
6456
6599
|
chmodSync2(userPromptSubmitScriptPath, 493);
|
|
6457
6600
|
chmodSync2(commonScriptPath, 493);
|
|
6458
6601
|
chmodSync2(commonBashScriptPath, 493);
|
|
6602
|
+
chmodSync2(installScanScriptPath, 493);
|
|
6459
6603
|
chmodSync2(cursorBashJudgePath, 493);
|
|
6460
6604
|
chmodSync2(cursorEditCapturePath, 493);
|
|
6461
6605
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
@@ -6471,6 +6615,7 @@ function writeHookScripts() {
|
|
|
6471
6615
|
sessionStartScript: sessionStartScriptPath,
|
|
6472
6616
|
transcriptSyncScript: transcriptSyncScriptPath,
|
|
6473
6617
|
userPromptSubmitScript: userPromptSubmitScriptPath,
|
|
6618
|
+
installScanScript: installScanScriptPath,
|
|
6474
6619
|
cursorBashJudgeScript: cursorBashJudgePath,
|
|
6475
6620
|
cursorEditCaptureScript: cursorEditCapturePath
|
|
6476
6621
|
};
|
|
@@ -6504,7 +6649,7 @@ function writeConfigEnv(opts) {
|
|
|
6504
6649
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6505
6650
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6506
6651
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6507
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
6652
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.16")}`
|
|
6508
6653
|
];
|
|
6509
6654
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6510
6655
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -6746,6 +6891,7 @@ async function installCommand(opts = {}) {
|
|
|
6746
6891
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
6747
6892
|
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
6748
6893
|
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
6894
|
+
installScanScriptPath: scripts.installScanScript,
|
|
6749
6895
|
skipTranscriptSync: !transcriptConsent
|
|
6750
6896
|
});
|
|
6751
6897
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
@@ -6763,7 +6909,8 @@ async function installCommand(opts = {}) {
|
|
|
6763
6909
|
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
6764
6910
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
6765
6911
|
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
6766
|
-
transcriptSyncScriptPath: scripts.transcriptSyncScript
|
|
6912
|
+
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
6913
|
+
installScanScriptPath: scripts.installScanScript
|
|
6767
6914
|
});
|
|
6768
6915
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
6769
6916
|
}
|
|
@@ -9169,7 +9316,7 @@ var args = process.argv.slice(2);
|
|
|
9169
9316
|
var cmd = args[0] || "";
|
|
9170
9317
|
var subArgs = args.slice(1);
|
|
9171
9318
|
function printVersion() {
|
|
9172
|
-
console.log("1.6.
|
|
9319
|
+
console.log("1.6.16");
|
|
9173
9320
|
}
|
|
9174
9321
|
function printHelp2() {
|
|
9175
9322
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|