@synkro-sh/cli 1.5.6 → 1.6.0
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 +340 -177
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -641,32 +641,9 @@ synkro_channel_up() {
|
|
|
641
641
|
}
|
|
642
642
|
|
|
643
643
|
# Fetch hook config. Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES, SYNKRO_SILENT, SYNKRO_POLICY_NAME.
|
|
644
|
-
_SYNKRO_RULES_FILE="$HOME/.synkro/rules.json"
|
|
645
644
|
_SYNKRO_MCP_JWT_FILE="$HOME/.synkro/.mcp-jwt"
|
|
646
645
|
|
|
647
646
|
synkro_load_config() {
|
|
648
|
-
# Local-first: read from ~/.synkro/rules.json if it exists (zero latency, no network)
|
|
649
|
-
if [ -f "$_SYNKRO_RULES_FILE" ]; then
|
|
650
|
-
local rdata
|
|
651
|
-
rdata=$(cat "$_SYNKRO_RULES_FILE" 2>/dev/null)
|
|
652
|
-
if [ -n "$rdata" ]; then
|
|
653
|
-
SYNKRO_CAPTURE_DEPTH="local_only"
|
|
654
|
-
SYNKRO_TIER="standard"
|
|
655
|
-
SYNKRO_SILENT=$(echo "$rdata" | jq -r '.config.silent // false' 2>/dev/null)
|
|
656
|
-
local active_id
|
|
657
|
-
active_id=$(echo "$rdata" | jq -r '.config.activePolicyId // empty' 2>/dev/null)
|
|
658
|
-
if [ -n "$active_id" ]; then
|
|
659
|
-
SYNKRO_POLICY_NAME=$(echo "$rdata" | jq -r --arg id "$active_id" '.policies[]? | select(.id == $id) | .name // empty' 2>/dev/null)
|
|
660
|
-
SYNKRO_RULES=$(echo "$rdata" | jq -c --arg id "$active_id" '[.policies[]? | select(.id == $id) | .rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
661
|
-
else
|
|
662
|
-
SYNKRO_POLICY_NAME=$(echo "$rdata" | jq -r '.policies[0]?.name // empty' 2>/dev/null)
|
|
663
|
-
SYNKRO_RULES=$(echo "$rdata" | jq -c '[.policies[0]?.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
664
|
-
fi
|
|
665
|
-
return
|
|
666
|
-
fi
|
|
667
|
-
fi
|
|
668
|
-
|
|
669
|
-
# Fallback: fetch from cloud API
|
|
670
647
|
local resp
|
|
671
648
|
resp=$(curl -sS "\${GATEWAY_URL}/api/v1/hook/config\${1:+?$1}" -H "Authorization: Bearer $JWT" --max-time 4 2>/dev/null || echo "")
|
|
672
649
|
if [ -z "$resp" ]; then return; fi
|
|
@@ -674,7 +651,7 @@ synkro_load_config() {
|
|
|
674
651
|
SYNKRO_TIER=$(echo "$resp" | jq -r '.tier // "standard"' 2>/dev/null)
|
|
675
652
|
SYNKRO_SILENT=$(echo "$resp" | jq -r '.silent_mode // false' 2>/dev/null)
|
|
676
653
|
SYNKRO_POLICY_NAME=$(echo "$resp" | jq -r '.active_policy_name // empty' 2>/dev/null)
|
|
677
|
-
SYNKRO_RULES=$(echo "$resp" | jq -c '[.rules[]? |
|
|
654
|
+
SYNKRO_RULES=$(echo "$resp" | jq -c '[.rules[]? | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
678
655
|
}
|
|
679
656
|
|
|
680
657
|
synkro_local_capture() {
|
|
@@ -988,6 +965,14 @@ export async function cweChannelUp(): Promise<boolean> {
|
|
|
988
965
|
return channelUp(18930);
|
|
989
966
|
}
|
|
990
967
|
|
|
968
|
+
// \u2500\u2500\u2500 Mode Normalization \u2500\u2500\u2500
|
|
969
|
+
|
|
970
|
+
function normalizeMode(m?: string): 'ask' | 'fix' {
|
|
971
|
+
if (m === 'blocking' || m === 'ask') return 'ask';
|
|
972
|
+
if (m === 'audit' || m === 'fix') return 'fix';
|
|
973
|
+
return 'ask';
|
|
974
|
+
}
|
|
975
|
+
|
|
991
976
|
// \u2500\u2500\u2500 Config Loading \u2500\u2500\u2500
|
|
992
977
|
|
|
993
978
|
export interface Rule {
|
|
@@ -1029,13 +1014,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1029
1014
|
if (policy) {
|
|
1030
1015
|
config.policyName = policy.name || '';
|
|
1031
1016
|
config.rules = (policy.rules || [])
|
|
1032
|
-
.filter((r: any) => r.hook_stage === 'pre' || r.hook_stage === 'both' || r.hook_stage == null)
|
|
1033
1017
|
.map((r: any) => ({
|
|
1034
1018
|
rule_id: r.rule_id || '',
|
|
1035
1019
|
text: r.text || '',
|
|
1036
1020
|
severity: r.severity || '',
|
|
1037
1021
|
category: r.category || '',
|
|
1038
|
-
mode: r.mode
|
|
1022
|
+
mode: normalizeMode(r.mode),
|
|
1039
1023
|
}));
|
|
1040
1024
|
}
|
|
1041
1025
|
config.silent = raw.silent === true;
|
|
@@ -1067,13 +1051,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1067
1051
|
}
|
|
1068
1052
|
if (Array.isArray(data.rules)) {
|
|
1069
1053
|
config.rules = data.rules
|
|
1070
|
-
.filter((r: any) => r.hook_stage === 'pre' || r.hook_stage === 'both' || r.hook_stage == null)
|
|
1071
1054
|
.map((r: any) => ({
|
|
1072
1055
|
rule_id: r.rule_id || '',
|
|
1073
1056
|
text: r.text || '',
|
|
1074
1057
|
severity: r.severity || '',
|
|
1075
1058
|
category: r.category || '',
|
|
1076
|
-
mode: r.mode
|
|
1059
|
+
mode: normalizeMode(r.mode),
|
|
1077
1060
|
}));
|
|
1078
1061
|
}
|
|
1079
1062
|
} catch {}
|
|
@@ -1410,11 +1393,11 @@ export function appendLocalTelemetry(body: Record<string, any>): void {
|
|
|
1410
1393
|
|
|
1411
1394
|
// \u2500\u2500\u2500 Rule Mode Lookup \u2500\u2500\u2500
|
|
1412
1395
|
|
|
1413
|
-
export function ruleMode(ruleId: string, rules: Rule[]): '
|
|
1414
|
-
if (!ruleId || !rules.length) return '
|
|
1396
|
+
export function ruleMode(ruleId: string, rules: Rule[]): 'ask' | 'fix' {
|
|
1397
|
+
if (!ruleId || !rules.length) return 'ask';
|
|
1415
1398
|
const matched = rules.filter(r => r.rule_id === ruleId);
|
|
1416
|
-
if (matched.some(r => r.mode === 'blocking')) return '
|
|
1417
|
-
return (matched[0]?.mode
|
|
1399
|
+
if (matched.some(r => r.mode === 'blocking' || r.mode === 'ask')) return 'ask';
|
|
1400
|
+
return normalizeMode(matched[0]?.mode);
|
|
1418
1401
|
}
|
|
1419
1402
|
|
|
1420
1403
|
// \u2500\u2500\u2500 Content Reconstruction \u2500\u2500\u2500
|
|
@@ -2027,7 +2010,7 @@ async function main() {
|
|
|
2027
2010
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
2028
2011
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
2029
2012
|
'Org rules: ' + JSON.stringify(config.rules),
|
|
2030
|
-
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "
|
|
2013
|
+
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
2031
2014
|
].join('\\n');
|
|
2032
2015
|
|
|
2033
2016
|
let gradeResp: string;
|
|
@@ -2044,37 +2027,27 @@ async function main() {
|
|
|
2044
2027
|
const violatedRules = verdict.ruleId ? [verdict.ruleId] : [];
|
|
2045
2028
|
|
|
2046
2029
|
if (!verdict.ok) {
|
|
2047
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
2030
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
2048
2031
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
2049
2032
|
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
command: editContent, reasoning: guardReason,
|
|
2055
|
-
rulesChecked: config.rules, violatedRules,
|
|
2056
|
-
ccModel: transcript.ccModel,
|
|
2057
|
-
});
|
|
2058
|
-
outputJson({
|
|
2059
|
-
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason,
|
|
2060
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
|
|
2061
|
-
});
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
// Audit mode \u2014 warn but allow
|
|
2066
|
-
dispatchCapture(jwt, 'edit', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
2033
|
+
const denyReason = mode === 'fix'
|
|
2034
|
+
? 'Guard: ' + guardReason + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
|
|
2035
|
+
: 'Guard: ' + guardReason + '\\nAsk the user for explicit consent before retrying.';
|
|
2036
|
+
dispatchCapture(jwt, 'edit', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
2067
2037
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2068
2038
|
command: editContent, reasoning: guardReason,
|
|
2069
2039
|
rulesChecked: config.rules, violatedRules,
|
|
2070
2040
|
ccModel: transcript.ccModel,
|
|
2071
2041
|
});
|
|
2072
|
-
outputJson({
|
|
2042
|
+
outputJson({
|
|
2043
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason,
|
|
2044
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
|
|
2045
|
+
});
|
|
2073
2046
|
return;
|
|
2074
2047
|
}
|
|
2075
2048
|
|
|
2076
2049
|
// Clean
|
|
2077
|
-
dispatchCapture(jwt, 'edit', 'pass', '
|
|
2050
|
+
dispatchCapture(jwt, 'edit', 'pass', 'clean', verdict.category || 'trivial_edit',
|
|
2078
2051
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2079
2052
|
command: editContent, reasoning: verdict.reason || 'no policy violations detected',
|
|
2080
2053
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -2572,7 +2545,7 @@ async function main() {
|
|
|
2572
2545
|
const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
|
|
2573
2546
|
if (cweResp?.action === 'deny' && findings.length > 0) {
|
|
2574
2547
|
const activeCweIds = findings
|
|
2575
|
-
.filter((f: any) => f.mode === 'blocking')
|
|
2548
|
+
.filter((f: any) => f.mode === 'blocking' || f.mode === 'ask')
|
|
2576
2549
|
.map((f: any) => f.cwe)
|
|
2577
2550
|
.filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
|
|
2578
2551
|
|
|
@@ -2804,7 +2777,7 @@ import {
|
|
|
2804
2777
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
2805
2778
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
2806
2779
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
2807
|
-
logGraderUnavailable,
|
|
2780
|
+
logGraderUnavailable, isPathUnder,
|
|
2808
2781
|
type HookConfig, type Rule,
|
|
2809
2782
|
} from './_synkro-common.ts';
|
|
2810
2783
|
|
|
@@ -2849,6 +2822,24 @@ async function main() {
|
|
|
2849
2822
|
const cmdShort = command.slice(0, 80);
|
|
2850
2823
|
log('bashGuard checking: ' + cmdShort);
|
|
2851
2824
|
|
|
2825
|
+
// Load JWT + routing config eagerly so even the short-circuit message
|
|
2826
|
+
// carries the live pack name + local/cloud tag. Cost: ~200-500ms for the
|
|
2827
|
+
// config fetch (network call, no caching). The fetch is unavoidable for
|
|
2828
|
+
// the LLM path anyway — we just pay it sooner so the short-circuit can
|
|
2829
|
+
// produce a properly-tagged system message.
|
|
2830
|
+
let jwt = loadJwt();
|
|
2831
|
+
if (!jwt) { outputEmpty(); return; }
|
|
2832
|
+
jwt = await ensureFreshJwt(jwt);
|
|
2833
|
+
|
|
2834
|
+
const config = await loadConfig(jwt);
|
|
2835
|
+
const rt = await route(config);
|
|
2836
|
+
const tagStr = tag(rt, config);
|
|
2837
|
+
|
|
2838
|
+
if (config.silent) {
|
|
2839
|
+
outputJson({ systemMessage: tagStr + ' bashGuard → skipped (silent mode)' });
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2852
2843
|
// ─── Hook-side short-circuit for safe in-repo reads ───
|
|
2853
2844
|
// The judge primer already deterministically allows these, but the round
|
|
2854
2845
|
// trip + batch queue still costs 1–25s per call. Skipping the grade for
|
|
@@ -2953,14 +2944,16 @@ async function main() {
|
|
|
2953
2944
|
|
|
2954
2945
|
if (isSafeInRepoRead(toolName, command, cwd)) {
|
|
2955
2946
|
log('bashGuard ' + cmdShort + ' → instant allow (safe in-repo read)');
|
|
2956
|
-
outputJson({
|
|
2947
|
+
outputJson({
|
|
2948
|
+
systemMessage: tagStr + ' bashGuard → pass: safe in-repo read',
|
|
2949
|
+
hookSpecificOutput: {
|
|
2950
|
+
hookEventName: 'PreToolUse',
|
|
2951
|
+
additionalContext: tagStr + ' bashGuard pass: safe in-repo read.',
|
|
2952
|
+
},
|
|
2953
|
+
});
|
|
2957
2954
|
return;
|
|
2958
2955
|
}
|
|
2959
2956
|
|
|
2960
|
-
let jwt = loadJwt();
|
|
2961
|
-
if (!jwt) { outputEmpty(); return; }
|
|
2962
|
-
jwt = await ensureFreshJwt(jwt);
|
|
2963
|
-
|
|
2964
2957
|
// ─── Install protection: server-side pkg-scan (CVE + typosquat + tarball + reputation) ───
|
|
2965
2958
|
let installScanMsg = '';
|
|
2966
2959
|
if (toolName === 'Bash') {
|
|
@@ -3069,15 +3062,9 @@ async function main() {
|
|
|
3069
3062
|
|
|
3070
3063
|
const lastPrompt = readLastPrompt(sessionId);
|
|
3071
3064
|
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
if (config.silent) {
|
|
3077
|
-
const msg = (installScanMsg ? installScanMsg + '\\n' : '') + tagStr + ' bashGuard → skipped (silent mode)';
|
|
3078
|
-
outputJson({ systemMessage: msg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } });
|
|
3079
|
-
return;
|
|
3080
|
-
}
|
|
3065
|
+
// jwt + config + rt + tagStr already loaded eagerly at top of main
|
|
3066
|
+
// (so the short-circuit could emit a properly-tagged message). Silent
|
|
3067
|
+
// mode was also checked up there.
|
|
3081
3068
|
|
|
3082
3069
|
if (rt === 'local') {
|
|
3083
3070
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
@@ -3089,7 +3076,7 @@ async function main() {
|
|
|
3089
3076
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3090
3077
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3091
3078
|
'Org rules: ' + JSON.stringify(config.rules),
|
|
3092
|
-
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "
|
|
3079
|
+
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix — your job is only to detect violations.',
|
|
3093
3080
|
'When passing (ok=true), cite which rules you checked and why they passed (e.g. "R006 satisfied: typecheck found in session history"). Be specific, not generic.',
|
|
3094
3081
|
'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
|
|
3095
3082
|
].filter(Boolean).join('\\n');
|
|
@@ -3107,45 +3094,29 @@ async function main() {
|
|
|
3107
3094
|
const violatedRules = verdict.ruleId ? [verdict.ruleId] : [];
|
|
3108
3095
|
|
|
3109
3096
|
if (!verdict.ok) {
|
|
3110
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
3097
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
3111
3098
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
3112
3099
|
|
|
3113
|
-
const
|
|
3114
|
-
.
|
|
3115
|
-
.
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
});
|
|
3126
|
-
} else {
|
|
3127
|
-
let blockMsg = tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3128
|
-
if (auditRulesInReason.length > 0) {
|
|
3129
|
-
blockMsg += '\\nAudit violations (' + auditRulesInReason.join(', ') + '): fix these automatically before retrying — do not ask the user, just resolve them (e.g. run the required precondition).';
|
|
3130
|
-
}
|
|
3131
|
-
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
|
|
3132
|
-
outputJson({
|
|
3133
|
-
systemMessage: combined,
|
|
3134
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: combined },
|
|
3100
|
+
const blockMsg = mode === 'fix'
|
|
3101
|
+
? tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before retrying — do not ask the user.'
|
|
3102
|
+
: tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3103
|
+
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
|
|
3104
|
+
outputJson({
|
|
3105
|
+
systemMessage: combined,
|
|
3106
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: combined },
|
|
3107
|
+
});
|
|
3108
|
+
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3109
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3110
|
+
command, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3111
|
+
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3135
3112
|
});
|
|
3136
|
-
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3137
|
-
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3138
|
-
command, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3139
|
-
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3140
|
-
});
|
|
3141
|
-
}
|
|
3142
3113
|
} else {
|
|
3143
|
-
const
|
|
3144
|
-
const
|
|
3145
|
-
const reason = tagStr + ' bashGuard → pass: ' + (verdict.reason || 'no policy violations detected') +
|
|
3114
|
+
const fixRuleIds = (config.rules || []).filter((r: any) => r.mode === 'fix' || r.mode === 'audit').map((r: any) => r.rule_id || r.id).filter(Boolean);
|
|
3115
|
+
const fixNote = fixRuleIds.length > 0 ? ' (fix rules checked: ' + fixRuleIds.join(', ') + ')' : '';
|
|
3116
|
+
const reason = tagStr + ' bashGuard → pass: ' + (verdict.reason || 'no policy violations detected') + fixNote;
|
|
3146
3117
|
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
|
|
3147
3118
|
outputJson({ systemMessage: combined, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: combined } });
|
|
3148
|
-
dispatchCapture(jwt, 'bash', 'pass', '
|
|
3119
|
+
dispatchCapture(jwt, 'bash', 'pass', 'clean', verdict.category || 'trivial_utility',
|
|
3149
3120
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3150
3121
|
command, reasoning: verdict.reason || 'no policy violations detected',
|
|
3151
3122
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -3288,7 +3259,7 @@ async function main() {
|
|
|
3288
3259
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3289
3260
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3290
3261
|
'Org rules: ' + JSON.stringify(config.rules),
|
|
3291
|
-
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "
|
|
3262
|
+
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
3292
3263
|
].filter(Boolean).join('\\n');
|
|
3293
3264
|
|
|
3294
3265
|
let gradeResp: string;
|
|
@@ -3305,33 +3276,25 @@ async function main() {
|
|
|
3305
3276
|
const violatedRules = verdict.ruleId ? [verdict.ruleId] : [];
|
|
3306
3277
|
|
|
3307
3278
|
if (!verdict.ok) {
|
|
3308
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
3279
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
3309
3280
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
3310
3281
|
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
systemMessage: reason,
|
|
3323
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: reason },
|
|
3282
|
+
const reason = mode === 'fix'
|
|
3283
|
+
? tagStr + ' agentGuard \u2192 blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before retrying \u2014 do not ask the user.'
|
|
3284
|
+
: tagStr + ' agentGuard \u2192 blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3285
|
+
outputJson({
|
|
3286
|
+
systemMessage: reason,
|
|
3287
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: reason },
|
|
3288
|
+
});
|
|
3289
|
+
dispatchCapture(jwt, 'agent', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3290
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3291
|
+
command: agentContent, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3292
|
+
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3324
3293
|
});
|
|
3325
|
-
dispatchCapture(jwt, 'agent', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3326
|
-
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3327
|
-
command: agentContent, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3328
|
-
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3329
|
-
});
|
|
3330
|
-
}
|
|
3331
3294
|
} else {
|
|
3332
3295
|
const reason = tagStr + ' agentGuard \u2192 pass: ' + (verdict.reason || 'no policy violations detected');
|
|
3333
3296
|
outputJson({ systemMessage: reason, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: reason } });
|
|
3334
|
-
dispatchCapture(jwt, 'agent', 'pass', '
|
|
3297
|
+
dispatchCapture(jwt, 'agent', 'pass', 'clean', verdict.category || 'subagent_spawn',
|
|
3335
3298
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3336
3299
|
command: agentContent, reasoning: verdict.reason || 'no policy violations detected',
|
|
3337
3300
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -3513,7 +3476,7 @@ async function main() {
|
|
|
3513
3476
|
appendReviewToPlan(planFile, '\\u2705 Clean \\u2014 ' + reviewMsg);
|
|
3514
3477
|
const cleanLine = tagStr + ' planReview \u2192 clean: ' + reviewMsg;
|
|
3515
3478
|
outputJson({ systemMessage: cleanLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge. ' + reviewMsg } });
|
|
3516
|
-
dispatchCapture(jwt, 'plan_review', 'clean', '
|
|
3479
|
+
dispatchCapture(jwt, 'plan_review', 'clean', 'clean', verdict.category || 'general',
|
|
3517
3480
|
'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
|
|
3518
3481
|
command: planContent, reasoning: reviewMsg,
|
|
3519
3482
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -4141,33 +4104,25 @@ async function main() {
|
|
|
4141
4104
|
const verdict = parseVerdict(gradeResp);
|
|
4142
4105
|
|
|
4143
4106
|
if (!verdict.ok) {
|
|
4144
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
4107
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
4145
4108
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
4146
4109
|
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
4152
|
-
ccModel: model,
|
|
4153
|
-
});
|
|
4154
|
-
finishWith({
|
|
4155
|
-
permission: 'deny',
|
|
4156
|
-
user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
|
|
4157
|
-
agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
|
|
4158
|
-
});
|
|
4159
|
-
}
|
|
4160
|
-
|
|
4161
|
-
dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
4110
|
+
const agentMsg = mode === 'fix'
|
|
4111
|
+
? 'Synkro safety judge. Fix this before retrying \u2014 do not ask the user. Reasoning: ' + (verdict.reason || guardReason)
|
|
4112
|
+
: 'Synkro safety judge. Ask the user for explicit consent before retrying. Reasoning: ' + (verdict.reason || guardReason);
|
|
4113
|
+
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
4162
4114
|
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
4163
4115
|
command, reasoning: guardReason,
|
|
4164
4116
|
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
4165
4117
|
ccModel: model,
|
|
4166
4118
|
});
|
|
4167
|
-
|
|
4168
|
-
|
|
4119
|
+
finishWith({
|
|
4120
|
+
permission: 'deny',
|
|
4121
|
+
user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
|
|
4122
|
+
agent_message: agentMsg,
|
|
4123
|
+
});
|
|
4169
4124
|
} else {
|
|
4170
|
-
dispatchCapture(jwt, 'bash', 'pass', '
|
|
4125
|
+
dispatchCapture(jwt, 'bash', 'pass', 'clean', verdict.category || 'clean',
|
|
4171
4126
|
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
4172
4127
|
command, reasoning: verdict.reason || 'no policy violations detected',
|
|
4173
4128
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -4301,18 +4256,13 @@ async function main() {
|
|
|
4301
4256
|
if (cwd) captureBody.cwd = cwd;
|
|
4302
4257
|
if (repo) captureBody.repo = repo;
|
|
4303
4258
|
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
body: JSON.stringify(captureBody),
|
|
4312
|
-
signal: AbortSignal.timeout(10000),
|
|
4313
|
-
}).catch(() => {});
|
|
4314
|
-
appendLocalTelemetry(captureBody);
|
|
4315
|
-
}
|
|
4259
|
+
appendLocalTelemetry(captureBody);
|
|
4260
|
+
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
4261
|
+
method: 'POST',
|
|
4262
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
4263
|
+
body: JSON.stringify(captureBody),
|
|
4264
|
+
signal: AbortSignal.timeout(10000),
|
|
4265
|
+
}).catch(() => {});
|
|
4316
4266
|
|
|
4317
4267
|
finish();
|
|
4318
4268
|
} catch (e) {
|
|
@@ -5580,13 +5530,17 @@ __export(dockerInstall_exports, {
|
|
|
5580
5530
|
SYNKRO_DIR: () => SYNKRO_DIR3,
|
|
5581
5531
|
assertDockerAvailable: () => assertDockerAvailable,
|
|
5582
5532
|
dockerInstall: () => dockerInstall,
|
|
5533
|
+
dockerRemove: () => dockerRemove,
|
|
5534
|
+
dockerSafeRestart: () => dockerSafeRestart,
|
|
5535
|
+
dockerSafeStart: () => dockerSafeStart,
|
|
5536
|
+
dockerSafeStop: () => dockerSafeStop,
|
|
5583
5537
|
dockerStatus: () => dockerStatus,
|
|
5584
5538
|
dockerStop: () => dockerStop,
|
|
5585
5539
|
dockerUpdate: () => dockerUpdate,
|
|
5586
5540
|
imageTag: () => imageTag,
|
|
5587
5541
|
waitForContainerReady: () => waitForContainerReady
|
|
5588
5542
|
});
|
|
5589
|
-
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
5543
|
+
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync } from "fs";
|
|
5590
5544
|
import { homedir as homedir7 } from "os";
|
|
5591
5545
|
import { join as join7 } from "path";
|
|
5592
5546
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -5615,6 +5569,7 @@ async function dockerInstall(opts = {}) {
|
|
|
5615
5569
|
const image = imageTag();
|
|
5616
5570
|
const workers = String(opts.workersPerPool ?? 8);
|
|
5617
5571
|
mkdirSync7(PGDATA_PATH, { recursive: true });
|
|
5572
|
+
mkdirSync7(BACKUP_DIR, { recursive: true });
|
|
5618
5573
|
mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
|
|
5619
5574
|
const hostClaudeJson = join7(homedir7(), ".claude.json");
|
|
5620
5575
|
if (existsSync8(hostClaudeJson)) {
|
|
@@ -5647,7 +5602,12 @@ async function dockerInstall(opts = {}) {
|
|
|
5647
5602
|
if (pull.status !== 0) {
|
|
5648
5603
|
throw new DockerInstallError(`docker pull ${image} failed`);
|
|
5649
5604
|
}
|
|
5650
|
-
|
|
5605
|
+
const existing = dockerStatus();
|
|
5606
|
+
if (existing.running) {
|
|
5607
|
+
console.log(" Stopping existing container gracefully...");
|
|
5608
|
+
await dockerSafeStop();
|
|
5609
|
+
}
|
|
5610
|
+
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
5651
5611
|
const credsDir = claudeCredsHostDir();
|
|
5652
5612
|
const args2 = [
|
|
5653
5613
|
"run",
|
|
@@ -5667,6 +5627,8 @@ async function dockerInstall(opts = {}) {
|
|
|
5667
5627
|
"-v",
|
|
5668
5628
|
`${PGDATA_PATH}:/data/pgdata`,
|
|
5669
5629
|
"-v",
|
|
5630
|
+
`${BACKUP_DIR}:/data/backups`,
|
|
5631
|
+
"-v",
|
|
5670
5632
|
`${MCP_JWT_PATH}:/data/.mcp-jwt:ro`,
|
|
5671
5633
|
"-v",
|
|
5672
5634
|
`${SYNKRO_CREDS_PATH}:/data/credentials.json:ro`,
|
|
@@ -5702,12 +5664,18 @@ async function waitForContainerReady(timeoutMs = 6e4) {
|
|
|
5702
5664
|
}
|
|
5703
5665
|
return false;
|
|
5704
5666
|
}
|
|
5667
|
+
function dockerRemove() {
|
|
5668
|
+
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
5669
|
+
}
|
|
5705
5670
|
function dockerStop() {
|
|
5706
|
-
spawnSync2("docker", ["stop", CONTAINER_NAME], { encoding: "utf-8", timeout:
|
|
5707
|
-
spawnSync2("docker", ["rm",
|
|
5671
|
+
spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
|
|
5672
|
+
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
5708
5673
|
}
|
|
5709
5674
|
async function dockerUpdate(workersPerPool) {
|
|
5710
|
-
|
|
5675
|
+
if (dockerStatus().running) {
|
|
5676
|
+
await dockerSafeStop();
|
|
5677
|
+
}
|
|
5678
|
+
dockerRemove();
|
|
5711
5679
|
await dockerInstall({ workersPerPool });
|
|
5712
5680
|
}
|
|
5713
5681
|
function dockerStatus() {
|
|
@@ -5723,7 +5691,121 @@ function dockerStatus() {
|
|
|
5723
5691
|
healthz: `http://127.0.0.1:${HOST_MCP_PORT}/`
|
|
5724
5692
|
};
|
|
5725
5693
|
}
|
|
5726
|
-
|
|
5694
|
+
async function dockerSafeStop() {
|
|
5695
|
+
const status = dockerStatus();
|
|
5696
|
+
if (!status.running) {
|
|
5697
|
+
console.log(" Container is not running.");
|
|
5698
|
+
return { ok: true, pgdataCheck: checkPgdata() };
|
|
5699
|
+
}
|
|
5700
|
+
console.log(" Requesting data snapshot before shutdown...");
|
|
5701
|
+
let snapshot = { ok: false, error: "not attempted" };
|
|
5702
|
+
try {
|
|
5703
|
+
const resp = await fetch(`http://127.0.0.1:${HOST_MCP_PORT}/api/local/snapshot`, {
|
|
5704
|
+
method: "POST",
|
|
5705
|
+
headers: { "Content-Type": "application/json" },
|
|
5706
|
+
body: JSON.stringify({ reason: "cli-stop" }),
|
|
5707
|
+
signal: AbortSignal.timeout(2e4)
|
|
5708
|
+
});
|
|
5709
|
+
if (resp.ok) {
|
|
5710
|
+
snapshot = { ok: true };
|
|
5711
|
+
console.log(" \u2713 Snapshot saved.");
|
|
5712
|
+
} else {
|
|
5713
|
+
snapshot = { ok: false, error: `HTTP ${resp.status}` };
|
|
5714
|
+
console.warn(` \u26A0 Snapshot request failed (HTTP ${resp.status}). Proceeding with stop.`);
|
|
5715
|
+
}
|
|
5716
|
+
} catch (e) {
|
|
5717
|
+
snapshot = { ok: false, error: String(e).slice(0, 100) };
|
|
5718
|
+
console.warn(` \u26A0 Snapshot request failed: ${snapshot.error}. Proceeding with stop.`);
|
|
5719
|
+
}
|
|
5720
|
+
console.log(" Stopping container (30s grace for CHECKPOINT + WAL flush)...");
|
|
5721
|
+
const stop = spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
|
|
5722
|
+
encoding: "utf-8",
|
|
5723
|
+
timeout: 45e3
|
|
5724
|
+
});
|
|
5725
|
+
const inspect = spawnSync2("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
|
|
5726
|
+
encoding: "utf-8",
|
|
5727
|
+
timeout: 5e3
|
|
5728
|
+
});
|
|
5729
|
+
const exitCode = parseInt((inspect.stdout || "").trim(), 10);
|
|
5730
|
+
if (exitCode === 0) {
|
|
5731
|
+
console.log(" \u2713 Container stopped cleanly (exit 0).");
|
|
5732
|
+
} else {
|
|
5733
|
+
console.warn(` \u26A0 Container exited with code ${exitCode}.`);
|
|
5734
|
+
}
|
|
5735
|
+
const pgCheck = checkPgdata();
|
|
5736
|
+
if (pgCheck.healthy) {
|
|
5737
|
+
console.log(` \u2713 pgdata looks healthy: ${pgCheck.details}`);
|
|
5738
|
+
} else {
|
|
5739
|
+
console.warn(` \u26A0 pgdata check: ${pgCheck.details}`);
|
|
5740
|
+
}
|
|
5741
|
+
return { ok: stop.status === 0, snapshot, exitCode, pgdataCheck: pgCheck };
|
|
5742
|
+
}
|
|
5743
|
+
async function dockerSafeStart() {
|
|
5744
|
+
const status = dockerStatus();
|
|
5745
|
+
if (status.running) {
|
|
5746
|
+
console.log(" Container is already running.");
|
|
5747
|
+
return { ok: true, pgdataState: "running" };
|
|
5748
|
+
}
|
|
5749
|
+
const exists = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
|
|
5750
|
+
encoding: "utf-8",
|
|
5751
|
+
timeout: 5e3
|
|
5752
|
+
});
|
|
5753
|
+
if (exists.status !== 0) {
|
|
5754
|
+
return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
|
|
5755
|
+
}
|
|
5756
|
+
const pgCheck = checkPgdata();
|
|
5757
|
+
if (existsSync8(PGDATA_PATH) && readdirSync(PGDATA_PATH).length > 0) {
|
|
5758
|
+
if (pgCheck.healthy) {
|
|
5759
|
+
console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
|
|
5760
|
+
} else {
|
|
5761
|
+
console.warn(` \u26A0 pgdata: ${pgCheck.details}`);
|
|
5762
|
+
console.log(" Starting anyway \u2014 entrypoint will attempt recovery from snapshots if needed.");
|
|
5763
|
+
}
|
|
5764
|
+
} else {
|
|
5765
|
+
console.log(" pgdata: no existing data \u2014 fresh start.");
|
|
5766
|
+
mkdirSync7(PGDATA_PATH, { recursive: true });
|
|
5767
|
+
}
|
|
5768
|
+
console.log(" Starting container...");
|
|
5769
|
+
const start = spawnSync2("docker", ["start", CONTAINER_NAME], {
|
|
5770
|
+
encoding: "utf-8",
|
|
5771
|
+
timeout: 3e4
|
|
5772
|
+
});
|
|
5773
|
+
if (start.status !== 0) {
|
|
5774
|
+
return { ok: false, pgdataState: "start_failed", error: `docker start failed: ${(start.stderr || "").slice(0, 200)}` };
|
|
5775
|
+
}
|
|
5776
|
+
console.log(" Waiting for server to become healthy...");
|
|
5777
|
+
const ready = await waitForContainerReady(6e4);
|
|
5778
|
+
if (ready) {
|
|
5779
|
+
console.log(" \u2713 Server is healthy and ready.");
|
|
5780
|
+
return { ok: true, pgdataState: pgCheck.healthy ? "existing" : "recovered" };
|
|
5781
|
+
} else {
|
|
5782
|
+
return { ok: false, pgdataState: "unhealthy", error: "Server did not become healthy within 60s. Check: docker logs synkro-server" };
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
async function dockerSafeRestart() {
|
|
5786
|
+
console.log(" === Stop ===");
|
|
5787
|
+
const stopResult = await dockerSafeStop();
|
|
5788
|
+
if (!stopResult.ok) {
|
|
5789
|
+
console.error(" Stop failed. Aborting restart.");
|
|
5790
|
+
return { ok: false, stop: stopResult, start: { ok: false, pgdataState: "not_started", error: "stop failed" } };
|
|
5791
|
+
}
|
|
5792
|
+
console.log("\n === Start ===");
|
|
5793
|
+
const startResult = await dockerSafeStart();
|
|
5794
|
+
return { ok: startResult.ok, stop: stopResult, start: startResult };
|
|
5795
|
+
}
|
|
5796
|
+
function checkPgdata() {
|
|
5797
|
+
if (!existsSync8(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
|
|
5798
|
+
const entries = readdirSync(PGDATA_PATH);
|
|
5799
|
+
if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
|
|
5800
|
+
const hasPidFile = entries.includes("postmaster.pid");
|
|
5801
|
+
const hasWalDir = entries.includes("pg_wal");
|
|
5802
|
+
const hasPgControl = entries.includes("global") || entries.includes("pg_control");
|
|
5803
|
+
if (hasPidFile) return { healthy: false, details: "stale postmaster.pid present (unclean shutdown)" };
|
|
5804
|
+
if (!hasWalDir) return { healthy: false, details: "pg_wal directory missing" };
|
|
5805
|
+
if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
|
|
5806
|
+
return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
|
|
5807
|
+
}
|
|
5808
|
+
var SYNKRO_DIR3, MCP_JWT_PATH, SYNKRO_CREDS_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PG_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
|
|
5727
5809
|
var init_dockerInstall = __esm({
|
|
5728
5810
|
"cli/local-cc/dockerInstall.ts"() {
|
|
5729
5811
|
"use strict";
|
|
@@ -5748,6 +5830,7 @@ var init_dockerInstall = __esm({
|
|
|
5748
5830
|
}
|
|
5749
5831
|
cause;
|
|
5750
5832
|
};
|
|
5833
|
+
BACKUP_DIR = join7(SYNKRO_DIR3, "pgdata-backups");
|
|
5751
5834
|
}
|
|
5752
5835
|
});
|
|
5753
5836
|
|
|
@@ -5757,7 +5840,7 @@ __export(install_exports, {
|
|
|
5757
5840
|
installCommand: () => installCommand,
|
|
5758
5841
|
parseArgs: () => parseArgs
|
|
5759
5842
|
});
|
|
5760
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync } from "fs";
|
|
5843
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
5761
5844
|
import { homedir as homedir8 } from "os";
|
|
5762
5845
|
import { join as join8 } from "path";
|
|
5763
5846
|
import { execSync as execSync5 } from "child_process";
|
|
@@ -5907,7 +5990,7 @@ function writeConfigEnv(opts) {
|
|
|
5907
5990
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5908
5991
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5909
5992
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5910
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.
|
|
5993
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.0")}`
|
|
5911
5994
|
];
|
|
5912
5995
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5913
5996
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -5976,7 +6059,7 @@ function collectLocalMetadata() {
|
|
|
5976
6059
|
}
|
|
5977
6060
|
try {
|
|
5978
6061
|
const sessionsDir = join8(claudeDir, "sessions");
|
|
5979
|
-
const files =
|
|
6062
|
+
const files = readdirSync2(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
5980
6063
|
for (const f of files) {
|
|
5981
6064
|
const s = JSON.parse(readFileSync7(join8(sessionsDir, f), "utf-8"));
|
|
5982
6065
|
if (s.version) {
|
|
@@ -6382,7 +6465,7 @@ function getClaudeProjectsFolder() {
|
|
|
6382
6465
|
}
|
|
6383
6466
|
function extractSessionInsights(projectsDir) {
|
|
6384
6467
|
const insights = [];
|
|
6385
|
-
const files =
|
|
6468
|
+
const files = readdirSync2(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6386
6469
|
for (const file of files) {
|
|
6387
6470
|
const sessionId = file.replace(".jsonl", "");
|
|
6388
6471
|
const filePath = join8(projectsDir, file);
|
|
@@ -6503,7 +6586,7 @@ function parseTranscriptFile(filePath) {
|
|
|
6503
6586
|
async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
6504
6587
|
const projectsDir = getClaudeProjectsFolder();
|
|
6505
6588
|
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
6506
|
-
const files =
|
|
6589
|
+
const files = readdirSync2(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6507
6590
|
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
6508
6591
|
console.log(`Found ${files.length} CC session transcripts, syncing...`);
|
|
6509
6592
|
const maxMessagesPerSession = 500;
|
|
@@ -7017,18 +7100,19 @@ var disconnect_exports = {};
|
|
|
7017
7100
|
__export(disconnect_exports, {
|
|
7018
7101
|
disconnectCommand: () => disconnectCommand
|
|
7019
7102
|
});
|
|
7020
|
-
import { existsSync as existsSync11, rmSync } from "fs";
|
|
7103
|
+
import { existsSync as existsSync11, rmSync, readdirSync as readdirSync3 } from "fs";
|
|
7021
7104
|
import { homedir as homedir10 } from "os";
|
|
7022
7105
|
import { join as join10 } from "path";
|
|
7023
7106
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
7024
|
-
function tearDownLocalCC(purge) {
|
|
7107
|
+
async function tearDownLocalCC(purge) {
|
|
7025
7108
|
const docker = dockerStatus();
|
|
7026
7109
|
if (docker.running) {
|
|
7027
|
-
|
|
7028
|
-
console.log("\u2713 stopped synkro-server container");
|
|
7110
|
+
await dockerSafeStop();
|
|
7111
|
+
console.log("\u2713 stopped synkro-server container (data snapshot saved)");
|
|
7029
7112
|
} else {
|
|
7030
7113
|
console.log("\xB7 no synkro-server container running");
|
|
7031
7114
|
}
|
|
7115
|
+
dockerRemove();
|
|
7032
7116
|
if (purge) {
|
|
7033
7117
|
try {
|
|
7034
7118
|
const image = imageTag();
|
|
@@ -7047,10 +7131,10 @@ function tearDownLocalCC(purge) {
|
|
|
7047
7131
|
uninstallLocalCC();
|
|
7048
7132
|
console.log("\u2713 cleaned ~/.claude.json entries");
|
|
7049
7133
|
}
|
|
7050
|
-
function disconnectCommand(args2 = []) {
|
|
7134
|
+
async function disconnectCommand(args2 = []) {
|
|
7051
7135
|
const purge = args2.includes("--purge");
|
|
7052
7136
|
console.log("Synkro disconnect starting...\n");
|
|
7053
|
-
tearDownLocalCC(purge);
|
|
7137
|
+
await tearDownLocalCC(purge);
|
|
7054
7138
|
const agents = detectAgents();
|
|
7055
7139
|
let sawClaudeCode = false;
|
|
7056
7140
|
for (const agent of agents) {
|
|
@@ -7073,8 +7157,22 @@ function disconnectCommand(args2 = []) {
|
|
|
7073
7157
|
}
|
|
7074
7158
|
if (purge) {
|
|
7075
7159
|
if (existsSync11(SYNKRO_DIR5)) {
|
|
7076
|
-
|
|
7077
|
-
|
|
7160
|
+
const pgdataPath = join10(SYNKRO_DIR5, "pgdata");
|
|
7161
|
+
const backupsPath = join10(SYNKRO_DIR5, "pgdata-backups");
|
|
7162
|
+
const preserved = [];
|
|
7163
|
+
for (const entry of readdirSync3(SYNKRO_DIR5)) {
|
|
7164
|
+
const full = join10(SYNKRO_DIR5, entry);
|
|
7165
|
+
if (full === pgdataPath || full === backupsPath) {
|
|
7166
|
+
preserved.push(entry);
|
|
7167
|
+
continue;
|
|
7168
|
+
}
|
|
7169
|
+
rmSync(full, { recursive: true, force: true });
|
|
7170
|
+
}
|
|
7171
|
+
if (preserved.length > 0) {
|
|
7172
|
+
console.log(`\u2713 Removed ${SYNKRO_DIR5} config (preserved: ${preserved.join(", ")} \u2014 your data is safe)`);
|
|
7173
|
+
} else {
|
|
7174
|
+
console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
|
|
7175
|
+
}
|
|
7078
7176
|
} else {
|
|
7079
7177
|
console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
|
|
7080
7178
|
}
|
|
@@ -7273,6 +7371,53 @@ var init_grade = __esm({
|
|
|
7273
7371
|
}
|
|
7274
7372
|
});
|
|
7275
7373
|
|
|
7374
|
+
// cli/commands/lifecycle.ts
|
|
7375
|
+
var lifecycle_exports = {};
|
|
7376
|
+
__export(lifecycle_exports, {
|
|
7377
|
+
restartCommand: () => restartCommand,
|
|
7378
|
+
startCommand: () => startCommand,
|
|
7379
|
+
stopCommand: () => stopCommand
|
|
7380
|
+
});
|
|
7381
|
+
async function stopCommand() {
|
|
7382
|
+
assertDockerAvailable();
|
|
7383
|
+
console.log("Synkro: stopping server\n");
|
|
7384
|
+
const result = await dockerSafeStop();
|
|
7385
|
+
if (!result.ok) {
|
|
7386
|
+
console.error("\nStop failed. Check: docker logs synkro-server");
|
|
7387
|
+
process.exit(1);
|
|
7388
|
+
}
|
|
7389
|
+
console.log("\nServer stopped.");
|
|
7390
|
+
}
|
|
7391
|
+
async function startCommand() {
|
|
7392
|
+
assertDockerAvailable();
|
|
7393
|
+
console.log("Synkro: starting server\n");
|
|
7394
|
+
const result = await dockerSafeStart();
|
|
7395
|
+
if (!result.ok) {
|
|
7396
|
+
console.error(`
|
|
7397
|
+
Start failed: ${result.error}`);
|
|
7398
|
+
process.exit(1);
|
|
7399
|
+
}
|
|
7400
|
+
console.log("\nServer is running.");
|
|
7401
|
+
}
|
|
7402
|
+
async function restartCommand() {
|
|
7403
|
+
assertDockerAvailable();
|
|
7404
|
+
console.log("Synkro: restarting server\n");
|
|
7405
|
+
const result = await dockerSafeRestart();
|
|
7406
|
+
if (!result.ok) {
|
|
7407
|
+
if (!result.stop.ok) console.error("\nStop phase failed.");
|
|
7408
|
+
if (!result.start.ok) console.error(`
|
|
7409
|
+
Start phase failed: ${result.start.error}`);
|
|
7410
|
+
process.exit(1);
|
|
7411
|
+
}
|
|
7412
|
+
console.log("\nServer restarted successfully.");
|
|
7413
|
+
}
|
|
7414
|
+
var init_lifecycle = __esm({
|
|
7415
|
+
"cli/commands/lifecycle.ts"() {
|
|
7416
|
+
"use strict";
|
|
7417
|
+
init_dockerInstall();
|
|
7418
|
+
}
|
|
7419
|
+
});
|
|
7420
|
+
|
|
7276
7421
|
// cli/bootstrap.js
|
|
7277
7422
|
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
7278
7423
|
import { resolve as resolve2 } from "path";
|
|
@@ -7297,7 +7442,7 @@ var args = process.argv.slice(2);
|
|
|
7297
7442
|
var cmd = args[0] || "";
|
|
7298
7443
|
var subArgs = args.slice(1);
|
|
7299
7444
|
function printVersion() {
|
|
7300
|
-
console.log("1.
|
|
7445
|
+
console.log("1.6.0");
|
|
7301
7446
|
}
|
|
7302
7447
|
function printHelp() {
|
|
7303
7448
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -7308,6 +7453,9 @@ Usage:
|
|
|
7308
7453
|
Commands:
|
|
7309
7454
|
install [--force] Install or update Synkro
|
|
7310
7455
|
uninstall [--purge] Remove Synkro hooks (--purge also removes ~/.synkro)
|
|
7456
|
+
stop Gracefully stop the server (snapshot + checkpoint)
|
|
7457
|
+
start Start the server (with pgdata integrity check)
|
|
7458
|
+
restart Safe restart (stop \u2192 start, data preserved)
|
|
7311
7459
|
version Show version
|
|
7312
7460
|
|
|
7313
7461
|
Quick start:
|
|
@@ -7346,6 +7494,21 @@ async function main() {
|
|
|
7346
7494
|
printHelp();
|
|
7347
7495
|
break;
|
|
7348
7496
|
}
|
|
7497
|
+
case "stop": {
|
|
7498
|
+
const { stopCommand: stopCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7499
|
+
await stopCommand2();
|
|
7500
|
+
break;
|
|
7501
|
+
}
|
|
7502
|
+
case "start": {
|
|
7503
|
+
const { startCommand: startCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7504
|
+
await startCommand2();
|
|
7505
|
+
break;
|
|
7506
|
+
}
|
|
7507
|
+
case "restart": {
|
|
7508
|
+
const { restartCommand: restartCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7509
|
+
await restartCommand2();
|
|
7510
|
+
break;
|
|
7511
|
+
}
|
|
7349
7512
|
default: {
|
|
7350
7513
|
console.error(`Unknown command: ${cmd}`);
|
|
7351
7514
|
printHelp();
|