@synkro-sh/cli 1.5.5 → 1.5.7
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 +114 -47
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1110,7 +1110,7 @@ const ROLE_MAP: Record<string, GradeRole> = {
|
|
|
1110
1110
|
edit: 'grade-edit', bash: 'grade-bash', plan: 'grade-plan', cwe: 'grade-cwe',
|
|
1111
1111
|
};
|
|
1112
1112
|
|
|
1113
|
-
async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port: number, timeoutMs =
|
|
1113
|
+
async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port: number, timeoutMs = 30000): Promise<string> {
|
|
1114
1114
|
const body = JSON.stringify({ role, payload: prompt, content: prompt });
|
|
1115
1115
|
|
|
1116
1116
|
const resp = await fetch('http://127.0.0.1:' + port + '/submit', {
|
|
@@ -1130,7 +1130,7 @@ async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port:
|
|
|
1130
1130
|
return String(data.result || '');
|
|
1131
1131
|
}
|
|
1132
1132
|
|
|
1133
|
-
export async function localGrade(surface: string, prompt: string, timeoutMs =
|
|
1133
|
+
export async function localGrade(surface: string, prompt: string, timeoutMs = 30000): Promise<string> {
|
|
1134
1134
|
if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
|
|
1135
1135
|
const jwt = loadJwt();
|
|
1136
1136
|
if (!jwt) throw new Error('NO_JWT');
|
|
@@ -2804,7 +2804,7 @@ import {
|
|
|
2804
2804
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
2805
2805
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
2806
2806
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
2807
|
-
logGraderUnavailable,
|
|
2807
|
+
logGraderUnavailable, isPathUnder,
|
|
2808
2808
|
type HookConfig, type Rule,
|
|
2809
2809
|
} from './_synkro-common.ts';
|
|
2810
2810
|
|
|
@@ -2849,65 +2849,138 @@ async function main() {
|
|
|
2849
2849
|
const cmdShort = command.slice(0, 80);
|
|
2850
2850
|
log('bashGuard checking: ' + cmdShort);
|
|
2851
2851
|
|
|
2852
|
+
// Load JWT + routing config eagerly so even the short-circuit message
|
|
2853
|
+
// carries the live pack name + local/cloud tag. Cost: ~200-500ms for the
|
|
2854
|
+
// config fetch (network call, no caching). The fetch is unavoidable for
|
|
2855
|
+
// the LLM path anyway — we just pay it sooner so the short-circuit can
|
|
2856
|
+
// produce a properly-tagged system message.
|
|
2857
|
+
let jwt = loadJwt();
|
|
2858
|
+
if (!jwt) { outputEmpty(); return; }
|
|
2859
|
+
jwt = await ensureFreshJwt(jwt);
|
|
2860
|
+
|
|
2861
|
+
const config = await loadConfig(jwt);
|
|
2862
|
+
const rt = await route(config);
|
|
2863
|
+
const tagStr = tag(rt, config);
|
|
2864
|
+
|
|
2865
|
+
if (config.silent) {
|
|
2866
|
+
outputJson({ systemMessage: tagStr + ' bashGuard → skipped (silent mode)' });
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2852
2870
|
// ─── Hook-side short-circuit for safe in-repo reads ───
|
|
2853
2871
|
// The judge primer already deterministically allows these, but the round
|
|
2854
2872
|
// trip + batch queue still costs 1–25s per call. Skipping the grade for
|
|
2855
2873
|
// unambiguously read-only operations removes that latency for ~half of
|
|
2856
2874
|
// typical commands (cat/grep/git status/ls/etc.) and unblocks the worker
|
|
2857
2875
|
// pool to grade the operations that actually need judgment.
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2876
|
+
// Returning FALSE just means "don't short-circuit — let the LLM grade it."
|
|
2877
|
+
// Never blocks. The judge sees the command, applies rules, returns its
|
|
2878
|
+
// own verdict. Path scoping below: STRICT, only short-circuit when every
|
|
2879
|
+
// absolute path is under the linked repo root.
|
|
2880
|
+
function isSafeBashSegment(seg: string, repoRoot: string): boolean {
|
|
2881
|
+
const UNSAFE_CHARS = ['>', ';', '&', '\`'];
|
|
2882
|
+
for (const ch of UNSAFE_CHARS) { if (seg.indexOf(ch) !== -1) return false; }
|
|
2883
|
+
const padded = ' ' + seg + ' ';
|
|
2884
|
+
const UNSAFE_WORDS = [
|
|
2885
|
+
' sudo ', ' su ', ' rm ', ' mv ', ' cp ', ' chmod ', ' chown ',
|
|
2886
|
+
' tee ', ' kill ', ' sed -i', ' sed --in-place',
|
|
2887
|
+
' sh -c', ' bash -c', ' zsh -c', ' eval ', ' exec ',
|
|
2888
|
+
'\$(',
|
|
2889
|
+
];
|
|
2871
2890
|
for (const w of UNSAFE_WORDS) { if (padded.indexOf(w) !== -1) return false; }
|
|
2891
|
+
|
|
2892
|
+
// Narrowed verb set. Removed:
|
|
2893
|
+
// awk: has system() / |& shell-spawn
|
|
2894
|
+
// env: \`env FOO=bar evil_cmd\` runs evil_cmd
|
|
2895
|
+
// sed: scripting + -i write capability; not worth parsing
|
|
2872
2896
|
const SAFE_VERBS = new Set([
|
|
2873
2897
|
'cat','head','tail','less','more','grep','egrep','fgrep','rg','ag',
|
|
2874
2898
|
'find','fd','ls','wc','cmp','diff','file','stat','which','whereis','type',
|
|
2875
|
-
'pwd','whoami','id','date','echo','printf','
|
|
2876
|
-
'jq','yq','
|
|
2899
|
+
'pwd','whoami','id','date','echo','printf','true','false',
|
|
2900
|
+
'jq','yq','sort','uniq','cut','tr','xxd','hexdump','od','column',
|
|
2877
2901
|
'node','npm','pnpm','yarn','bun','python','python3','ruby','go','rustc','cargo',
|
|
2878
|
-
'git',
|
|
2902
|
+
'git',
|
|
2879
2903
|
]);
|
|
2880
|
-
const tokens =
|
|
2904
|
+
const tokens = seg.trim().split(' ').filter(t => t.length > 0);
|
|
2881
2905
|
const verb = tokens[0] || '';
|
|
2882
2906
|
if (!SAFE_VERBS.has(verb)) return false;
|
|
2907
|
+
|
|
2908
|
+
// find/fd: reject any execution / mutation action flag.
|
|
2909
|
+
if (verb === 'find' || verb === 'fd') {
|
|
2910
|
+
const BAD = new Set([
|
|
2911
|
+
'-exec','-execdir','-ok','-okdir','-delete',
|
|
2912
|
+
'-fprint','-fprintf','-fprint0','-fls',
|
|
2913
|
+
'--exec','--exec-batch',
|
|
2914
|
+
]);
|
|
2915
|
+
for (const t of tokens) { if (BAD.has(t)) return false; }
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// git: only pure-read subcommands. branch/tag/remote/config dropped —
|
|
2919
|
+
// each has flag combinations that mutate state.
|
|
2883
2920
|
if (verb === 'git') {
|
|
2884
|
-
const SAFE_GIT = new Set([
|
|
2921
|
+
const SAFE_GIT = new Set([
|
|
2922
|
+
'log','show','diff','blame','status','rev-parse',
|
|
2923
|
+
'ls-files','ls-tree','cat-file','shortlog','reflog',
|
|
2924
|
+
'describe','symbolic-ref','--version',
|
|
2925
|
+
]);
|
|
2885
2926
|
const sub = tokens[1] || '';
|
|
2886
|
-
|
|
2887
|
-
}
|
|
2888
|
-
if (['npm','pnpm','yarn','bun','cargo','go'].includes(verb)) {
|
|
2927
|
+
if (!SAFE_GIT.has(sub)) return false;
|
|
2928
|
+
} else if (['npm','pnpm','yarn','bun','cargo','go'].includes(verb)) {
|
|
2889
2929
|
const sub = tokens[1] || '';
|
|
2890
|
-
const SAFE_PKG = new Set([
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2930
|
+
const SAFE_PKG = new Set([
|
|
2931
|
+
'--version','-v','version','list','ls','why','view','show','info','outdated',
|
|
2932
|
+
'-h','--help','help',
|
|
2933
|
+
]);
|
|
2934
|
+
if (!SAFE_PKG.has(sub)) return false;
|
|
2935
|
+
} else if (['node','python','python3','ruby','rustc'].includes(verb)) {
|
|
2894
2936
|
const sub = tokens[1] || '';
|
|
2895
|
-
|
|
2937
|
+
if (sub !== '--version' && sub !== '-v' && sub !== '-V') return false;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// STRICT path scoping. Absolute paths MUST resolve under repoRoot.
|
|
2941
|
+
// Home-relative (~/...) paths fall through to the LLM. Relative paths
|
|
2942
|
+
// are implicitly under cwd which is the repo root for the agent session.
|
|
2943
|
+
if (!repoRoot) return false;
|
|
2944
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
2945
|
+
const t = tokens[i];
|
|
2946
|
+
const stripped = t.replace(/^['"]/, '').replace(/['"]$/, '');
|
|
2947
|
+
if (stripped.startsWith('~')) return false;
|
|
2948
|
+
if (stripped.startsWith('/')) {
|
|
2949
|
+
if (!isPathUnder(stripped, repoRoot)) return false;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
return true;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
function isSafeInRepoRead(tName: string, cmd: string, repoRoot: string): boolean {
|
|
2956
|
+
if (tName === 'Read' || tName === 'Grep' || tName === 'Glob') return true;
|
|
2957
|
+
if (tName !== 'Bash' && tName !== 'Shell' && tName !== 'terminal' &&
|
|
2958
|
+
tName !== 'run_terminal_cmd' && tName !== 'execute_command') return false;
|
|
2959
|
+
if (!cmd || !repoRoot) return false;
|
|
2960
|
+
// Allow pipes only if EVERY segment is safe on its own. Catches
|
|
2961
|
+
// \`grep ... | head\`, \`cat foo | wc -l\`, \`git log | less\`, etc.
|
|
2962
|
+
// Empty segments (from \`||\`) cause rejection.
|
|
2963
|
+
const segments = cmd.split('|');
|
|
2964
|
+
for (const seg of segments) {
|
|
2965
|
+
const t = seg.trim();
|
|
2966
|
+
if (t.length === 0) return false;
|
|
2967
|
+
if (!isSafeBashSegment(t, repoRoot)) return false;
|
|
2896
2968
|
}
|
|
2897
|
-
// sed: only safe without -i (we filtered that above).
|
|
2898
2969
|
return true;
|
|
2899
2970
|
}
|
|
2900
2971
|
|
|
2901
|
-
if (isSafeInRepoRead(toolName, command)) {
|
|
2972
|
+
if (isSafeInRepoRead(toolName, command, cwd)) {
|
|
2902
2973
|
log('bashGuard ' + cmdShort + ' → instant allow (safe in-repo read)');
|
|
2903
|
-
outputJson({
|
|
2974
|
+
outputJson({
|
|
2975
|
+
systemMessage: tagStr + ' bashGuard → pass: safe in-repo read',
|
|
2976
|
+
hookSpecificOutput: {
|
|
2977
|
+
hookEventName: 'PreToolUse',
|
|
2978
|
+
additionalContext: tagStr + ' bashGuard pass: safe in-repo read.',
|
|
2979
|
+
},
|
|
2980
|
+
});
|
|
2904
2981
|
return;
|
|
2905
2982
|
}
|
|
2906
2983
|
|
|
2907
|
-
let jwt = loadJwt();
|
|
2908
|
-
if (!jwt) { outputEmpty(); return; }
|
|
2909
|
-
jwt = await ensureFreshJwt(jwt);
|
|
2910
|
-
|
|
2911
2984
|
// ─── Install protection: server-side pkg-scan (CVE + typosquat + tarball + reputation) ───
|
|
2912
2985
|
let installScanMsg = '';
|
|
2913
2986
|
if (toolName === 'Bash') {
|
|
@@ -3016,15 +3089,9 @@ async function main() {
|
|
|
3016
3089
|
|
|
3017
3090
|
const lastPrompt = readLastPrompt(sessionId);
|
|
3018
3091
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
if (config.silent) {
|
|
3024
|
-
const msg = (installScanMsg ? installScanMsg + '\\n' : '') + tagStr + ' bashGuard → skipped (silent mode)';
|
|
3025
|
-
outputJson({ systemMessage: msg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } });
|
|
3026
|
-
return;
|
|
3027
|
-
}
|
|
3092
|
+
// jwt + config + rt + tagStr already loaded eagerly at top of main
|
|
3093
|
+
// (so the short-circuit could emit a properly-tagged message). Silent
|
|
3094
|
+
// mode was also checked up there.
|
|
3028
3095
|
|
|
3029
3096
|
if (rt === 'local') {
|
|
3030
3097
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
@@ -5854,7 +5921,7 @@ function writeConfigEnv(opts) {
|
|
|
5854
5921
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5855
5922
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5856
5923
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5857
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.5.
|
|
5924
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.5.7")}`
|
|
5858
5925
|
];
|
|
5859
5926
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5860
5927
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7244,7 +7311,7 @@ var args = process.argv.slice(2);
|
|
|
7244
7311
|
var cmd = args[0] || "";
|
|
7245
7312
|
var subArgs = args.slice(1);
|
|
7246
7313
|
function printVersion() {
|
|
7247
|
-
console.log("1.5.
|
|
7314
|
+
console.log("1.5.7");
|
|
7248
7315
|
}
|
|
7249
7316
|
function printHelp() {
|
|
7250
7317
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|