@synkro-sh/cli 1.3.33 → 1.3.34
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 +181 -166
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -600,21 +600,9 @@ elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1;
|
|
|
600
600
|
fi
|
|
601
601
|
|
|
602
602
|
if [ "$USE_LOCAL" = "true" ]; then
|
|
603
|
-
# \u2500\u2500\u2500
|
|
604
|
-
|
|
605
|
-
#
|
|
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
|
|
603
|
+
# \u2500\u2500\u2500 LOCAL: grade via the persistent claude daemon (mode=bash). \u2500\u2500\u2500
|
|
604
|
+
# The daemon fetches the primer (Synkro IP) directly from the server at
|
|
605
|
+
# startup and holds it in memory \u2014 never written to disk.
|
|
618
606
|
|
|
619
607
|
# Fetch org guardrail rules. In local_only mode use GET (no command leaks);
|
|
620
608
|
# in other modes use POST with content for embedding-based top_k matching.
|
|
@@ -648,20 +636,35 @@ if [ "$USE_LOCAL" = "true" ]; then
|
|
|
648
636
|
printf 'Recent actions: %s\\n' "$RECENT_ACTIONS" >> "$GRADER_PROMPT_FILE"
|
|
649
637
|
printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
|
|
650
638
|
|
|
651
|
-
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] &&
|
|
652
|
-
|
|
653
|
-
CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode bash grade "$HOME/.synkro/grader-primer-bash.txt" < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
639
|
+
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && command -v python3 >/dev/null 2>&1; then
|
|
640
|
+
CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode bash grade < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
654
641
|
else
|
|
655
|
-
|
|
656
642
|
CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
657
643
|
fi
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
644
|
+
# Wrapper extraction \u2014 greedy so it tolerates nested XML tags inside.
|
|
645
|
+
V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
|
|
646
|
+
|
|
647
|
+
# Local primer emits XML tags. Parse into the same bash vars the
|
|
648
|
+
# downstream code expects \u2014 bypassing jq entirely (XML is faster on local
|
|
649
|
+
# claude --print than JSON-mode generation).
|
|
650
|
+
xtag() { printf '%s' "$1" | sed -nE "s|.*<$2>(.*)</$2>.*|\\1|p" | head -1; }
|
|
651
|
+
VERDICT_KIND=$(xtag "$V_INNER" verdict)
|
|
652
|
+
SEVERITY=$(xtag "$V_INNER" severity)
|
|
653
|
+
RISK_LEVEL=$(xtag "$V_INNER" risk_level)
|
|
654
|
+
CATEGORY=$(xtag "$V_INNER" category)
|
|
655
|
+
REASONING=$(xtag "$V_INNER" reasoning)
|
|
656
|
+
ALTERNATIVE=$(xtag "$V_INNER" alternative)
|
|
657
|
+
|
|
658
|
+
# Fail-open on no verdict \u2014 daemon handles cold fallback internally; if it
|
|
659
|
+
# still came back empty (network/Anthropic outage), don't block the user.
|
|
660
|
+
if [ -z "$VERDICT_KIND" ] || [ -z "$SEVERITY" ]; then
|
|
661
|
+
synkro_log "bashGuard $CMD_SHORT \u2192 pass (grader unavailable)"
|
|
662
|
+
jq -n --arg m "[synkro] bashGuard \u2192 pass (grader unavailable)" '{systemMessage: $m}'
|
|
662
663
|
exit 0
|
|
663
664
|
fi
|
|
664
|
-
|
|
665
|
+
# Sentinel \u2014 tells the downstream parser to skip jq and use the bash vars
|
|
666
|
+
# already populated above. Server-side path (else branch) keeps using JSON.
|
|
667
|
+
VERDICT="__LOCAL_XML_PARSED__"
|
|
665
668
|
else
|
|
666
669
|
# \u2500\u2500\u2500 FAST TIER: server-side Cerebras grading. \u2500\u2500\u2500
|
|
667
670
|
|
|
@@ -689,13 +692,22 @@ if [ -z "$VERDICT" ]; then
|
|
|
689
692
|
exit 0
|
|
690
693
|
fi
|
|
691
694
|
|
|
692
|
-
# Parse verdict \u2014 fail open on any parse error
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
695
|
+
# Parse verdict \u2014 fail open on any parse error.
|
|
696
|
+
# Local XML path already populated the vars above; only parse JSON for the
|
|
697
|
+
# server (BYOK) path.
|
|
698
|
+
if [ "$VERDICT" != "__LOCAL_XML_PARSED__" ]; then
|
|
699
|
+
SEVERITY=$(echo "$VERDICT" | jq -r '.severity // "audit"' 2>/dev/null)
|
|
700
|
+
VERDICT_KIND=$(echo "$VERDICT" | jq -r '.verdict // "warn"' 2>/dev/null)
|
|
701
|
+
REASONING=$(echo "$VERDICT" | jq -r '.reasoning // "matched dangerous-verb regex"' 2>/dev/null)
|
|
702
|
+
ALTERNATIVE=$(echo "$VERDICT" | jq -r '.alternative // ""' 2>/dev/null)
|
|
703
|
+
CATEGORY=$(echo "$VERDICT" | jq -r '.category // "destructive_command"' 2>/dev/null)
|
|
704
|
+
RISK_LEVEL=$(echo "$VERDICT" | jq -r '.risk_level // empty' 2>/dev/null)
|
|
705
|
+
fi
|
|
706
|
+
# Defaults if any var is empty (defensive \u2014 XML primer should always emit them).
|
|
707
|
+
SEVERITY="\${SEVERITY:-audit}"
|
|
708
|
+
VERDICT_KIND="\${VERDICT_KIND:-warn}"
|
|
709
|
+
REASONING="\${REASONING:-matched dangerous-verb regex}"
|
|
710
|
+
CATEGORY="\${CATEGORY:-destructive_command}"
|
|
699
711
|
|
|
700
712
|
# Backwards-compat: if severity isn't block/audit, derive it from verdict_kind
|
|
701
713
|
# and treat the original severity as the risk_level.
|
|
@@ -1053,20 +1065,9 @@ elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1;
|
|
|
1053
1065
|
fi
|
|
1054
1066
|
|
|
1055
1067
|
if [ "$USE_LOCAL" = "true" ]; then
|
|
1056
|
-
# \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (
|
|
1057
|
-
|
|
1058
|
-
#
|
|
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
|
|
1068
|
+
# \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (mode=edit).
|
|
1069
|
+
# The daemon fetches the primer (Synkro IP) directly from the server at
|
|
1070
|
+
# startup and holds it in memory \u2014 never written to disk.
|
|
1070
1071
|
RULES_CACHE="$HOME/.synkro/.rules-cache-edit"
|
|
1071
1072
|
ORG_RULES="[]"
|
|
1072
1073
|
if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
|
|
@@ -1097,34 +1098,50 @@ if [ "$USE_LOCAL" = "true" ]; then
|
|
|
1097
1098
|
printf 'Diff:\\n' >> "$GRADER_PROMPT_FILE"
|
|
1098
1099
|
printf '%s\\n' "$PROPOSED" >> "$GRADER_PROMPT_FILE"
|
|
1099
1100
|
|
|
1100
|
-
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] &&
|
|
1101
|
-
CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode edit grade
|
|
1101
|
+
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && command -v python3 >/dev/null 2>&1; then
|
|
1102
|
+
CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode edit grade < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
1102
1103
|
else
|
|
1103
1104
|
CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
1104
1105
|
fi
|
|
1105
1106
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
#
|
|
1110
|
-
#
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1107
|
+
# Wrapper extraction (greedy \u2014 tolerates nested XML tags).
|
|
1108
|
+
V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
|
|
1109
|
+
|
|
1110
|
+
# Parse XML tags from local primer. Defaults to ok=true so the server's
|
|
1111
|
+
# literal_match audit path still runs even if the grader returned nothing.
|
|
1112
|
+
LOCAL_OK="true"
|
|
1113
|
+
LOCAL_VIOLATION_REASON=""
|
|
1114
|
+
LOCAL_VIOLATION_RULE_ID=""
|
|
1115
|
+
if [ -n "$V_INNER" ]; then
|
|
1116
|
+
OK_TAG=$(printf '%s' "$V_INNER" | sed -nE 's|.*<ok>(.*)</ok>.*|\\1|p' | head -1)
|
|
1117
|
+
[ -n "$OK_TAG" ] && LOCAL_OK="$OK_TAG"
|
|
1118
|
+
if [ "$LOCAL_OK" = "false" ]; then
|
|
1119
|
+
# Extract first <violation>...</violation> block via awk (RS=closing tag),
|
|
1120
|
+
# then xtag for fields. Handles < in content + multi-violation correctly.
|
|
1121
|
+
FIRST_V=$(printf '%s' "$V_INNER" | awk -v RS='</violation>' '/<violation>/{print; exit}')
|
|
1122
|
+
LOCAL_VIOLATION_RULE_ID=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
|
|
1123
|
+
LOCAL_VIOLATION_REASON=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
|
|
1124
|
+
fi
|
|
1119
1125
|
fi
|
|
1126
|
+
# Build a JSON shape that downstream / server code already understands. This
|
|
1127
|
+
# keeps the existing /precheck-edit/local-verdict POST format unchanged so
|
|
1128
|
+
# the server-side literal_match path still works in non-local_only mode.
|
|
1129
|
+
VERDICT_JSON=$(jq -n \\
|
|
1130
|
+
--arg ok "$LOCAL_OK" \\
|
|
1131
|
+
--arg reason "$LOCAL_VIOLATION_REASON" \\
|
|
1132
|
+
--arg rule_id "$LOCAL_VIOLATION_RULE_ID" \\
|
|
1133
|
+
'if $ok == "false" then
|
|
1134
|
+
{ok: false, violations: [{rule_id: $rule_id, reason: $reason}]}
|
|
1135
|
+
else
|
|
1136
|
+
{ok: true, violations: []}
|
|
1137
|
+
end')
|
|
1120
1138
|
|
|
1121
1139
|
if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
|
|
1122
1140
|
# No file content / diff / intent / actions leave the device. Synthesize
|
|
1123
1141
|
# the hook response from the local verdict only.
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
RULE_ID=$(echo "$VERDICT_JSON" | jq -r '.violations[0].rule_id // "local_violation"' 2>/dev/null)
|
|
1142
|
+
if [ "$LOCAL_OK" = "false" ]; then
|
|
1143
|
+
FIRST_REASON="\${LOCAL_VIOLATION_REASON:-policy violation}"
|
|
1144
|
+
RULE_ID="\${LOCAL_VIOLATION_RULE_ID:-local_violation}"
|
|
1128
1145
|
if [ "$IS_HEADLESS" = "1" ]; then DEC="deny"; else DEC="ask"; fi
|
|
1129
1146
|
RESP=$(jq -n \\
|
|
1130
1147
|
--arg dec "$DEC" \\
|
|
@@ -1479,14 +1496,32 @@ if [ "$USE_LOCAL" = "true" ]; then
|
|
|
1479
1496
|
printf 'Content:\\n' >> "$GRADER_PROMPT_FILE"
|
|
1480
1497
|
printf '%s\\n' "$FILE_CONTENT" >> "$GRADER_PROMPT_FILE"
|
|
1481
1498
|
|
|
1482
|
-
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] &&
|
|
1483
|
-
CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode edit grade
|
|
1499
|
+
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && command -v python3 >/dev/null 2>&1; then
|
|
1500
|
+
CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode edit grade < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
1484
1501
|
else
|
|
1485
1502
|
CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
1486
1503
|
fi
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1504
|
+
# Wrapper extraction (greedy \u2014 tolerates nested XML tags).
|
|
1505
|
+
V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
|
|
1506
|
+
if [ -n "$V_INNER" ]; then
|
|
1507
|
+
LOCAL_OK=$(printf '%s' "$V_INNER" | sed -nE 's|.*<ok>(.*)</ok>.*|\\1|p' | head -1)
|
|
1508
|
+
LOCAL_OK="\${LOCAL_OK:-true}"
|
|
1509
|
+
# Extract first <violation>...</violation> block, then fields from it.
|
|
1510
|
+
FIRST_V=$(printf '%s' "$V_INNER" | awk -v RS='</violation>' '/<violation>/{print; exit}')
|
|
1511
|
+
LOCAL_SEV=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
|
|
1512
|
+
LOCAL_CAT=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
|
|
1513
|
+
LOCAL_REASON=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
|
|
1514
|
+
# Convert to JSON shape downstream code expects.
|
|
1515
|
+
RESP=$(jq -n \\
|
|
1516
|
+
--arg ok "$LOCAL_OK" \\
|
|
1517
|
+
--arg sev "\${LOCAL_SEV:-low}" \\
|
|
1518
|
+
--arg cat "\${LOCAL_CAT:-unspecified}" \\
|
|
1519
|
+
--arg reason "$LOCAL_REASON" \\
|
|
1520
|
+
'if $ok == "false" then
|
|
1521
|
+
{ok: false, severity: $sev, category: $cat, reason: $reason}
|
|
1522
|
+
else
|
|
1523
|
+
{ok: true, severity: "low", category: "clean", reason: ""}
|
|
1524
|
+
end')
|
|
1490
1525
|
else
|
|
1491
1526
|
RESP=""
|
|
1492
1527
|
fi
|
|
@@ -1896,7 +1931,7 @@ exit 0
|
|
|
1896
1931
|
});
|
|
1897
1932
|
|
|
1898
1933
|
// cli/installer/graderDaemon.ts
|
|
1899
|
-
var GRADER_DAEMON_PY
|
|
1934
|
+
var GRADER_DAEMON_PY;
|
|
1900
1935
|
var init_graderDaemon = __esm({
|
|
1901
1936
|
"cli/installer/graderDaemon.ts"() {
|
|
1902
1937
|
"use strict";
|
|
@@ -1919,9 +1954,56 @@ Commands:
|
|
|
1919
1954
|
"""
|
|
1920
1955
|
|
|
1921
1956
|
import os, sys, json, socket, time, signal, fcntl, re, select
|
|
1922
|
-
import subprocess, threading
|
|
1957
|
+
import subprocess, threading, urllib.request, urllib.error
|
|
1923
1958
|
from pathlib import Path
|
|
1924
1959
|
|
|
1960
|
+
# \u2500\u2500 Primer fetch \u2014 server-side IP, never written to disk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1961
|
+
SYNKRO_HOME = Path.home() / ".synkro"
|
|
1962
|
+
CREDS_PATH = Path(os.environ.get("SYNKRO_CREDENTIALS_PATH") or str(SYNKRO_HOME / "credentials.json"))
|
|
1963
|
+
GATEWAY_URL = os.environ.get("SYNKRO_GATEWAY_URL", "https://api.synkro.sh").rstrip("/")
|
|
1964
|
+
CONFIG_ENV = SYNKRO_HOME / "config.env"
|
|
1965
|
+
|
|
1966
|
+
def _load_gateway_url():
|
|
1967
|
+
"""Read SYNKRO_GATEWAY_URL from config.env if not in env."""
|
|
1968
|
+
global GATEWAY_URL
|
|
1969
|
+
if "SYNKRO_GATEWAY_URL" in os.environ:
|
|
1970
|
+
return
|
|
1971
|
+
try:
|
|
1972
|
+
for line in CONFIG_ENV.read_text().splitlines():
|
|
1973
|
+
line = line.strip()
|
|
1974
|
+
if line.startswith("SYNKRO_GATEWAY_URL="):
|
|
1975
|
+
v = line.split("=", 1)[1].strip().strip("'").strip('"')
|
|
1976
|
+
if v.startswith(("http://", "https://")):
|
|
1977
|
+
GATEWAY_URL = v.rstrip("/")
|
|
1978
|
+
return
|
|
1979
|
+
except Exception:
|
|
1980
|
+
pass
|
|
1981
|
+
|
|
1982
|
+
def _read_jwt():
|
|
1983
|
+
try:
|
|
1984
|
+
with open(CREDS_PATH) as f:
|
|
1985
|
+
return json.load(f).get("access_token", "") or ""
|
|
1986
|
+
except Exception:
|
|
1987
|
+
return ""
|
|
1988
|
+
|
|
1989
|
+
def fetch_primer(mode):
|
|
1990
|
+
"""Fetch primer text for {bash,edit} from /cli/judge-prompts. In-memory only."""
|
|
1991
|
+
_load_gateway_url()
|
|
1992
|
+
jwt = _read_jwt()
|
|
1993
|
+
if not jwt:
|
|
1994
|
+
return ""
|
|
1995
|
+
field = "grader_primer_bash" if mode == "bash" else "grader_primer_edit"
|
|
1996
|
+
try:
|
|
1997
|
+
req = urllib.request.Request(
|
|
1998
|
+
f"{GATEWAY_URL}/api/v1/cli/judge-prompts",
|
|
1999
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
2000
|
+
)
|
|
2001
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
2002
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
2003
|
+
return data.get(field, "") or ""
|
|
2004
|
+
except Exception:
|
|
2005
|
+
return ""
|
|
2006
|
+
|
|
1925
2007
|
ALLOWED_MODE_RE = re.compile(r"^[a-z][a-z0-9_-]{0,30}$")
|
|
1926
2008
|
DAEMON_BASE = Path.home() / ".synkro" / "daemon"
|
|
1927
2009
|
DAEMON_BASE.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
@@ -1993,7 +2075,12 @@ def _read_response(proc, timeout=45):
|
|
|
1993
2075
|
return "".join(acc)
|
|
1994
2076
|
|
|
1995
2077
|
|
|
1996
|
-
def _send_msg(proc, text):
|
|
2078
|
+
def _send_msg(proc, text, close_stdin=False):
|
|
2079
|
+
"""Send a stream-json user message. If close_stdin=True, close stdin after
|
|
2080
|
+
flushing \u2014 this forces claude --print to stop waiting for more input and
|
|
2081
|
+
process what it has. Required for the final/grade message; the prewarm
|
|
2082
|
+
primer ack must keep stdin open so the warm process stays alive for the
|
|
2083
|
+
upcoming grade call."""
|
|
1997
2084
|
msg = json.dumps({
|
|
1998
2085
|
"type": "user",
|
|
1999
2086
|
"message": {"role": "user", "content": [{"type": "text", "text": text}]},
|
|
@@ -2002,6 +2089,9 @@ def _send_msg(proc, text):
|
|
|
2002
2089
|
})
|
|
2003
2090
|
proc.stdin.write(msg + "\\n")
|
|
2004
2091
|
proc.stdin.flush()
|
|
2092
|
+
if close_stdin:
|
|
2093
|
+
try: proc.stdin.close()
|
|
2094
|
+
except Exception: pass
|
|
2005
2095
|
|
|
2006
2096
|
|
|
2007
2097
|
class WarmGrader:
|
|
@@ -2203,10 +2293,18 @@ def daemon_running():
|
|
|
2203
2293
|
return False
|
|
2204
2294
|
|
|
2205
2295
|
|
|
2206
|
-
def ensure_daemon_running(primer_path):
|
|
2296
|
+
def ensure_daemon_running(primer_path=None):
|
|
2207
2297
|
if daemon_running():
|
|
2208
2298
|
return True
|
|
2209
|
-
primer
|
|
2299
|
+
# Fetch primer from server (in-memory only \u2014 never written to disk).
|
|
2300
|
+
# Backwards compat: if a primer_path arg was passed (legacy hook scripts),
|
|
2301
|
+
# read from there as a fallback when API fetch fails.
|
|
2302
|
+
primer = fetch_primer(MODE)
|
|
2303
|
+
if not primer and primer_path:
|
|
2304
|
+
try:
|
|
2305
|
+
primer = Path(primer_path).read_text()
|
|
2306
|
+
except Exception:
|
|
2307
|
+
primer = ""
|
|
2210
2308
|
pid = os.fork()
|
|
2211
2309
|
if pid == 0:
|
|
2212
2310
|
os.setsid()
|
|
@@ -2295,76 +2393,6 @@ def main():
|
|
|
2295
2393
|
|
|
2296
2394
|
if __name__ == "__main__":
|
|
2297
2395
|
main()
|
|
2298
|
-
`;
|
|
2299
|
-
GRADER_PRIMER_EDIT = `You are Synkro's security pre-check grader. You will be given proposed file diffs from an AI coding agent. For each one, decide whether it has security issues \u2014 possibly multiple distinct ones in the same diff.
|
|
2300
|
-
|
|
2301
|
-
EACH GRADING REQUEST INCLUDES:
|
|
2302
|
-
- File: the path being written
|
|
2303
|
-
- User intent: what the user told the agent to do
|
|
2304
|
-
- Org rules: a JSON array of this organization's active policies, each with rule_id, text, severity, category. These are the org's primary source of truth \u2014 apply them.
|
|
2305
|
-
- Diff: the proposed file content
|
|
2306
|
-
|
|
2307
|
-
JUDGING PRIORITY:
|
|
2308
|
-
1. ORG RULES first. If the diff matches the prose of any rule in the Org rules array, flag it \u2014 emit the rule's rule_id verbatim and the rule's severity. A rule that bans an action class covers ALL forms of that action: splitting arguments across function calls, wrapping in helpers, renaming a variable, using a different invocation pattern \u2014 none of those bypass the rule. Match on semantic intent of the rule's prose, not literal substring.
|
|
2309
|
-
2. BASELINE security issues \u2014 hardcoded real-looking secrets, eval/exec on user input, SQL string concat with untrusted input, MD5/SHA1 for security-sensitive purposes, unsafe deserialization, command injection, path traversal, missing auth on routes that mutate user/billing data, weak random for tokens, broken JWT verification, CORS misconfig, env-dump logging. Flag these even if no org rule covers them \u2014 they're universally bad. Use a sensible snake_case rule_id like \`no-hardcoded-secrets\`, \`eval-on-user-input\`, \`sql-string-concat\`.
|
|
2310
|
-
3. Stylistic issues, placeholder fixtures, test files (path under /tests/, /__tests__/, *.test.*), and config-only files are NOT security issues \u2014 return ok=true.
|
|
2311
|
-
|
|
2312
|
-
INDEPENDENCE: Each grade request is INDEPENDENT. Judge ONLY the current request's File / User intent / Org rules / Diff. Prior "allows" do NOT authorize the current request \u2014 re-evaluate fresh against the rules in THIS prompt.
|
|
2313
|
-
|
|
2314
|
-
OUTPUT RULES \u2014 strictest possible, no exceptions:
|
|
2315
|
-
|
|
2316
|
-
1. NO reasoning. NO preamble. NO commentary.
|
|
2317
|
-
2. Your reply is exactly one <synkro-verdict>JSON</synkro-verdict> block. Nothing else.
|
|
2318
|
-
3. JSON shape:
|
|
2319
|
-
{"ok": true | false,
|
|
2320
|
-
"violations": [
|
|
2321
|
-
{"rule_id": "<rule_id from Org rules if matched, else short snake_case slug>",
|
|
2322
|
-
"severity": "low" | "medium" | "high" | "critical",
|
|
2323
|
-
"category": "<short snake_case>",
|
|
2324
|
-
"reason": "<= 25 words, file:line + issue + fix",
|
|
2325
|
-
"confidence": 0.0-1.0}
|
|
2326
|
-
]}
|
|
2327
|
-
|
|
2328
|
-
ok=true \u2192 "violations": [] (empty array). Safe diffs (placeholder, fixture, sandboxed, clean) are ok=true.
|
|
2329
|
-
|
|
2330
|
-
ok=false \u2192 list EVERY distinct violation. A diff that hardcodes a Stripe key AND uses eval AND has SQL injection produces THREE entries \u2014 one per independently-fixable issue. Don't collapse multiple issues into one "composite" finding; the agent retries based on this list and missing entries become extra round trips.
|
|
2331
|
-
|
|
2332
|
-
ONE VIOLATION = ONE ENTRY. If the same line/issue can be described multiple ways ("uses concat", "missing $1 placeholder", "not parameterized"), pick the most specific rule_id and write a single entry. Multiple entries are warranted only when the diff has multiple INDEPENDENTLY-FIXABLE issues.
|
|
2333
|
-
|
|
2334
|
-
ORG RULE PRECEDENCE: when an org rule matches, use ITS rule_id and severity verbatim \u2014 never override "critical" down to "medium" because the diff seems mild, and never invent your own rule_id when an org rule applies. The user authored those rules for a reason.
|
|
2335
|
-
|
|
2336
|
-
Reply with exactly: <synkro-verdict>{"ok":true,"violations":[]}</synkro-verdict>
|
|
2337
|
-
`;
|
|
2338
|
-
GRADER_PRIMER_BASH = `You are Synkro's bash command safety judge for AI coding agents. You will be given a proposed shell command, the user's most recent stated intent, the last 3-5 user-role messages from the chat (oldest first, JSON array under "Recent user messages"), and recent agent actions. Decide whether to allow or warn.
|
|
2339
|
-
|
|
2340
|
-
OUTPUT RULES \u2014 strictest possible, no exceptions:
|
|
2341
|
-
|
|
2342
|
-
1. NO reasoning. NO preamble. NO commentary.
|
|
2343
|
-
2. Your reply is exactly one <synkro-verdict>JSON</synkro-verdict> block. Nothing else.
|
|
2344
|
-
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"}
|
|
2345
|
-
|
|
2346
|
-
SEVERITY MAPPING (strict):
|
|
2347
|
-
- verdict="warn" \u2192 severity="block"
|
|
2348
|
-
- verdict="allow" \u2192 severity="audit"
|
|
2349
|
-
risk_level always reflects the underlying danger level (low/medium/high/critical), independent of the routing decision.
|
|
2350
|
-
|
|
2351
|
-
Rules:
|
|
2352
|
-
- WARN if destructive/irreversible AND not aligned with user intent, OR has wildly disproportionate blast radius vs the request.
|
|
2353
|
-
- ALLOW if consistent with user intent and either reversible or low-blast-radius.
|
|
2354
|
-
- The same command can be allow OR warn depending on context. Match against the user's verbatim intent.
|
|
2355
|
-
- Be conservative: when uncertain, warn medium.
|
|
2356
|
-
- Token-scope check: if recent_actions shows a Read of a credentials file (e.g. ".env.deploy", "domain-token.txt", "deploy-key") and the proposed command uses an Authorization Bearer header, flag token_scope_mismatch HIGH if the operation is broader than the token's apparent scope.
|
|
2357
|
-
|
|
2358
|
-
CONSENT CARRYOVER \u2014 IMPORTANT:
|
|
2359
|
-
Treat "Recent user messages" as the consent context, not just the last entry. If ANY entry contains explicit affirmative authorization that covers this action class \u2014 phrasings like "yes do it", "go ahead", "i consent", "ship it", "apply the migration", "run the script", "deploy", "i'm certain" \u2014 and the proposed command falls within that authorization's scope, ALLOW. Re-blocking the same action class after consent already granted is a UX failure, not a safety win. Cite the verbatim consent quote in the reasoning.
|
|
2360
|
-
|
|
2361
|
-
Consent does NOT carry forward when:
|
|
2362
|
-
1. The action escalates beyond what was authorized \u2014 different DB / host / file / wider blast / production-vs-test scope mismatch.
|
|
2363
|
-
2. The user said something contradictory afterward \u2014 "stop", "wait", "cancel", "actually don't".
|
|
2364
|
-
3. The action involves credentials / secrets / supply-chain risk that prior consent didn't cover.
|
|
2365
|
-
4. The most recent message is a fresh task unrelated to the authorized action class.
|
|
2366
|
-
|
|
2367
|
-
Reply with exactly: <synkro-verdict>{"verdict":"allow","severity":"low","category":"primer_ack","reasoning":"primer received","alternative":null}</synkro-verdict>
|
|
2368
2396
|
`;
|
|
2369
2397
|
}
|
|
2370
2398
|
});
|
|
@@ -3554,28 +3582,15 @@ function ensureSynkroDir() {
|
|
|
3554
3582
|
mkdirSync5(BIN_DIR, { recursive: true });
|
|
3555
3583
|
mkdirSync5(OFFSETS_DIR, { recursive: true });
|
|
3556
3584
|
}
|
|
3557
|
-
|
|
3558
|
-
try {
|
|
3559
|
-
const resp = await fetch(`${gatewayUrl}/api/v1/cli/judge-prompts`, {
|
|
3560
|
-
headers: { "Authorization": `Bearer ${token}` }
|
|
3561
|
-
});
|
|
3562
|
-
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
|
3563
|
-
const data = await resp.json();
|
|
3564
|
-
if (!data.grader_primer_bash || !data.grader_primer_edit) throw new Error("missing primer fields");
|
|
3565
|
-
return { bash: data.grader_primer_bash, edit: data.grader_primer_edit };
|
|
3566
|
-
} catch (err) {
|
|
3567
|
-
console.warn(`[synkro] primer fetch failed (${err.message}); using bundled fallback`);
|
|
3568
|
-
return { bash: GRADER_PRIMER_BASH, edit: GRADER_PRIMER_EDIT };
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
async function writeGraderDaemon(gatewayUrl, token) {
|
|
3585
|
+
function writeGraderDaemon() {
|
|
3572
3586
|
writeFileSync5(GRADER_DAEMON_PATH, GRADER_DAEMON_PY, "utf-8");
|
|
3573
3587
|
chmodSync(GRADER_DAEMON_PATH, 493);
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3588
|
+
for (const p of [GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH]) {
|
|
3589
|
+
try {
|
|
3590
|
+
__require("fs").unlinkSync(p);
|
|
3591
|
+
} catch {
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3579
3594
|
}
|
|
3580
3595
|
function writeHookScripts() {
|
|
3581
3596
|
const bashScriptPath = join6(HOOKS_DIR, "cc-bash-judge.sh");
|
|
@@ -3632,7 +3647,7 @@ function writeConfigEnv(opts) {
|
|
|
3632
3647
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3633
3648
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3634
3649
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
3635
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3650
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.34")}`
|
|
3636
3651
|
];
|
|
3637
3652
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
3638
3653
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -3860,7 +3875,7 @@ async function installCommand(opts = {}) {
|
|
|
3860
3875
|
console.log(` ${scripts.sessionStartScript}`);
|
|
3861
3876
|
console.log(` ${scripts.transcriptSyncScript}
|
|
3862
3877
|
`);
|
|
3863
|
-
|
|
3878
|
+
writeGraderDaemon();
|
|
3864
3879
|
for (const mode of ["edit", "bash"]) {
|
|
3865
3880
|
const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
|
|
3866
3881
|
try {
|