@synkro-sh/cli 1.5.2 → 1.5.4
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 +105 -11
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1854,6 +1854,28 @@ function cursorHookExit(): never {
|
|
|
1854
1854
|
process.exit(0);
|
|
1855
1855
|
}
|
|
1856
1856
|
|
|
1857
|
+
// \u2500\u2500\u2500 Grader-unavailable diagnostic log \u2500\u2500\u2500
|
|
1858
|
+
// Records every time a hook tried to call the local grader and fell open
|
|
1859
|
+
// because the call failed. JSONL at ~/.synkro/grader-unavailable.log so the
|
|
1860
|
+
// user can pinpoint cause (timeout vs ECONNREFUSED vs HTTP 5xx vs sick pool)
|
|
1861
|
+
// instead of guessing from a one-shot system message in the CC UI.
|
|
1862
|
+
|
|
1863
|
+
const UNAVAIL_LOG = join(HOME, '.synkro', 'grader-unavailable.log');
|
|
1864
|
+
|
|
1865
|
+
export function logGraderUnavailable(hook: string, target: string, errorMessage: string): void {
|
|
1866
|
+
try {
|
|
1867
|
+
const entry = {
|
|
1868
|
+
ts: new Date().toISOString(),
|
|
1869
|
+
hook,
|
|
1870
|
+
target,
|
|
1871
|
+
error: errorMessage.slice(0, 500),
|
|
1872
|
+
};
|
|
1873
|
+
appendFileSync(UNAVAIL_LOG, JSON.stringify(entry) + '\\n', 'utf-8');
|
|
1874
|
+
} catch {
|
|
1875
|
+
// best-effort \u2014 never let logging failure cascade into a hook failure
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1857
1879
|
// \u2500\u2500\u2500 Output Helpers \u2500\u2500\u2500
|
|
1858
1880
|
|
|
1859
1881
|
export function outputJson(obj: any): void {
|
|
@@ -1916,6 +1938,7 @@ import {
|
|
|
1916
1938
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
1917
1939
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
1918
1940
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
1941
|
+
logGraderUnavailable,
|
|
1919
1942
|
type HookConfig, type Rule,
|
|
1920
1943
|
} from './_synkro-common.ts';
|
|
1921
1944
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -2010,7 +2033,8 @@ async function main() {
|
|
|
2010
2033
|
let gradeResp: string;
|
|
2011
2034
|
try {
|
|
2012
2035
|
gradeResp = await localGrade('edit', graderPrompt);
|
|
2013
|
-
} catch {
|
|
2036
|
+
} catch (err) {
|
|
2037
|
+
logGraderUnavailable('editGuard', fileShort, (err as Error).message || String(err));
|
|
2014
2038
|
outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 local grader unavailable, skipped' });
|
|
2015
2039
|
return;
|
|
2016
2040
|
}
|
|
@@ -2133,6 +2157,7 @@ import {
|
|
|
2133
2157
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
2134
2158
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
2135
2159
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, filePathFromToolInput, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
2160
|
+
logGraderUnavailable,
|
|
2136
2161
|
} from './_synkro-common.ts';
|
|
2137
2162
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
2138
2163
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
@@ -2403,6 +2428,7 @@ async function main() {
|
|
|
2403
2428
|
gradeResponses = [resp1, resp2];
|
|
2404
2429
|
} catch (gradeErr: any) {
|
|
2405
2430
|
const reason = gradeErr?.message || String(gradeErr);
|
|
2431
|
+
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
2406
2432
|
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
|
|
2407
2433
|
return;
|
|
2408
2434
|
}
|
|
@@ -2411,6 +2437,7 @@ async function main() {
|
|
|
2411
2437
|
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent))];
|
|
2412
2438
|
} catch (gradeErr: any) {
|
|
2413
2439
|
const reason = gradeErr?.message || String(gradeErr);
|
|
2440
|
+
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
2414
2441
|
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
|
|
2415
2442
|
return;
|
|
2416
2443
|
}
|
|
@@ -2777,6 +2804,7 @@ import {
|
|
|
2777
2804
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
2778
2805
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
2779
2806
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
2807
|
+
logGraderUnavailable,
|
|
2780
2808
|
type HookConfig, type Rule,
|
|
2781
2809
|
} from './_synkro-common.ts';
|
|
2782
2810
|
|
|
@@ -2821,6 +2849,59 @@ async function main() {
|
|
|
2821
2849
|
const cmdShort = command.slice(0, 80);
|
|
2822
2850
|
log('bashGuard checking: ' + cmdShort);
|
|
2823
2851
|
|
|
2852
|
+
// ─── Hook-side short-circuit for safe in-repo reads ───
|
|
2853
|
+
// The judge primer already deterministically allows these, but the round
|
|
2854
|
+
// trip + batch queue still costs 1–25s per call. Skipping the grade for
|
|
2855
|
+
// unambiguously read-only operations removes that latency for ~half of
|
|
2856
|
+
// typical commands (cat/grep/git status/ls/etc.) and unblocks the worker
|
|
2857
|
+
// pool to grade the operations that actually need judgment.
|
|
2858
|
+
function isSafeInRepoRead(tName: string, cmd: string): boolean {
|
|
2859
|
+
// CC's native read tools are inherently safe.
|
|
2860
|
+
if (tName === 'Read' || tName === 'Grep' || tName === 'Glob') return true;
|
|
2861
|
+
if (tName !== 'Bash' && tName !== 'Shell' && tName !== 'terminal' &&
|
|
2862
|
+
tName !== 'run_terminal_cmd' && tName !== 'execute_command') return false;
|
|
2863
|
+
// Reject any shell metacharacter that could turn a "read" into a write
|
|
2864
|
+
// or chain to an unsafe consumer: redirects, pipes, sequencing, command
|
|
2865
|
+
// substitution, sudo/su.
|
|
2866
|
+
if (/[>;&|\\\`]|\\$\\(|<<|\\bsudo\\b|\\bsu\\b|\\brm\\b|\\bmv\\b|\\bcp\\b|\\bchmod\\b|\\bchown\\b|\\btee\\b|\\bsed\\s+-i\\b|\\bkill\\b/.test(cmd)) return false;
|
|
2867
|
+
const SAFE_VERBS = new Set([
|
|
2868
|
+
'cat','head','tail','less','more','grep','egrep','fgrep','rg','ag',
|
|
2869
|
+
'find','fd','ls','wc','cmp','diff','file','stat','which','whereis','type',
|
|
2870
|
+
'pwd','whoami','id','date','echo','printf','env','true','false',
|
|
2871
|
+
'jq','yq','awk','sort','uniq','cut','tr','xxd','hexdump','od','column',
|
|
2872
|
+
'node','npm','pnpm','yarn','bun','python','python3','ruby','go','rustc','cargo',
|
|
2873
|
+
'git',
|
|
2874
|
+
]);
|
|
2875
|
+
const tokens = cmd.trim().split(/\\s+/);
|
|
2876
|
+
const verb = tokens[0] || '';
|
|
2877
|
+
if (!SAFE_VERBS.has(verb)) return false;
|
|
2878
|
+
// For multi-mode tools, only allow read subcommands / version flags.
|
|
2879
|
+
if (verb === 'git') {
|
|
2880
|
+
const SAFE_GIT = new Set(['log','show','diff','blame','status','branch','tag','remote','config','rev-parse','ls-files','ls-tree','cat-file','shortlog','reflog','describe','symbolic-ref']);
|
|
2881
|
+
const sub = tokens[1] || '';
|
|
2882
|
+
return SAFE_GIT.has(sub);
|
|
2883
|
+
}
|
|
2884
|
+
if (['npm','pnpm','yarn','bun','cargo','go'].includes(verb)) {
|
|
2885
|
+
// Only allow plain version/info/list/why probes — block install/add/update/run/exec.
|
|
2886
|
+
const sub = tokens[1] || '';
|
|
2887
|
+
const SAFE_PKG = new Set(['--version','-v','version','list','ls','why','view','show','info','outdated','-h','--help','help']);
|
|
2888
|
+
return SAFE_PKG.has(sub);
|
|
2889
|
+
}
|
|
2890
|
+
if (['node','python','python3','ruby','rustc'].includes(verb)) {
|
|
2891
|
+
const sub = tokens[1] || '';
|
|
2892
|
+
return sub === '--version' || sub === '-v' || sub === '-V';
|
|
2893
|
+
}
|
|
2894
|
+
// sed without -i flag is read-only by definition; we already excluded
|
|
2895
|
+
// sed -i above. Anything else with a SAFE_VERB and no metachars is fine.
|
|
2896
|
+
return true;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
if (isSafeInRepoRead(toolName, command)) {
|
|
2900
|
+
log('bashGuard ' + cmdShort + ' → instant allow (safe in-repo read)');
|
|
2901
|
+
outputJson({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro: safe in-repo read, deterministic allow.' } });
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2824
2905
|
let jwt = loadJwt();
|
|
2825
2906
|
if (!jwt) { outputEmpty(); return; }
|
|
2826
2907
|
jwt = await ensureFreshJwt(jwt);
|
|
@@ -2961,7 +3042,8 @@ async function main() {
|
|
|
2961
3042
|
let gradeResp: string;
|
|
2962
3043
|
try {
|
|
2963
3044
|
gradeResp = await localGrade('bash', graderPrompt);
|
|
2964
|
-
} catch {
|
|
3045
|
+
} catch (err) {
|
|
3046
|
+
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', (err as Error).message || String(err));
|
|
2965
3047
|
outputJson({ systemMessage: tagStr + ' bashGuard → local grader unavailable, skipped' });
|
|
2966
3048
|
return;
|
|
2967
3049
|
}
|
|
@@ -2973,8 +3055,12 @@ async function main() {
|
|
|
2973
3055
|
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
2974
3056
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
2975
3057
|
|
|
3058
|
+
const auditRulesInReason = (config.rules || [])
|
|
3059
|
+
.filter((r: any) => r.mode === 'audit' && r.rule_id && verdict.reason && verdict.reason.includes(r.rule_id))
|
|
3060
|
+
.map((r: any) => r.rule_id);
|
|
3061
|
+
|
|
2976
3062
|
if (mode === 'audit') {
|
|
2977
|
-
const reason = tagStr + ' bashGuard → warning' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation');
|
|
3063
|
+
const reason = tagStr + ' bashGuard → warning' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before proceeding — do not ask the user.';
|
|
2978
3064
|
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
|
|
2979
3065
|
outputJson({ systemMessage: combined, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: combined } });
|
|
2980
3066
|
dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
@@ -2983,11 +3069,14 @@ async function main() {
|
|
|
2983
3069
|
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
2984
3070
|
});
|
|
2985
3071
|
} else {
|
|
2986
|
-
|
|
2987
|
-
|
|
3072
|
+
let blockMsg = tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3073
|
+
if (auditRulesInReason.length > 0) {
|
|
3074
|
+
blockMsg += '\\nAudit violations (' + auditRulesInReason.join(', ') + '): fix these automatically before retrying — do not ask the user, just resolve them (e.g. run the required precondition).';
|
|
3075
|
+
}
|
|
3076
|
+
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
|
|
2988
3077
|
outputJson({
|
|
2989
3078
|
systemMessage: combined,
|
|
2990
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason:
|
|
3079
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: combined },
|
|
2991
3080
|
});
|
|
2992
3081
|
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
2993
3082
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
@@ -3078,6 +3167,7 @@ import {
|
|
|
3078
3167
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
3079
3168
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3080
3169
|
outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
3170
|
+
logGraderUnavailable,
|
|
3081
3171
|
type HookConfig, type Rule,
|
|
3082
3172
|
} from './_synkro-common.ts';
|
|
3083
3173
|
|
|
@@ -3149,7 +3239,8 @@ async function main() {
|
|
|
3149
3239
|
let gradeResp: string;
|
|
3150
3240
|
try {
|
|
3151
3241
|
gradeResp = await localGrade('bash', graderPrompt);
|
|
3152
|
-
} catch {
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), (err as Error).message || String(err));
|
|
3153
3244
|
outputJson({ systemMessage: tagStr + ' agentGuard \u2192 local grader unavailable, skipped' });
|
|
3154
3245
|
return;
|
|
3155
3246
|
}
|
|
@@ -3599,7 +3690,7 @@ async function main() {
|
|
|
3599
3690
|
let isError = payload.tool_result?.is_error === true;
|
|
3600
3691
|
try {
|
|
3601
3692
|
const out = JSON.parse(payload.tool_output || '{}');
|
|
3602
|
-
if (out.exitCode !== 0 || out.is_error === true) isError = true;
|
|
3693
|
+
if ((typeof out.exitCode === 'number' && out.exitCode !== 0) || out.is_error === true) isError = true;
|
|
3603
3694
|
} catch {}
|
|
3604
3695
|
const cmd = shellCmd;
|
|
3605
3696
|
const cmdHash = cmd ? hashCommand(cmd) : '';
|
|
@@ -5532,6 +5623,9 @@ async function dockerInstall(opts = {}) {
|
|
|
5532
5623
|
`${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
|
|
5533
5624
|
"-e",
|
|
5534
5625
|
`WORKERS_PER_POOL=${workers}`,
|
|
5626
|
+
// Pass through the batch-size lever if the operator set it. Defaults
|
|
5627
|
+
// inside the container to 5; clamped to [1, 20] by synkro-server.ts.
|
|
5628
|
+
...process.env.SYNKRO_MAX_BATCH_SIZE ? ["-e", `SYNKRO_MAX_BATCH_SIZE=${process.env.SYNKRO_MAX_BATCH_SIZE}`] : [],
|
|
5535
5629
|
image
|
|
5536
5630
|
];
|
|
5537
5631
|
const run = spawnSync2("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
|
|
@@ -5758,7 +5852,7 @@ function writeConfigEnv(opts) {
|
|
|
5758
5852
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5759
5853
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5760
5854
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5761
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.5.
|
|
5855
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.5.4")}`
|
|
5762
5856
|
];
|
|
5763
5857
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5764
5858
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -6165,7 +6259,7 @@ async function installCommand(opts = {}) {
|
|
|
6165
6259
|
process.exit(1);
|
|
6166
6260
|
}
|
|
6167
6261
|
console.log("Installing Synkro server container...");
|
|
6168
|
-
const workersPerPool = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "
|
|
6262
|
+
const workersPerPool = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
6169
6263
|
const { image, hostMcpPort, hostGraderPort, hostCwePort } = await dockerInstall({ workersPerPool });
|
|
6170
6264
|
console.log(` \u2713 pulled ${image}`);
|
|
6171
6265
|
console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort}`);
|
|
@@ -7148,7 +7242,7 @@ var args = process.argv.slice(2);
|
|
|
7148
7242
|
var cmd = args[0] || "";
|
|
7149
7243
|
var subArgs = args.slice(1);
|
|
7150
7244
|
function printVersion() {
|
|
7151
|
-
console.log("1.5.
|
|
7245
|
+
console.log("1.5.4");
|
|
7152
7246
|
}
|
|
7153
7247
|
function printHelp() {
|
|
7154
7248
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|