@synkro-sh/cli 1.3.41 → 1.3.43
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 +142 -29
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -793,6 +793,26 @@ esac
|
|
|
793
793
|
# Fire-and-forget anonymized telemetry for local_only mode
|
|
794
794
|
if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
|
|
795
795
|
(
|
|
796
|
+
MECH_CAT=""
|
|
797
|
+
BIZ_CAT=""
|
|
798
|
+
# For violations, run OWASP classification on user's machine
|
|
799
|
+
if [ "$VERDICT_KIND" = "warn" ]; then
|
|
800
|
+
CLASS_CACHE="$HOME/.synkro/.classification-prompt"
|
|
801
|
+
CLASS_PROMPT=""
|
|
802
|
+
if [ -f "$CLASS_CACHE" ] && find "$CLASS_CACHE" -mmin -1440 2>/dev/null | grep -q .; then
|
|
803
|
+
CLASS_PROMPT=$(cat "$CLASS_CACHE" 2>/dev/null)
|
|
804
|
+
else
|
|
805
|
+
CLASS_PROMPT=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/judge-prompts" \\
|
|
806
|
+
-H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null | jq -r '.classification_prompt // empty')
|
|
807
|
+
[ -n "$CLASS_PROMPT" ] && echo "$CLASS_PROMPT" > "$CLASS_CACHE"
|
|
808
|
+
fi
|
|
809
|
+
if [ -n "$CLASS_PROMPT" ]; then
|
|
810
|
+
CLASS_INPUT=$(printf '%s\\n\\nViolation context:\\n- Tool: %s\\n- Category: %s\\n- Severity: %s\\n- Hook type: bash command judge' "$CLASS_PROMPT" "$TOOL_NAME" "$CATEGORY" "$SEVERITY")
|
|
811
|
+
CLASS_RESP=$(echo "$CLASS_INPUT" | claude --print --model claude-sonnet-4-6 --no-session-persistence 2>/dev/null || echo "")
|
|
812
|
+
MECH_CAT=$(echo "$CLASS_RESP" | jq -r '.mechanism_category // empty' 2>/dev/null)
|
|
813
|
+
BIZ_CAT=$(echo "$CLASS_RESP" | jq -r '.business_category // empty' 2>/dev/null)
|
|
814
|
+
fi
|
|
815
|
+
fi
|
|
796
816
|
ANON_BODY=$(jq -n \\
|
|
797
817
|
--arg event_id "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
|
|
798
818
|
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
|
|
@@ -804,6 +824,9 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
|
|
|
804
824
|
--arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
|
|
805
825
|
--arg tool_name "$TOOL_NAME" \\
|
|
806
826
|
--arg repo "\${GIT_REPO:-}" \\
|
|
827
|
+
--arg session_id "$SESSION_ID" \\
|
|
828
|
+
--arg mech_cat "$MECH_CAT" \\
|
|
829
|
+
--arg biz_cat "$BIZ_CAT" \\
|
|
807
830
|
'{
|
|
808
831
|
event_id: $event_id,
|
|
809
832
|
timestamp: $timestamp,
|
|
@@ -814,7 +837,10 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
|
|
|
814
837
|
category: $category,
|
|
815
838
|
model: $model,
|
|
816
839
|
tool_name: $tool_name
|
|
817
|
-
} + (if $repo != "" then {repo: $repo} else {} end)
|
|
840
|
+
} + (if $repo != "" then {repo: $repo} else {} end)
|
|
841
|
+
+ (if $session_id != "" then {session_id: $session_id} else {} end)
|
|
842
|
+
+ (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
|
|
843
|
+
+ (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
|
|
818
844
|
curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
|
|
819
845
|
-H "Content-Type: application/json" \\
|
|
820
846
|
-H "Authorization: Bearer $JWT" \\
|
|
@@ -1279,6 +1305,25 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
|
|
|
1279
1305
|
LOCAL_CATEGORY="edit_violation"
|
|
1280
1306
|
fi
|
|
1281
1307
|
(
|
|
1308
|
+
MECH_CAT=""
|
|
1309
|
+
BIZ_CAT=""
|
|
1310
|
+
if [ "$LOCAL_VERDICT" = "warn" ]; then
|
|
1311
|
+
CLASS_CACHE="$HOME/.synkro/.classification-prompt"
|
|
1312
|
+
CLASS_PROMPT=""
|
|
1313
|
+
if [ -f "$CLASS_CACHE" ] && find "$CLASS_CACHE" -mmin -1440 2>/dev/null | grep -q .; then
|
|
1314
|
+
CLASS_PROMPT=$(cat "$CLASS_CACHE" 2>/dev/null)
|
|
1315
|
+
else
|
|
1316
|
+
CLASS_PROMPT=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/judge-prompts" \\
|
|
1317
|
+
-H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null | jq -r '.classification_prompt // empty')
|
|
1318
|
+
[ -n "$CLASS_PROMPT" ] && echo "$CLASS_PROMPT" > "$CLASS_CACHE"
|
|
1319
|
+
fi
|
|
1320
|
+
if [ -n "$CLASS_PROMPT" ]; then
|
|
1321
|
+
CLASS_INPUT=$(printf '%s\\n\\nViolation context:\\n- Tool: %s\\n- Category: %s\\n- Severity: %s\\n- Hook type: edit pre-check judge' "$CLASS_PROMPT" "$TOOL_NAME" "$LOCAL_CATEGORY" "$LOCAL_SEVERITY")
|
|
1322
|
+
CLASS_RESP=$(echo "$CLASS_INPUT" | claude --print --model claude-sonnet-4-6 --no-session-persistence 2>/dev/null || echo "")
|
|
1323
|
+
MECH_CAT=$(echo "$CLASS_RESP" | jq -r '.mechanism_category // empty' 2>/dev/null)
|
|
1324
|
+
BIZ_CAT=$(echo "$CLASS_RESP" | jq -r '.business_category // empty' 2>/dev/null)
|
|
1325
|
+
fi
|
|
1326
|
+
fi
|
|
1282
1327
|
ANON_BODY=$(jq -n \\
|
|
1283
1328
|
--arg event_id "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
|
|
1284
1329
|
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
|
|
@@ -1289,6 +1334,9 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
|
|
|
1289
1334
|
--arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
|
|
1290
1335
|
--arg tool_name "$TOOL_NAME" \\
|
|
1291
1336
|
--arg repo "\${GIT_REPO:-}" \\
|
|
1337
|
+
--arg session_id "$SESSION_ID" \\
|
|
1338
|
+
--arg mech_cat "$MECH_CAT" \\
|
|
1339
|
+
--arg biz_cat "$BIZ_CAT" \\
|
|
1292
1340
|
'{
|
|
1293
1341
|
event_id: $event_id,
|
|
1294
1342
|
timestamp: $timestamp,
|
|
@@ -1298,7 +1346,10 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
|
|
|
1298
1346
|
category: $category,
|
|
1299
1347
|
model: $model,
|
|
1300
1348
|
tool_name: $tool_name
|
|
1301
|
-
} + (if $repo != "" then {repo: $repo} else {} end)
|
|
1349
|
+
} + (if $repo != "" then {repo: $repo} else {} end)
|
|
1350
|
+
+ (if $session_id != "" then {session_id: $session_id} else {} end)
|
|
1351
|
+
+ (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
|
|
1352
|
+
+ (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
|
|
1302
1353
|
curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
|
|
1303
1354
|
-H "Content-Type: application/json" \\
|
|
1304
1355
|
-H "Authorization: Bearer $JWT" \\
|
|
@@ -1597,6 +1648,25 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
|
|
|
1597
1648
|
LOCAL_VERDICT="allow"; LOCAL_SEVERITY="audit"; LOCAL_RISK="low"
|
|
1598
1649
|
fi
|
|
1599
1650
|
(
|
|
1651
|
+
MECH_CAT=""
|
|
1652
|
+
BIZ_CAT=""
|
|
1653
|
+
if [ "$LOCAL_VERDICT" = "warn" ]; then
|
|
1654
|
+
CLASS_CACHE="$HOME/.synkro/.classification-prompt"
|
|
1655
|
+
CLASS_PROMPT=""
|
|
1656
|
+
if [ -f "$CLASS_CACHE" ] && find "$CLASS_CACHE" -mmin -1440 2>/dev/null | grep -q .; then
|
|
1657
|
+
CLASS_PROMPT=$(cat "$CLASS_CACHE" 2>/dev/null)
|
|
1658
|
+
else
|
|
1659
|
+
CLASS_PROMPT=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/judge-prompts" \\
|
|
1660
|
+
-H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null | jq -r '.classification_prompt // empty')
|
|
1661
|
+
[ -n "$CLASS_PROMPT" ] && echo "$CLASS_PROMPT" > "$CLASS_CACHE"
|
|
1662
|
+
fi
|
|
1663
|
+
if [ -n "$CLASS_PROMPT" ]; then
|
|
1664
|
+
CLASS_INPUT=$(printf '%s\\n\\nViolation context:\\n- Tool: %s\\n- Category: %s\\n- Severity: %s\\n- Hook type: post-edit capture grader' "$CLASS_PROMPT" "$TOOL_NAME" "$CATEGORY" "$LOCAL_SEVERITY")
|
|
1665
|
+
CLASS_RESP=$(echo "$CLASS_INPUT" | claude --print --model claude-sonnet-4-6 --no-session-persistence 2>/dev/null || echo "")
|
|
1666
|
+
MECH_CAT=$(echo "$CLASS_RESP" | jq -r '.mechanism_category // empty' 2>/dev/null)
|
|
1667
|
+
BIZ_CAT=$(echo "$CLASS_RESP" | jq -r '.business_category // empty' 2>/dev/null)
|
|
1668
|
+
fi
|
|
1669
|
+
fi
|
|
1600
1670
|
ANON_BODY=$(jq -n \\
|
|
1601
1671
|
--arg event_id "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
|
|
1602
1672
|
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
|
|
@@ -1608,11 +1678,17 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
|
|
|
1608
1678
|
--arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
|
|
1609
1679
|
--arg tool_name "$TOOL_NAME" \\
|
|
1610
1680
|
--arg repo "\${GIT_REPO:-}" \\
|
|
1681
|
+
--arg session_id "$SESSION_ID" \\
|
|
1682
|
+
--arg mech_cat "$MECH_CAT" \\
|
|
1683
|
+
--arg biz_cat "$BIZ_CAT" \\
|
|
1611
1684
|
'{
|
|
1612
1685
|
event_id: $event_id, timestamp: $timestamp, hook_type: $hook_type,
|
|
1613
1686
|
verdict: $verdict, severity: $severity, risk_level: $risk_level,
|
|
1614
1687
|
category: $category, model: $model, tool_name: $tool_name
|
|
1615
|
-
} + (if $repo != "" then {repo: $repo} else {} end)
|
|
1688
|
+
} + (if $repo != "" then {repo: $repo} else {} end)
|
|
1689
|
+
+ (if $session_id != "" then {session_id: $session_id} else {} end)
|
|
1690
|
+
+ (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
|
|
1691
|
+
+ (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
|
|
1616
1692
|
curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
|
|
1617
1693
|
-H "Content-Type: application/json" \\
|
|
1618
1694
|
-H "Authorization: Bearer $JWT" \\
|
|
@@ -1988,7 +2064,7 @@ Commands:
|
|
|
1988
2064
|
status - print "running"/"stopped"
|
|
1989
2065
|
"""
|
|
1990
2066
|
|
|
1991
|
-
import os, sys, json, socket, time, signal, fcntl, re, select
|
|
2067
|
+
import os, sys, json, socket, time, signal, fcntl, re, select, queue
|
|
1992
2068
|
import subprocess, threading, urllib.request, urllib.error
|
|
1993
2069
|
from pathlib import Path
|
|
1994
2070
|
|
|
@@ -2119,13 +2195,18 @@ def log(msg):
|
|
|
2119
2195
|
|
|
2120
2196
|
STALL_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_STALL_TIMEOUT", "10"))
|
|
2121
2197
|
|
|
2122
|
-
def _read_response(proc, timeout=45):
|
|
2198
|
+
def _read_response(proc, timeout=45, stop_event=None):
|
|
2199
|
+
"""Read stream-json from proc.stdout until a 'result' message arrives.
|
|
2200
|
+
If stop_event is set externally, returns immediately with empty string \u2014
|
|
2201
|
+
used by the parallel-race grade path to abort the loser."""
|
|
2123
2202
|
acc = []
|
|
2124
2203
|
deadline = time.time() + timeout
|
|
2125
2204
|
last_data = time.time()
|
|
2126
2205
|
fd = proc.stdout.fileno()
|
|
2127
2206
|
buf = ""
|
|
2128
2207
|
while True:
|
|
2208
|
+
if stop_event is not None and stop_event.is_set():
|
|
2209
|
+
return ""
|
|
2129
2210
|
remaining = deadline - time.time()
|
|
2130
2211
|
if remaining <= 0:
|
|
2131
2212
|
log("read timeout")
|
|
@@ -2133,7 +2214,7 @@ def _read_response(proc, timeout=45):
|
|
|
2133
2214
|
if time.time() - last_data > STALL_TIMEOUT_SEC:
|
|
2134
2215
|
log(f"stall timeout: no data for {STALL_TIMEOUT_SEC}s")
|
|
2135
2216
|
return ""
|
|
2136
|
-
ready, _, _ = select.select([fd], [], [], min(remaining,
|
|
2217
|
+
ready, _, _ = select.select([fd], [], [], min(remaining, 1.0))
|
|
2137
2218
|
if not ready:
|
|
2138
2219
|
if proc.poll() is not None:
|
|
2139
2220
|
log("process exited during read")
|
|
@@ -2262,39 +2343,71 @@ class WarmGrader:
|
|
|
2262
2343
|
self._warm_proc = None
|
|
2263
2344
|
self._warm_ready_at = 0.0
|
|
2264
2345
|
|
|
2265
|
-
# Discard warm processes that have been idle too long.
|
|
2266
2346
|
WARM_TTL_SEC = int(os.environ.get("SYNKRO_DAEMON_WARM_TTL", "15"))
|
|
2267
|
-
warm
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
proc = self._make_proc()
|
|
2271
|
-
warm = False
|
|
2272
|
-
elif proc.stdin.closed:
|
|
2273
|
-
log("warm process stdin closed, cold fallback")
|
|
2347
|
+
# Decide if the warm process is usable for the race.
|
|
2348
|
+
warm_usable = bool(proc) and proc.poll() is None and not proc.stdin.closed
|
|
2349
|
+
if warm_usable and ready_at and (time.time() - ready_at) > WARM_TTL_SEC:
|
|
2274
2350
|
self._kill_proc(proc)
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
elif ready_at and (time.time() - ready_at) > WARM_TTL_SEC:
|
|
2278
|
-
age = time.time() - ready_at
|
|
2279
|
-
log(f"warm process stale ({age:.0f}s > {WARM_TTL_SEC}s), cold fallback")
|
|
2351
|
+
warm_usable = False
|
|
2352
|
+
if not warm_usable and proc:
|
|
2280
2353
|
self._kill_proc(proc)
|
|
2281
|
-
proc =
|
|
2282
|
-
warm = False
|
|
2354
|
+
proc = None
|
|
2283
2355
|
|
|
2284
2356
|
wall_limit = int(os.environ.get("SYNKRO_DAEMON_WALL_TIMEOUT", "12"))
|
|
2357
|
+
race_timeout = min(GRADE_TIMEOUT_SEC, wall_limit)
|
|
2358
|
+
|
|
2359
|
+
# Race a warm and a fresh cold process. Whichever returns a non-empty
|
|
2360
|
+
# response first wins; the other is killed. If we have no warm, just
|
|
2361
|
+
# run cold solo (no benefit to racing two cold spawns of the same age).
|
|
2362
|
+
cold_proc = self._make_proc() if warm_usable else proc or self._make_proc()
|
|
2363
|
+
warm_proc = proc if warm_usable else None
|
|
2364
|
+
|
|
2365
|
+
result_q = queue.Queue()
|
|
2366
|
+
stop_event = threading.Event()
|
|
2367
|
+
|
|
2368
|
+
def grade_worker(p, label):
|
|
2369
|
+
try:
|
|
2370
|
+
_send_msg(p, prompt)
|
|
2371
|
+
r = _read_response(p, timeout=race_timeout, stop_event=stop_event)
|
|
2372
|
+
if r:
|
|
2373
|
+
result_q.put((label, r))
|
|
2374
|
+
except Exception as e:
|
|
2375
|
+
log(f"grade {label} error: {e}")
|
|
2376
|
+
|
|
2377
|
+
threads = []
|
|
2378
|
+
if warm_proc is not None:
|
|
2379
|
+
t = threading.Thread(target=grade_worker, args=(warm_proc, "warm"), daemon=True)
|
|
2380
|
+
t.start()
|
|
2381
|
+
threads.append(t)
|
|
2382
|
+
t = threading.Thread(target=grade_worker, args=(cold_proc, "cold"), daemon=True)
|
|
2383
|
+
t.start()
|
|
2384
|
+
threads.append(t)
|
|
2385
|
+
|
|
2285
2386
|
t0 = time.time()
|
|
2387
|
+
winner_label = None
|
|
2388
|
+
resp = ""
|
|
2286
2389
|
try:
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2390
|
+
deadline = time.time() + race_timeout + 2
|
|
2391
|
+
while time.time() < deadline:
|
|
2392
|
+
try:
|
|
2393
|
+
label, r = result_q.get(timeout=0.5)
|
|
2394
|
+
if r:
|
|
2395
|
+
resp = r
|
|
2396
|
+
winner_label = label
|
|
2397
|
+
break
|
|
2398
|
+
except queue.Empty:
|
|
2399
|
+
if all(not th.is_alive() for th in threads):
|
|
2400
|
+
break
|
|
2292
2401
|
finally:
|
|
2293
|
-
|
|
2402
|
+
stop_event.set()
|
|
2403
|
+
if warm_proc is not None:
|
|
2404
|
+
self._kill_proc(warm_proc)
|
|
2405
|
+
self._kill_proc(cold_proc)
|
|
2294
2406
|
|
|
2295
2407
|
elapsed = (time.time() - t0) * 1000
|
|
2296
2408
|
self._total_grades += 1
|
|
2297
|
-
|
|
2409
|
+
winner = winner_label or "none"
|
|
2410
|
+
log(f"grade #{self._total_grades} race={'warm+cold' if warm_proc else 'cold'} won={winner} elapsed={elapsed:.0f}ms resp={len(resp)}ch")
|
|
2298
2411
|
|
|
2299
2412
|
self._start_prewarm()
|
|
2300
2413
|
return resp
|
|
@@ -3746,7 +3859,7 @@ function writeConfigEnv(opts) {
|
|
|
3746
3859
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3747
3860
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3748
3861
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
3749
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3862
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.43")}`
|
|
3750
3863
|
];
|
|
3751
3864
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
3752
3865
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|