@synkro-sh/cli 1.3.29 → 1.3.31

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 CHANGED
@@ -602,9 +602,34 @@ fi
602
602
  if [ "$USE_LOCAL" = "true" ]; then
603
603
  # \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (mode=bash). \u2500\u2500\u2500
604
604
 
605
- # Fetch org guardrail rules relevant to this command (skip in local_only \u2014 no content leaves device).
605
+ # Refresh primer if older than 24h (server-side IP, fetched on demand).
606
+ PRIMER_FILE="$HOME/.synkro/grader-primer-bash.txt"
607
+ if [ ! -f "$PRIMER_FILE" ] || ! find "$PRIMER_FILE" -mmin -1440 2>/dev/null | grep -q .; then
608
+ NEW_PRIMER=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/judge-prompts" \\
609
+ -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null \\
610
+ | jq -r '.grader_primer_bash // empty' 2>/dev/null || echo "")
611
+ if [ -n "$NEW_PRIMER" ]; then
612
+ printf '%s' "$NEW_PRIMER" > "$PRIMER_FILE" 2>/dev/null || true
613
+ # Kill the daemon so it restarts with the fresh primer.
614
+ DAEMON_PID_FILE="$HOME/.synkro/daemon/bash/daemon.pid"
615
+ [ -f "$DAEMON_PID_FILE" ] && kill -TERM "$(cat "$DAEMON_PID_FILE" 2>/dev/null)" 2>/dev/null || true
616
+ fi
617
+ fi
618
+
619
+ # Fetch org guardrail rules. In local_only mode use GET (no command leaks);
620
+ # in other modes use POST with content for embedding-based top_k matching.
621
+ RULES_CACHE="$HOME/.synkro/.rules-cache-bash"
606
622
  ORG_RULES="[]"
607
- if [ "$SYNKRO_CAPTURE_DEPTH" != "local_only" ]; then
623
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
624
+ if find "$RULES_CACHE" -mmin -60 2>/dev/null | grep -q .; then
625
+ ORG_RULES=$(cat "$RULES_CACHE" 2>/dev/null || echo "[]")
626
+ else
627
+ ORG_RULES=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
628
+ -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null \\
629
+ | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
630
+ [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
631
+ fi
632
+ else
608
633
  ORG_RULES=$(printf '%s' "$COMMAND" | head -c 4000 \\
609
634
  | jq -Rs '{content: .}' \\
610
635
  | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=15" \\
@@ -612,8 +637,8 @@ if [ "$USE_LOCAL" = "true" ]; then
612
637
  -H "Authorization: Bearer $JWT" \\
613
638
  -d @- --max-time 2 2>/dev/null \\
614
639
  | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
615
- if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
616
640
  fi
641
+ if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
617
642
 
618
643
  GRADER_PROMPT_FILE=$(mktemp -t synkro-bash-prompt.XXXXXX)
619
644
  trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
@@ -670,6 +695,20 @@ VERDICT_KIND=$(echo "$VERDICT" | jq -r '.verdict // "warn"' 2>/dev/null)
670
695
  REASONING=$(echo "$VERDICT" | jq -r '.reasoning // "matched dangerous-verb regex"' 2>/dev/null)
671
696
  ALTERNATIVE=$(echo "$VERDICT" | jq -r '.alternative // ""' 2>/dev/null)
672
697
  CATEGORY=$(echo "$VERDICT" | jq -r '.category // "destructive_command"' 2>/dev/null)
698
+ RISK_LEVEL=$(echo "$VERDICT" | jq -r '.risk_level // empty' 2>/dev/null)
699
+
700
+ # Backwards-compat: if severity isn't block/audit, derive it from verdict_kind
701
+ # and treat the original severity as the risk_level.
702
+ case "$SEVERITY" in
703
+ block|audit) ;;
704
+ low|medium|high|critical)
705
+ [ -z "$RISK_LEVEL" ] && RISK_LEVEL="$SEVERITY"
706
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
707
+ ;;
708
+ *)
709
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
710
+ ;;
711
+ esac
673
712
 
674
713
  # Severity-driven surfacing:
675
714
  # block \u2192 permissionDecision: "ask" (interactive) or "deny" (headless)
@@ -735,6 +774,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
735
774
  --arg hook_type "bash" \\
736
775
  --arg verdict "$VERDICT_KIND" \\
737
776
  --arg severity "$SEVERITY" \\
777
+ --arg risk_level "\${RISK_LEVEL:-low}" \\
738
778
  --arg category "$CATEGORY" \\
739
779
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
740
780
  --arg tool_name "$TOOL_NAME" \\
@@ -744,6 +784,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
744
784
  hook_type: $hook_type,
745
785
  verdict: $verdict,
746
786
  severity: $severity,
787
+ risk_level: $risk_level,
747
788
  category: $category,
748
789
  model: $model,
749
790
  tool_name: $tool_name
@@ -1013,8 +1054,31 @@ fi
1013
1054
 
1014
1055
  if [ "$USE_LOCAL" = "true" ]; then
1015
1056
  # \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (Python helper).
1057
+
1058
+ # Refresh primer if older than 24h (server-side IP, fetched on demand).
1059
+ PRIMER_FILE="$HOME/.synkro/grader-primer-edit.txt"
1060
+ if [ ! -f "$PRIMER_FILE" ] || ! find "$PRIMER_FILE" -mmin -1440 2>/dev/null | grep -q .; then
1061
+ NEW_PRIMER=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/judge-prompts" \\
1062
+ -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null \\
1063
+ | jq -r '.grader_primer_edit // empty' 2>/dev/null || echo "")
1064
+ if [ -n "$NEW_PRIMER" ]; then
1065
+ printf '%s' "$NEW_PRIMER" > "$PRIMER_FILE" 2>/dev/null || true
1066
+ DAEMON_PID_FILE="$HOME/.synkro/daemon/edit/daemon.pid"
1067
+ [ -f "$DAEMON_PID_FILE" ] && kill -TERM "$(cat "$DAEMON_PID_FILE" 2>/dev/null)" 2>/dev/null || true
1068
+ fi
1069
+ fi
1070
+ RULES_CACHE="$HOME/.synkro/.rules-cache-edit"
1016
1071
  ORG_RULES="[]"
1017
- if [ "$SYNKRO_CAPTURE_DEPTH" != "local_only" ]; then
1072
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1073
+ if find "$RULES_CACHE" -mmin -60 2>/dev/null | grep -q .; then
1074
+ ORG_RULES=$(cat "$RULES_CACHE" 2>/dev/null || echo "[]")
1075
+ else
1076
+ ORG_RULES=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
1077
+ -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null \\
1078
+ | jq -c '[.rules[]? | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
1079
+ [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
1080
+ fi
1081
+ else
1018
1082
  ORG_RULES=$(printf '%s' "$PROPOSED" | head -c 8000 \\
1019
1083
  | jq -Rs '{content: .}' \\
1020
1084
  | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
@@ -1022,8 +1086,8 @@ if [ "$USE_LOCAL" = "true" ]; then
1022
1086
  -H "Authorization: Bearer $JWT" \\
1023
1087
  -d @- --max-time 2 2>/dev/null \\
1024
1088
  | jq -c '[.rules[]? | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
1025
- if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1026
1089
  fi
1090
+ if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1027
1091
 
1028
1092
  GRADER_PROMPT_FILE=$(mktemp -t synkro-grade.XXXXXX)
1029
1093
  trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
@@ -2199,7 +2263,12 @@ OUTPUT RULES \u2014 strictest possible, no exceptions:
2199
2263
 
2200
2264
  1. NO reasoning. NO preamble. NO commentary.
2201
2265
  2. Your reply is exactly one <synkro-verdict>JSON</synkro-verdict> block. Nothing else.
2202
- 3. JSON shape: {"verdict": "warn"|"allow", "severity": "low|medium|high|critical", "category": "snake_case", "reasoning": "<= 25 words, cites intent + match/mismatch", "alternative": "safer command or null"}
2266
+ 3. JSON shape: {"verdict": "warn"|"allow", "severity": "block"|"audit", "risk_level": "low"|"medium"|"high"|"critical", "category": "snake_case", "reasoning": "<= 25 words, cites intent + match/mismatch", "alternative": "safer command or null"}
2267
+
2268
+ SEVERITY MAPPING (strict):
2269
+ - verdict="warn" \u2192 severity="block"
2270
+ - verdict="allow" \u2192 severity="audit"
2271
+ risk_level always reflects the underlying danger level (low/medium/high/critical), independent of the routing decision.
2203
2272
 
2204
2273
  Rules:
2205
2274
  - WARN if destructive/irreversible AND not aligned with user intent, OR has wildly disproportionate blast radius vs the request.
@@ -3407,12 +3476,27 @@ function ensureSynkroDir() {
3407
3476
  mkdirSync5(BIN_DIR, { recursive: true });
3408
3477
  mkdirSync5(OFFSETS_DIR, { recursive: true });
3409
3478
  }
3410
- function writeGraderDaemon() {
3479
+ async function fetchGraderPrimers(gatewayUrl, token) {
3480
+ try {
3481
+ const resp = await fetch(`${gatewayUrl}/api/v1/cli/judge-prompts`, {
3482
+ headers: { "Authorization": `Bearer ${token}` }
3483
+ });
3484
+ if (!resp.ok) throw new Error(`status ${resp.status}`);
3485
+ const data = await resp.json();
3486
+ if (!data.grader_primer_bash || !data.grader_primer_edit) throw new Error("missing primer fields");
3487
+ return { bash: data.grader_primer_bash, edit: data.grader_primer_edit };
3488
+ } catch (err) {
3489
+ console.warn(`[synkro] primer fetch failed (${err.message}); using bundled fallback`);
3490
+ return { bash: GRADER_PRIMER_BASH, edit: GRADER_PRIMER_EDIT };
3491
+ }
3492
+ }
3493
+ async function writeGraderDaemon(gatewayUrl, token) {
3411
3494
  writeFileSync5(GRADER_DAEMON_PATH, GRADER_DAEMON_PY, "utf-8");
3412
3495
  chmodSync(GRADER_DAEMON_PATH, 493);
3413
- writeFileSync5(GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_EDIT, "utf-8");
3496
+ const primers = await fetchGraderPrimers(gatewayUrl, token);
3497
+ writeFileSync5(GRADER_PRIMER_EDIT_PATH, primers.edit, "utf-8");
3414
3498
  chmodSync(GRADER_PRIMER_EDIT_PATH, 420);
3415
- writeFileSync5(GRADER_PRIMER_BASH_PATH, GRADER_PRIMER_BASH, "utf-8");
3499
+ writeFileSync5(GRADER_PRIMER_BASH_PATH, primers.bash, "utf-8");
3416
3500
  chmodSync(GRADER_PRIMER_BASH_PATH, 420);
3417
3501
  }
3418
3502
  function writeHookScripts() {
@@ -3470,7 +3554,7 @@ function writeConfigEnv(opts) {
3470
3554
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3471
3555
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3472
3556
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3473
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.29")}`
3557
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.31")}`
3474
3558
  ];
3475
3559
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3476
3560
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -3698,7 +3782,7 @@ async function installCommand(opts = {}) {
3698
3782
  console.log(` ${scripts.sessionStartScript}`);
3699
3783
  console.log(` ${scripts.transcriptSyncScript}
3700
3784
  `);
3701
- writeGraderDaemon();
3785
+ await writeGraderDaemon(gatewayUrl, token);
3702
3786
  for (const mode of ["edit", "bash"]) {
3703
3787
  const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
3704
3788
  try {