@synkro-sh/cli 1.4.23 → 1.4.25

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
@@ -499,23 +499,14 @@ synkro_channel_up() {
499
499
  (exec 3<>/dev/tcp/127.0.0.1/\${SYNKRO_CHANNEL_PORT:-8929}) 2>/dev/null && exec 3<&- 3>&-
500
500
  }
501
501
 
502
- # Fetch hook config (cached 5min). Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES, SYNKRO_GRADER_PRIMER_BASH, SYNKRO_GRADER_PRIMER_EDIT, SYNKRO_CLASSIFICATION_PROMPT.
502
+ # Fetch hook config. Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES.
503
503
  synkro_load_config() {
504
- local cache="$HOME/.synkro/.hook-config-cache"
505
- if [ -f "$cache" ] && find "$cache" -mmin -5 2>/dev/null | grep -q .; then
506
- . "$cache" 2>/dev/null
507
- return
508
- fi
509
504
  local resp
510
505
  resp=$(curl -sS "\${GATEWAY_URL}/api/v1/hook/config\${1:+?$1}" -H "Authorization: Bearer $JWT" --max-time 4 2>/dev/null || echo "")
511
506
  if [ -z "$resp" ]; then return; fi
512
507
  SYNKRO_CAPTURE_DEPTH=$(echo "$resp" | jq -r '.capture_depth // "local_only"' 2>/dev/null)
513
508
  SYNKRO_TIER=$(echo "$resp" | jq -r '.tier // "standard"' 2>/dev/null)
514
509
  SYNKRO_RULES=$(echo "$resp" | jq -c '[.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
515
- # Cache the values (SYNKRO_RULES uses single quotes to avoid expansion issues with JSON)
516
- { printf 'SYNKRO_CAPTURE_DEPTH="%s"\\nSYNKRO_TIER="%s"\\n' "$SYNKRO_CAPTURE_DEPTH" "$SYNKRO_TIER"
517
- printf "SYNKRO_RULES='%s'\\n" "$SYNKRO_RULES"
518
- } > "$cache" 2>/dev/null || true
519
510
  }
520
511
 
521
512
  # Decide routing: "local" (grade on device) or "cloud" (POST to server)
@@ -551,13 +542,19 @@ synkro_parse_local_verdict() {
551
542
  LOCAL_REASON=$(printf '%s' "$inner" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
552
543
  [ -z "$LOCAL_REASON" ] && LOCAL_REASON=$(printf '%s' "$inner" | sed -nE 's|.*<reasoning>(.*)</reasoning>.*|\\1|p' | head -1)
553
544
  if [ "$LOCAL_OK" = "false" ]; then
554
- local fv
555
- fv=$(printf '%s' "$inner" | awk -v RS='</violation>' '/<violation>/{print; exit}')
556
- LOCAL_RULE_ID=$(printf '%s' "$fv" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
557
- [ -z "$LOCAL_REASON" ] && LOCAL_REASON=$(printf '%s' "$fv" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
558
- LOCAL_SEV=$(printf '%s' "$fv" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
559
- LOCAL_CAT=$(printf '%s' "$fv" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
545
+ LOCAL_RULE_ID=$(printf '%s' "$inner" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
546
+ LOCAL_SEV=$(printf '%s' "$inner" | sed -nE 's|.*<risk_level>(.*)</risk_level>.*|\\1|p' | head -1)
547
+ LOCAL_CAT=$(printf '%s' "$inner" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
548
+ if [ -z "$LOCAL_RULE_ID" ]; then
549
+ local fv
550
+ fv=$(printf '%s' "$inner" | awk -v RS='</violation>' '/<violation>/{print; exit}')
551
+ LOCAL_RULE_ID=$(printf '%s' "$fv" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
552
+ [ -z "$LOCAL_REASON" ] && LOCAL_REASON=$(printf '%s' "$fv" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
553
+ [ -z "$LOCAL_SEV" ] && LOCAL_SEV=$(printf '%s' "$fv" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
554
+ [ -z "$LOCAL_CAT" ] && LOCAL_CAT=$(printf '%s' "$fv" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
555
+ fi
560
556
  LOCAL_SEV="\${LOCAL_SEV:-high}"; LOCAL_CAT="\${LOCAL_CAT:-policy_violation}"
557
+ [ -z "$LOCAL_RULE_ID" ] && LOCAL_RULE_ID=$(printf '%s' "$LOCAL_REASON" | grep -oE '[Rr][0-9]{3}' | head -1)
561
558
  fi
562
559
  }
563
560
 
@@ -588,6 +585,37 @@ synkro_rule_mode() {
588
585
  echo "$m"
589
586
  }
590
587
 
588
+ SYNKRO_CONSENT_FILE="$HOME/.synkro/.local-consent"
589
+
590
+ synkro_consent_grant() {
591
+ local sid="$1" hash="$2"
592
+ printf '%s\\t%s\\tactive\\n' "$sid" "$hash" >> "$SYNKRO_CONSENT_FILE" 2>/dev/null || true
593
+ }
594
+
595
+ synkro_consent_has_active() {
596
+ local sid="$1" hash="$2"
597
+ grep -q "^$sid\\\\t$hash\\\\tactive$" "$SYNKRO_CONSENT_FILE" 2>/dev/null
598
+ }
599
+
600
+ synkro_consent_consume() {
601
+ local sid="$1" hash="$2"
602
+ [ ! -f "$SYNKRO_CONSENT_FILE" ] && return
603
+ local tmp="\${SYNKRO_CONSENT_FILE}.tmp"
604
+ sed "s/^$sid\\\\t$hash\\\\tactive$/$sid\\t$hash\\tconsumed/" "$SYNKRO_CONSENT_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$SYNKRO_CONSENT_FILE" 2>/dev/null || true
605
+ }
606
+
607
+ synkro_consent_has_consumed() {
608
+ local sid="$1" hash="$2"
609
+ grep -q "^$sid\\\\t$hash\\\\tconsumed$" "$SYNKRO_CONSENT_FILE" 2>/dev/null
610
+ }
611
+
612
+ synkro_consent_clear_consumed() {
613
+ local sid="$1" hash="$2"
614
+ [ ! -f "$SYNKRO_CONSENT_FILE" ] && return
615
+ local tmp="\${SYNKRO_CONSENT_FILE}.tmp"
616
+ grep -v "^$sid\\\\t$hash\\\\tconsumed$" "$SYNKRO_CONSENT_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$SYNKRO_CONSENT_FILE" 2>/dev/null || true
617
+ }
618
+
591
619
  synkro_post_with_retry() {
592
620
  local url="$1" body="$2" timeout="\${3:-8}"
593
621
  local resp
@@ -669,11 +697,21 @@ if [ "$ROUTE" = "local" ]; then
669
697
  jq -n --arg m "$REASON" '{systemMessage: $m}'
670
698
  synkro_capture_local "bash" "warning" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
671
699
  else
672
- if [ "$IS_HEADLESS" = "1" ]; then DEC="deny"; else DEC="ask"; fi
673
- REASON="[synkro:local] bashGuard \u2192 block\${LOCAL_RULE_ID:+ ($LOCAL_RULE_ID)}: \${LOCAL_REASON:-policy violation}"
674
- jq -n --arg dec "$DEC" --arg reason "$REASON" --arg ctx "$REASON" \\
675
- '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$dec,permissionDecisionReason:$reason,additionalContext:$ctx}}'
676
- synkro_capture_local "bash" "block" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
700
+ CMD_HASH=$(printf '%s' "$COMMAND" | shasum -a 256 | cut -c1-16)
701
+ if [ -n "$SESSION_ID" ] && synkro_consent_has_active "$SESSION_ID" "$CMD_HASH"; then
702
+ REASON="[synkro:local] bashGuard \u2192 pass (consented retry): \${LOCAL_REASON:-retrying previously consented command}"
703
+ jq -n --arg m "$REASON" '{systemMessage: $m}'
704
+ synkro_capture_local "bash" "pass" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
705
+ else
706
+ if [ -n "$SESSION_ID" ] && synkro_consent_has_consumed "$SESSION_ID" "$CMD_HASH"; then
707
+ synkro_consent_clear_consumed "$SESSION_ID" "$CMD_HASH"
708
+ fi
709
+ if [ "$IS_HEADLESS" = "1" ]; then DEC="deny"; else DEC="ask"; fi
710
+ REASON="[synkro:local] bashGuard \u2192 block\${LOCAL_RULE_ID:+ ($LOCAL_RULE_ID)}: \${LOCAL_REASON:-policy violation}"
711
+ jq -n --arg dec "$DEC" --arg reason "$REASON" --arg ctx "$REASON" \\
712
+ '{systemMessage:$reason,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$dec,permissionDecisionReason:$reason,additionalContext:$ctx}}'
713
+ synkro_capture_local "bash" "block" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
714
+ fi
677
715
  fi
678
716
  else
679
717
  jq -n --arg m "[synkro:local] bashGuard \u2192 pass: \${LOCAL_REASON:-no policy violations detected}" '{systemMessage: $m}'
@@ -1191,6 +1229,19 @@ BODY=$(jq -n --arg sid "$SESSION_ID" --arg tid "$TOOL_USE_ID" \\
1191
1229
  --argjson err "$IS_ERROR" --arg ch "$CMD_HASH" \\
1192
1230
  '{capture_type:"bash_followup",session_id:$sid,tool_use_id:$tid,is_error:$err,command_hash:$ch}')
1193
1231
 
1232
+ # Local consent tracking: command ran = user consented
1233
+ # On success \u2192 consume (next attempt blocks fresh)
1234
+ # On failure \u2192 grant active (retry allowed)
1235
+ if [ -n "$CMD_HASH" ] && [ -n "$SESSION_ID" ]; then
1236
+ if [ "$IS_ERROR" = "false" ]; then
1237
+ synkro_consent_consume "$SESSION_ID" "$CMD_HASH"
1238
+ else
1239
+ if ! synkro_consent_has_active "$SESSION_ID" "$CMD_HASH"; then
1240
+ synkro_consent_grant "$SESSION_ID" "$CMD_HASH"
1241
+ fi
1242
+ fi
1243
+ fi
1244
+
1194
1245
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/hook/capture" \\
1195
1246
  -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \\
1196
1247
  -d "$BODY" --max-time 2 >/dev/null 2>&1 || true
@@ -1448,6 +1499,16 @@ if [ -n "$CMD" ]; then
1448
1499
  CMD_HASH=$(printf '%s' "$CMD" | shasum -a 256 | cut -c1-16)
1449
1500
  fi
1450
1501
 
1502
+ if [ -n "$CMD_HASH" ] && [ -n "$SESSION_ID" ]; then
1503
+ if [ "$IS_ERROR" = "false" ]; then
1504
+ synkro_consent_consume "$SESSION_ID" "$CMD_HASH"
1505
+ else
1506
+ if ! synkro_consent_has_active "$SESSION_ID" "$CMD_HASH"; then
1507
+ synkro_consent_grant "$SESSION_ID" "$CMD_HASH"
1508
+ fi
1509
+ fi
1510
+ fi
1511
+
1451
1512
  if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
1452
1513
  (
1453
1514
  BODY=$(jq -n --arg sid "$SESSION_ID" --arg tid "$TOOL_USE_ID" \\
@@ -3608,7 +3669,7 @@ function writeConfigEnv(opts) {
3608
3669
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3609
3670
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3610
3671
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3611
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.23")}`
3672
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.25")}`
3612
3673
  ];
3613
3674
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
3614
3675
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);