@synkro-sh/cli 1.6.73 → 1.6.74
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 +176 -10
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1272,7 +1272,7 @@ export function findGitRoot(cwd: string): string {
|
|
|
1272
1272
|
export interface SynkroFileConfig {
|
|
1273
1273
|
version: number;
|
|
1274
1274
|
harness: ('claude-code' | 'cursor')[];
|
|
1275
|
-
grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string };
|
|
1275
|
+
grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string; combinedEditGrade?: boolean };
|
|
1276
1276
|
workers: { claude?: number; cursor?: number };
|
|
1277
1277
|
ruleset: string;
|
|
1278
1278
|
skills: string[];
|
|
@@ -1379,6 +1379,7 @@ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
|
|
|
1379
1379
|
grader: {
|
|
1380
1380
|
pool: normalizePoolToken(parsed.grader?.pool),
|
|
1381
1381
|
mode: ['local', 'byok'].includes(parsed.grader?.mode) ? parsed.grader.mode : undefined,
|
|
1382
|
+
combinedEditGrade: parsed.grader?.combined_edit_grade === true,
|
|
1382
1383
|
},
|
|
1383
1384
|
workers: {
|
|
1384
1385
|
// Counts may live under the grader block (unified form) or the legacy
|
|
@@ -1632,7 +1633,7 @@ export function tag(rt: string, config: HookConfig, grader?: string): string {
|
|
|
1632
1633
|
|
|
1633
1634
|
// \u2500\u2500\u2500 Local Grading (direct channel call) \u2500\u2500\u2500
|
|
1634
1635
|
|
|
1635
|
-
type GradeRole = 'grade-edit' | 'grade-bash' | 'grade-plan' | 'grade-cwe';
|
|
1636
|
+
type GradeRole = 'grade-edit' | 'grade-bash' | 'grade-plan' | 'grade-cwe' | 'grade-edit-cwe';
|
|
1636
1637
|
|
|
1637
1638
|
// Which coding agent fired this grade. The dispatcher routes grades to a
|
|
1638
1639
|
// worker pool of the matching kind so a Cursor grade is judged by Cursor and
|
|
@@ -1642,8 +1643,21 @@ export type AgentKind = 'claude_code' | 'cursor';
|
|
|
1642
1643
|
|
|
1643
1644
|
const ROLE_MAP: Record<string, GradeRole> = {
|
|
1644
1645
|
edit: 'grade-edit', bash: 'grade-bash', plan: 'grade-plan', cwe: 'grade-cwe',
|
|
1646
|
+
'edit-cwe': 'grade-edit-cwe',
|
|
1645
1647
|
};
|
|
1646
1648
|
|
|
1649
|
+
// Combined edit-time grade (org rules + CWE in one inference). OFF by default;
|
|
1650
|
+
// flipped on by env var or synkro.toml [grader] combined_edit_grade = true.
|
|
1651
|
+
// When on: the edit-precheck hook runs ONE grade-edit-cwe call and the
|
|
1652
|
+
// cwe-precheck hook no-ops (the combined call owns CWE) \u2014 halving inferences
|
|
1653
|
+
// per edit. See GRADER_PRIMER_EDIT_CWE.
|
|
1654
|
+
export function combinedEditGradeEnabled(synkroFile?: SynkroFileConfig): boolean {
|
|
1655
|
+
const env = process.env.SYNKRO_COMBINED_EDIT_GRADE;
|
|
1656
|
+
if (env === '1' || env === 'true') return true;
|
|
1657
|
+
if (env === '0' || env === 'false') return false;
|
|
1658
|
+
return synkroFile?.grader?.combinedEditGrade === true;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1647
1661
|
async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port: number, timeoutMs = 30000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1648
1662
|
const body = JSON.stringify({ role, payload: prompt, content: prompt, agent_kind: agentKind });
|
|
1649
1663
|
|
|
@@ -2188,6 +2202,67 @@ export function parseVerdict(resp: string): Verdict {
|
|
|
2188
2202
|
return verdict;
|
|
2189
2203
|
}
|
|
2190
2204
|
|
|
2205
|
+
export interface CombinedFinding {
|
|
2206
|
+
id: string;
|
|
2207
|
+
reason: string;
|
|
2208
|
+
suggestedFix: string;
|
|
2209
|
+
severity: string;
|
|
2210
|
+
ruleMode: string;
|
|
2211
|
+
codeSnippet: string;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Splits a grade-edit-cwe verdict (one <synkro-verdict> whose <violation> blocks
|
|
2215
|
+
// MIX org-rule ids and cwe-### ids) into (a) a rules Verdict the existing rule
|
|
2216
|
+
// enforcement consumes verbatim and (b) a list of CWE findings for the CWE block
|
|
2217
|
+
// path. A block whose rule_id starts with "cwe-" is a CWE finding; anything else
|
|
2218
|
+
// is an org-rule violation (org rules take precedence). Org-rule verdict is ok
|
|
2219
|
+
// UNLESS a non-cwe violation is present \u2014 so a CWE-only failure still passes the
|
|
2220
|
+
// rule gate while blocking on CWE.
|
|
2221
|
+
export function parseCombinedVerdict(resp: string): { ruleVerdict: Verdict; cweFindings: CombinedFinding[] } {
|
|
2222
|
+
const base = parseVerdict(resp);
|
|
2223
|
+
const ruleVerdict: Verdict = { ok: true, reason: '', suggestedFix: '', ruleId: '', ruleMode: '', severity: 'low', category: 'clean' };
|
|
2224
|
+
const cweFindings: CombinedFinding[] = [];
|
|
2225
|
+
const blocks = resp.match(/<violation>[\\s\\S]*?<\\/violation>/g) || [];
|
|
2226
|
+
let firstRule: CombinedFinding | null = null;
|
|
2227
|
+
for (const b of blocks) {
|
|
2228
|
+
const idRaw = (b.match(/<rule_id>([^<]+)<\\/rule_id>/) || [])[1];
|
|
2229
|
+
if (!idRaw) continue;
|
|
2230
|
+
const id = idRaw.trim();
|
|
2231
|
+
const finding: CombinedFinding = {
|
|
2232
|
+
id,
|
|
2233
|
+
reason: ((b.match(/<reason>([\\s\\S]*?)<\\/reason>/) || [])[1] || '').trim(),
|
|
2234
|
+
suggestedFix: ((b.match(/<suggested_fix>([\\s\\S]*?)<\\/suggested_fix>/) || [])[1] || '').trim(),
|
|
2235
|
+
severity: ((b.match(/<severity>([^<]+)<\\/severity>/) || [])[1] || '').trim(),
|
|
2236
|
+
ruleMode: ((b.match(/<rule_mode>([^<]+)<\\/rule_mode>/) || [])[1] || '').trim(),
|
|
2237
|
+
codeSnippet: ((b.match(/<code_snippet>([\\s\\S]*?)<\\/code_snippet>/) || [])[1] || '').replace(/^\\n+|\\n+$/g, ''),
|
|
2238
|
+
};
|
|
2239
|
+
if (/^cwe-/i.test(id)) {
|
|
2240
|
+
finding.id = id.replace(/^cwe-/i, 'CWE-');
|
|
2241
|
+
if (!cweFindings.some(f => f.id === finding.id)) cweFindings.push(finding);
|
|
2242
|
+
} else if (!firstRule) {
|
|
2243
|
+
firstRule = finding;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (firstRule) {
|
|
2247
|
+
ruleVerdict.ok = false;
|
|
2248
|
+
ruleVerdict.ruleId = firstRule.id;
|
|
2249
|
+
ruleVerdict.reason = firstRule.reason;
|
|
2250
|
+
ruleVerdict.suggestedFix = firstRule.suggestedFix;
|
|
2251
|
+
ruleVerdict.ruleMode = firstRule.ruleMode;
|
|
2252
|
+
ruleVerdict.severity = firstRule.severity || 'high';
|
|
2253
|
+
ruleVerdict.category = base.category || 'uncategorized';
|
|
2254
|
+
} else if (base.ok === false && base.ruleId && !/^cwe-/i.test(base.ruleId)) {
|
|
2255
|
+
// No <violation> blocks but a top-level rule_id failure (defensive).
|
|
2256
|
+
ruleVerdict.ok = false;
|
|
2257
|
+
ruleVerdict.ruleId = base.ruleId;
|
|
2258
|
+
ruleVerdict.reason = base.reason;
|
|
2259
|
+
ruleVerdict.suggestedFix = base.suggestedFix;
|
|
2260
|
+
ruleVerdict.ruleMode = base.ruleMode;
|
|
2261
|
+
ruleVerdict.severity = base.severity || 'high';
|
|
2262
|
+
}
|
|
2263
|
+
return { ruleVerdict, cweFindings };
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2191
2266
|
// \u2500\u2500\u2500 Telemetry Dispatch \u2500\u2500\u2500
|
|
2192
2267
|
|
|
2193
2268
|
// Gated cloud telemetry POST \u2014 fires only when storage mode is 'cloud'. The
|
|
@@ -3636,7 +3711,7 @@ export function outputEmpty(): void {
|
|
|
3636
3711
|
EDIT_PRECHECK_TS = `#!/usr/bin/env bun
|
|
3637
3712
|
import {
|
|
3638
3713
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3639
|
-
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
3714
|
+
parseVerdict, parseCombinedVerdict, combinedEditGradeEnabled, dispatchCapture, dispatchFinding, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
3640
3715
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
3641
3716
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3642
3717
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
|
|
@@ -3786,6 +3861,90 @@ async function main() {
|
|
|
3786
3861
|
'The rules shown were pre-selected as the ones relevant to this edit \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no hardcoded secrets in file. R005: in-repo path only." Cover every rule shown.',
|
|
3787
3862
|
].join('\\n');
|
|
3788
3863
|
|
|
3864
|
+
// \u2500\u2500\u2500 Combined org-rules + CWE in ONE inference (SYNKRO_COMBINED_EDIT_GRADE) \u2500\u2500\u2500
|
|
3865
|
+
// Self-contained early-return branch \u2014 the default two-grade path below is
|
|
3866
|
+
// left untouched. Appends the CWE rule set to the rules prompt, runs one
|
|
3867
|
+
// grade-edit-cwe call, then splits the verdict: org-rule violations drive
|
|
3868
|
+
// the same ask/fix enforcement; cwe-### violations drive a fix-mode block.
|
|
3869
|
+
// The cwe-precheck hook no-ops when this flag is on (see CWE_PRECHECK_TS).
|
|
3870
|
+
if (combinedEditGradeEnabled(synkroFile)) {
|
|
3871
|
+
const fileExt = (filePath.match(/\\.([^.\\/]+)$/) || ['', ''])[1].toLowerCase();
|
|
3872
|
+
let cweRules: any[] = [];
|
|
3873
|
+
try {
|
|
3874
|
+
const cr = await fetch(GATEWAY_URL + '/api/v1/cwe-rules?ext=' + encodeURIComponent(fileExt), {
|
|
3875
|
+
headers: { Authorization: 'Bearer ' + jwt },
|
|
3876
|
+
signal: AbortSignal.timeout(4000),
|
|
3877
|
+
});
|
|
3878
|
+
if (cr.ok) cweRules = ((await cr.json() as any) || {}).rules || [];
|
|
3879
|
+
} catch { /* CWE rules optional \u2014 rule grading still runs in the combined pass */ }
|
|
3880
|
+
|
|
3881
|
+
const combinedPrompt = graderPrompt
|
|
3882
|
+
+ '\\n\\nCWE rules to ALSO check the proposed content against (emit cwe-### violations with a <code_snippet>): '
|
|
3883
|
+
+ JSON.stringify(cweRules);
|
|
3884
|
+
|
|
3885
|
+
let cResp: string;
|
|
3886
|
+
try {
|
|
3887
|
+
cResp = await localGrade('edit-cwe', combinedPrompt, undefined, graderPool);
|
|
3888
|
+
} catch (err) {
|
|
3889
|
+
const errMsg = (err as Error).message || String(err);
|
|
3890
|
+
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
3891
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
const { ruleVerdict, cweFindings } = parseCombinedVerdict(cResp);
|
|
3896
|
+
const editContent = 'file=' + filePath + ' content=' + proposed.slice(0, 2000);
|
|
3897
|
+
const violatedRules = ruleVerdict.ruleId ? [ruleVerdict.ruleId] : [];
|
|
3898
|
+
const cweBlock = cweFindings.slice(0, 5)
|
|
3899
|
+
.map(f => '[' + f.id + '] ' + (f.reason || 'weakness') + (f.suggestedFix ? ' Fix: ' + f.suggestedFix : ''))
|
|
3900
|
+
.join('\\n');
|
|
3901
|
+
for (const f of cweFindings) {
|
|
3902
|
+
dispatchFinding(jwt, { session_id: sessionId, file_path: filePath, finding_type: 'cwe', finding_id: f.id, status: 'open' }, config.captureDepth);
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// Org-rule violation \u2192 ask/fix enforcement (CWE findings appended to the deny).
|
|
3906
|
+
if (!ruleVerdict.ok) {
|
|
3907
|
+
const mode = normalizeMode(ruleVerdict.ruleMode || ruleMode(ruleVerdict.ruleId, config.rules));
|
|
3908
|
+
const guardReason = (ruleVerdict.ruleId ? '(' + ruleVerdict.ruleId + ') ' : '') + (ruleVerdict.reason || 'policy violation');
|
|
3909
|
+
let denyReason = mode === 'fix'
|
|
3910
|
+
? 'Guard: ' + guardReason + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
|
|
3911
|
+
: 'Guard: ' + guardReason + '\\nAsk the user for explicit consent before retrying.';
|
|
3912
|
+
if (cweBlock) denyReason += '\\nAlso resolve these CWE weaknesses in code:\\n' + cweBlock;
|
|
3913
|
+
dispatchCapture(jwt, 'edit', 'block', ruleVerdict.severity || 'critical', ruleVerdict.category || 'security',
|
|
3914
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3915
|
+
command: editContent, reasoning: guardReason,
|
|
3916
|
+
rulesChecked: relevantRules, violatedRules,
|
|
3917
|
+
ccModel: captureModel, ...lineMetrics,
|
|
3918
|
+
});
|
|
3919
|
+
outputJson({
|
|
3920
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason + (cweFindings.length ? ' (+' + cweFindings.length + ' CWE)' : ''),
|
|
3921
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
|
|
3922
|
+
});
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// Rules clean but CWE weakness(es) \u2192 fix-mode block (resolve in code).
|
|
3927
|
+
if (cweFindings.length > 0) {
|
|
3928
|
+
const ctx = 'CWE: ' + cweBlock + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
|
|
3929
|
+
outputJson({
|
|
3930
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked (' + cweFindings.map(f => f.id).join(', ') + ')',
|
|
3931
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3932
|
+
});
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
// Both clean.
|
|
3937
|
+
dispatchCapture(jwt, 'edit', 'pass', 'clean', ruleVerdict.category || 'trivial_edit',
|
|
3938
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3939
|
+
command: editContent, reasoning: ruleVerdict.reason || 'no violations (rules + CWE)',
|
|
3940
|
+
rulesChecked: relevantRules, violatedRules: [],
|
|
3941
|
+
ccModel: captureModel, ...lineMetrics,
|
|
3942
|
+
});
|
|
3943
|
+
const cPassLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (ruleVerdict.reason || 'clean (rules + CWE)');
|
|
3944
|
+
outputJson({ systemMessage: cPassLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro combined edit judge (rules + CWE). ' + (ruleVerdict.reason || 'clean') } });
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3789
3948
|
let gradeResp: string;
|
|
3790
3949
|
try {
|
|
3791
3950
|
gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
|
|
@@ -3907,7 +4066,7 @@ import {
|
|
|
3907
4066
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, isShellTool, isCursorHookFormat,
|
|
3908
4067
|
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, cweSnippetLines, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3909
4068
|
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3910
|
-
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4069
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage, combinedEditGradeEnabled,
|
|
3911
4070
|
} from './_synkro-common.ts';
|
|
3912
4071
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
3913
4072
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
@@ -4135,6 +4294,11 @@ async function main() {
|
|
|
4135
4294
|
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
4136
4295
|
const rt = await cweRoute(config, synkroFile);
|
|
4137
4296
|
|
|
4297
|
+
// Combined-grade mode: the edit-precheck hook runs CWE inside its single
|
|
4298
|
+
// grade-edit-cwe call, so this standalone CWE scan must no-op — otherwise the
|
|
4299
|
+
// edit would be graded for CWE twice (defeating the inference-merge).
|
|
4300
|
+
if (combinedEditGradeEnabled(synkroFile)) { outputEmpty(); return; }
|
|
4301
|
+
|
|
4138
4302
|
if (config.silent) {
|
|
4139
4303
|
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan:' + (graderPool === 'claude_code' ? 'claude' : graderPool) + '] skipped (silent mode)' });
|
|
4140
4304
|
return;
|
|
@@ -4560,14 +4724,16 @@ async function main() {
|
|
|
4560
4724
|
jwt = await ensureFreshJwt(jwt);
|
|
4561
4725
|
|
|
4562
4726
|
const config = await loadConfig(jwt);
|
|
4563
|
-
const rt = await route(config);
|
|
4564
4727
|
|
|
4565
4728
|
if (config.silent) {
|
|
4566
|
-
outputJson({ systemMessage: '[synkro:
|
|
4729
|
+
outputJson({ systemMessage: '[synkro:cveScan] ' + fileShort + ' \u2192 skipped (silent mode)' });
|
|
4567
4730
|
return;
|
|
4568
4731
|
}
|
|
4569
4732
|
|
|
4570
|
-
|
|
4733
|
+
// CVE scanning has no local/cloud distinction from the user's view \u2014 it's an
|
|
4734
|
+
// OSV database lookup that always runs server-side \u2014 so the tag omits the
|
|
4735
|
+
// location segment that grading/CWE carry. Just [synkro:cveScan].
|
|
4736
|
+
const cveTag = '[synkro:cveScan]';
|
|
4571
4737
|
|
|
4572
4738
|
// Reconstruct proposed content
|
|
4573
4739
|
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
@@ -4667,7 +4833,7 @@ async function main() {
|
|
|
4667
4833
|
finding_ids: violatedIds, severity: 'critical',
|
|
4668
4834
|
repo: gitRepo || undefined,
|
|
4669
4835
|
});
|
|
4670
|
-
const tagStr = '[synkro:
|
|
4836
|
+
const tagStr = '[synkro:pkgScan]';
|
|
4671
4837
|
const denyReason = tagStr + ' BLOCKED: ' + summary + '\\nDo not write this version. Pick a fixed/safe version instead.';
|
|
4672
4838
|
outputJson({
|
|
4673
4839
|
systemMessage: denyReason,
|
|
@@ -10403,7 +10569,7 @@ function writeConfigEnv(opts) {
|
|
|
10403
10569
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
10404
10570
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
10405
10571
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
10406
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10572
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.74")}`
|
|
10407
10573
|
];
|
|
10408
10574
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
10409
10575
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -14055,7 +14221,7 @@ var args = process.argv.slice(2);
|
|
|
14055
14221
|
var cmd = args[0] || "";
|
|
14056
14222
|
var subArgs = args.slice(1);
|
|
14057
14223
|
function printVersion() {
|
|
14058
|
-
console.log("1.6.
|
|
14224
|
+
console.log("1.6.74");
|
|
14059
14225
|
}
|
|
14060
14226
|
function printHelp2() {
|
|
14061
14227
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|