@synkro-sh/cli 1.4.22 → 1.4.24

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,21 +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
516
- printf 'SYNKRO_CAPTURE_DEPTH="%s"\\nSYNKRO_TIER="%s"\\n' "$SYNKRO_CAPTURE_DEPTH" "$SYNKRO_TIER" > "$cache" 2>/dev/null || true
517
510
  }
518
511
 
519
512
  # Decide routing: "local" (grade on device) or "cloud" (POST to server)
@@ -549,13 +542,19 @@ synkro_parse_local_verdict() {
549
542
  LOCAL_REASON=$(printf '%s' "$inner" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
550
543
  [ -z "$LOCAL_REASON" ] && LOCAL_REASON=$(printf '%s' "$inner" | sed -nE 's|.*<reasoning>(.*)</reasoning>.*|\\1|p' | head -1)
551
544
  if [ "$LOCAL_OK" = "false" ]; then
552
- local fv
553
- fv=$(printf '%s' "$inner" | awk -v RS='</violation>' '/<violation>/{print; exit}')
554
- LOCAL_RULE_ID=$(printf '%s' "$fv" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
555
- [ -z "$LOCAL_REASON" ] && LOCAL_REASON=$(printf '%s' "$fv" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
556
- LOCAL_SEV=$(printf '%s' "$fv" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
557
- 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
558
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)
559
558
  fi
560
559
  }
561
560
 
@@ -586,6 +585,37 @@ synkro_rule_mode() {
586
585
  echo "$m"
587
586
  }
588
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
+
589
619
  synkro_post_with_retry() {
590
620
  local url="$1" body="$2" timeout="\${3:-8}"
591
621
  local resp
@@ -667,11 +697,21 @@ if [ "$ROUTE" = "local" ]; then
667
697
  jq -n --arg m "$REASON" '{systemMessage: $m}'
668
698
  synkro_capture_local "bash" "warning" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
669
699
  else
670
- if [ "$IS_HEADLESS" = "1" ]; then DEC="deny"; else DEC="ask"; fi
671
- REASON="[synkro:local] bashGuard \u2192 block\${LOCAL_RULE_ID:+ ($LOCAL_RULE_ID)}: \${LOCAL_REASON:-policy violation}"
672
- jq -n --arg dec "$DEC" --arg reason "$REASON" --arg ctx "$REASON" \\
673
- '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$dec,permissionDecisionReason:$reason,additionalContext:$ctx}}'
674
- 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
+ '{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
675
715
  fi
676
716
  else
677
717
  jq -n --arg m "[synkro:local] bashGuard \u2192 pass: \${LOCAL_REASON:-no policy violations detected}" '{systemMessage: $m}'
@@ -1189,6 +1229,19 @@ BODY=$(jq -n --arg sid "$SESSION_ID" --arg tid "$TOOL_USE_ID" \\
1189
1229
  --argjson err "$IS_ERROR" --arg ch "$CMD_HASH" \\
1190
1230
  '{capture_type:"bash_followup",session_id:$sid,tool_use_id:$tid,is_error:$err,command_hash:$ch}')
1191
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
+
1192
1245
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/hook/capture" \\
1193
1246
  -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \\
1194
1247
  -d "$BODY" --max-time 2 >/dev/null 2>&1 || true
@@ -1446,6 +1499,16 @@ if [ -n "$CMD" ]; then
1446
1499
  CMD_HASH=$(printf '%s' "$CMD" | shasum -a 256 | cut -c1-16)
1447
1500
  fi
1448
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
+
1449
1512
  if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
1450
1513
  (
1451
1514
  BODY=$(jq -n --arg sid "$SESSION_ID" --arg tid "$TOOL_USE_ID" \\
@@ -3606,7 +3669,7 @@ function writeConfigEnv(opts) {
3606
3669
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3607
3670
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3608
3671
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3609
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.22")}`
3672
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.24")}`
3610
3673
  ];
3611
3674
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
3612
3675
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);