@synkro-sh/cli 1.3.39 → 1.3.41
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 +91 -106
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -388,12 +388,7 @@ fi
|
|
|
388
388
|
CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
|
|
389
389
|
synkro_log "bashGuard checking: $CMD_SHORT"
|
|
390
390
|
|
|
391
|
-
#
|
|
392
|
-
# (including HTTP-payload destructive shapes like graphql_destructive_mutation
|
|
393
|
-
# and http_method_delete) and returns a "trivial" fast-path verdict for boring
|
|
394
|
-
# read-only commands (ls, pwd, git status, --version, etc.). Anything ambiguous
|
|
395
|
-
# goes to Cerebras for grading. This closes the gap that verb-only filters miss
|
|
396
|
-
# (e.g. a curl POST whose JSON body contains a destructive mutation).
|
|
391
|
+
# All commands are graded; no client-side regex gate.
|
|
397
392
|
|
|
398
393
|
# Extract context from the transcript file
|
|
399
394
|
TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
@@ -455,7 +450,7 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
|
455
450
|
] | .[-10:]' 2>/dev/null || echo "[]")
|
|
456
451
|
# Recent agent actions (last 5 tool_use blocks paired with results)
|
|
457
452
|
RECENT_ACTIONS=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
458
|
-
# tool_result blocks live in USER messages
|
|
453
|
+
# tool_result blocks live in USER messages
|
|
459
454
|
([ .[]
|
|
460
455
|
| select(.type == "user")
|
|
461
456
|
| .message.content[]?
|
|
@@ -571,6 +566,24 @@ refresh_jwt() {
|
|
|
571
566
|
return 0
|
|
572
567
|
}
|
|
573
568
|
|
|
569
|
+
ensure_fresh_jwt() {
|
|
570
|
+
[ -z "$JWT" ] && return 1
|
|
571
|
+
local payload exp now remaining
|
|
572
|
+
payload=$(printf '%s' "$JWT" | cut -d. -f2)
|
|
573
|
+
case $((\${#payload} % 4)) in
|
|
574
|
+
2) payload="\${payload}==" ;;
|
|
575
|
+
3) payload="\${payload}=" ;;
|
|
576
|
+
esac
|
|
577
|
+
exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
|
|
578
|
+
now=$(date -u +%s)
|
|
579
|
+
remaining=$((exp - now))
|
|
580
|
+
if [ "$remaining" -lt 60 ]; then
|
|
581
|
+
refresh_jwt
|
|
582
|
+
fi
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
ensure_fresh_jwt
|
|
586
|
+
|
|
574
587
|
# Resolve tier + capture_depth (cached 60 min) \u2014 server is canonical via /cli/me.
|
|
575
588
|
TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
|
|
576
589
|
CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
|
|
@@ -656,7 +669,7 @@ if [ "$USE_LOCAL" = "true" ]; then
|
|
|
656
669
|
ALTERNATIVE=$(xtag "$V_INNER" alternative)
|
|
657
670
|
|
|
658
671
|
# Fail-open on no verdict \u2014 daemon handles cold fallback internally; if it
|
|
659
|
-
# still came back empty (
|
|
672
|
+
# still came back empty (grader unavailable), don't block the user.
|
|
660
673
|
if [ -z "$VERDICT_KIND" ] || [ -z "$SEVERITY" ]; then
|
|
661
674
|
synkro_log "bashGuard $CMD_SHORT \u2192 pass (grader unavailable)"
|
|
662
675
|
jq -n --arg m "[synkro] bashGuard \u2192 pass (grader unavailable)" '{systemMessage: $m}'
|
|
@@ -666,7 +679,7 @@ if [ "$USE_LOCAL" = "true" ]; then
|
|
|
666
679
|
# already populated above. Server-side path (else branch) keeps using JSON.
|
|
667
680
|
VERDICT="__LOCAL_XML_PARSED__"
|
|
668
681
|
else
|
|
669
|
-
# \u2500\u2500\u2500
|
|
682
|
+
# \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
|
|
670
683
|
|
|
671
684
|
VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
|
|
672
685
|
-H "Content-Type: application/json" \\
|
|
@@ -814,18 +827,7 @@ exit 0
|
|
|
814
827
|
`;
|
|
815
828
|
CC_EDIT_PRECHECK_SCRIPT = `#!/bin/bash
|
|
816
829
|
# Synkro PreToolUse Edit/Write/MultiEdit/NotebookEdit pre-check hook.
|
|
817
|
-
#
|
|
818
|
-
# Steering-at-violation: thin shim that POSTs the proposed file content to
|
|
819
|
-
# /api/v1/precheck-edit. The server cosines it against the org's active
|
|
820
|
-
# agent_runtime rules in Timescale and returns the CC hook JSON shape
|
|
821
|
-
# directly (permissionDecision: "deny" + retry guidance, or empty {} to allow).
|
|
822
|
-
#
|
|
823
|
-
# No LLM in this path \u2014 just embedding + cosine. Customer's CC pays for the
|
|
824
|
-
# retry generation when the agent reads the denial reason and rewrites.
|
|
825
|
-
# Synkro's COGS = ~$0.00001 per edit (one Gemini embedding call).
|
|
826
|
-
#
|
|
827
830
|
# Always exits 0 with valid JSON. Fails open on any error.
|
|
828
|
-
# No set -e: hook must ALWAYS produce JSON output. Silent death = CC timeout.
|
|
829
831
|
|
|
830
832
|
synkro_log() { echo "[synkro] $1" >&2; }
|
|
831
833
|
|
|
@@ -1036,6 +1038,24 @@ refresh_jwt() {
|
|
|
1036
1038
|
return 0
|
|
1037
1039
|
}
|
|
1038
1040
|
|
|
1041
|
+
ensure_fresh_jwt() {
|
|
1042
|
+
[ -z "$JWT" ] && return 1
|
|
1043
|
+
local payload exp now remaining
|
|
1044
|
+
payload=$(printf '%s' "$JWT" | cut -d. -f2)
|
|
1045
|
+
case $((\${#payload} % 4)) in
|
|
1046
|
+
2) payload="\${payload}==" ;;
|
|
1047
|
+
3) payload="\${payload}=" ;;
|
|
1048
|
+
esac
|
|
1049
|
+
exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
|
|
1050
|
+
now=$(date -u +%s)
|
|
1051
|
+
remaining=$((exp - now))
|
|
1052
|
+
if [ "$remaining" -lt 60 ]; then
|
|
1053
|
+
refresh_jwt
|
|
1054
|
+
fi
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
ensure_fresh_jwt
|
|
1058
|
+
|
|
1039
1059
|
|
|
1040
1060
|
# Resolve tier + capture_depth (cached 60 min) \u2014 server is canonical via /cli/me.
|
|
1041
1061
|
TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
|
|
@@ -1201,7 +1221,7 @@ if [ "$USE_LOCAL" = "true" ]; then
|
|
|
1201
1221
|
fi
|
|
1202
1222
|
fi
|
|
1203
1223
|
else
|
|
1204
|
-
# \u2500\u2500\u2500
|
|
1224
|
+
# \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
|
|
1205
1225
|
RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit" \\
|
|
1206
1226
|
-H "Content-Type: application/json" \\
|
|
1207
1227
|
-H "Authorization: Bearer $JWT" \\
|
|
@@ -1291,19 +1311,8 @@ exit 0
|
|
|
1291
1311
|
`;
|
|
1292
1312
|
CC_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
|
|
1293
1313
|
# Synkro PostToolUse Edit/Write afterFileEdit hook.
|
|
1294
|
-
#
|
|
1295
|
-
# Reads the file from disk after the edit lands, sends to /api/v1/judge-edit
|
|
1296
|
-
# for Cerebras grading against the org's rules. On ok=false, emits a system
|
|
1297
|
-
# message that surfaces inline in CC ("[synkro] afterFileEdit \u2192 Finding: \u2026").
|
|
1298
|
-
# The agent reads the finding via transcript and re-edits on its own
|
|
1299
|
-
# inference \u2014 same retry pattern as PreToolUse precheck, looser/asynchronous.
|
|
1300
|
-
#
|
|
1301
|
-
# Complementary to the PreToolUse precheck: precheck blocks unsafe writes
|
|
1302
|
-
# before disk; this hook catches issues that only become visible AFTER the
|
|
1303
|
-
# edit lands (cross-file shapes, real disk state, post-edit semantic effects).
|
|
1304
|
-
#
|
|
1314
|
+
# On ok=false, emits a system message that surfaces inline in CC.
|
|
1305
1315
|
# Always exits 0 with valid JSON \u2014 never breaks CC's flow.
|
|
1306
|
-
# No set -e: hook must ALWAYS produce JSON output. Silent death = CC timeout.
|
|
1307
1316
|
|
|
1308
1317
|
synkro_log() { echo "[synkro] $1" >&2; }
|
|
1309
1318
|
|
|
@@ -1422,6 +1431,24 @@ refresh_jwt() {
|
|
|
1422
1431
|
return 0
|
|
1423
1432
|
}
|
|
1424
1433
|
|
|
1434
|
+
ensure_fresh_jwt() {
|
|
1435
|
+
[ -z "$JWT" ] && return 1
|
|
1436
|
+
local payload exp now remaining
|
|
1437
|
+
payload=$(printf '%s' "$JWT" | cut -d. -f2)
|
|
1438
|
+
case $((\${#payload} % 4)) in
|
|
1439
|
+
2) payload="\${payload}==" ;;
|
|
1440
|
+
3) payload="\${payload}=" ;;
|
|
1441
|
+
esac
|
|
1442
|
+
exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
|
|
1443
|
+
now=$(date -u +%s)
|
|
1444
|
+
remaining=$((exp - now))
|
|
1445
|
+
if [ "$remaining" -lt 60 ]; then
|
|
1446
|
+
refresh_jwt
|
|
1447
|
+
fi
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
ensure_fresh_jwt
|
|
1451
|
+
|
|
1425
1452
|
# Fire-and-forget correction-followup (backgrounded \u2014 must not block grading).
|
|
1426
1453
|
if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
|
|
1427
1454
|
(
|
|
@@ -1533,7 +1560,7 @@ if [ "$USE_LOCAL" = "true" ]; then
|
|
|
1533
1560
|
RESP=""
|
|
1534
1561
|
fi
|
|
1535
1562
|
else
|
|
1536
|
-
# \u2500\u2500\u2500
|
|
1563
|
+
# \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
|
|
1537
1564
|
RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge-edit" \\
|
|
1538
1565
|
-H "Content-Type: application/json" \\
|
|
1539
1566
|
-H "Authorization: Bearer $JWT" \\
|
|
@@ -2235,11 +2262,7 @@ class WarmGrader:
|
|
|
2235
2262
|
self._warm_proc = None
|
|
2236
2263
|
self._warm_ready_at = 0.0
|
|
2237
2264
|
|
|
2238
|
-
#
|
|
2239
|
-
# that goes stale after ~20s idle. The subprocess doesn't notice the
|
|
2240
|
-
# broken pipe and just hangs forever when given a prompt. Force cold
|
|
2241
|
-
# if the warm has been sitting idle too long \u2014 still faster than
|
|
2242
|
-
# eating a 10s stall timeout + cold fallback.
|
|
2265
|
+
# Discard warm processes that have been idle too long.
|
|
2243
2266
|
WARM_TTL_SEC = int(os.environ.get("SYNKRO_DAEMON_WARM_TTL", "15"))
|
|
2244
2267
|
warm = True
|
|
2245
2268
|
if not proc or proc.poll() is not None:
|
|
@@ -3723,7 +3746,7 @@ function writeConfigEnv(opts) {
|
|
|
3723
3746
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3724
3747
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3725
3748
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
3726
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3749
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.41")}`
|
|
3727
3750
|
];
|
|
3728
3751
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
3729
3752
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -4770,54 +4793,9 @@ async function ensureOpenPr(repo, prNumber, sha) {
|
|
|
4770
4793
|
try {
|
|
4771
4794
|
pr = ghJson(["api", `/repos/${repo}/pulls/${prNumber}`]);
|
|
4772
4795
|
} catch {
|
|
4773
|
-
return { prNumber, sha, branched: false };
|
|
4774
|
-
}
|
|
4775
|
-
if (pr.state === "open") {
|
|
4776
|
-
return { prNumber, sha, branched: false };
|
|
4777
|
-
}
|
|
4778
|
-
if (pr.merged) {
|
|
4779
|
-
console.log(`PR #${prNumber} is merged. Scanning original diff and posting findings there.
|
|
4780
|
-
`);
|
|
4781
|
-
return { prNumber, sha: pr.head.sha, branched: false };
|
|
4782
|
-
}
|
|
4783
|
-
console.log(`PR #${prNumber} is closed. Creating a scan branch...
|
|
4784
|
-
`);
|
|
4785
|
-
const scanBranch = `synkro/scan-${pr.head.ref}-${Date.now()}`;
|
|
4786
|
-
try {
|
|
4787
|
-
execSync5(`gh api -X POST /repos/${repo}/git/refs --input -`, {
|
|
4788
|
-
encoding: "utf-8",
|
|
4789
|
-
input: JSON.stringify({ ref: `refs/heads/${scanBranch}`, sha: pr.head.sha }),
|
|
4790
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4791
|
-
});
|
|
4792
|
-
} catch (err) {
|
|
4793
|
-
console.warn(`Failed to create scan branch: ${err.message}`);
|
|
4794
|
-
return { prNumber, sha, branched: false };
|
|
4795
|
-
}
|
|
4796
|
-
try {
|
|
4797
|
-
const newPrBody = `Security scan of closed PR #${prNumber} (\`${pr.head.ref}\`).
|
|
4798
|
-
|
|
4799
|
-
This PR contains no code changes \u2014 it exists so Synkro can post review findings on an active PR.`;
|
|
4800
|
-
const result = ghJson([
|
|
4801
|
-
"api",
|
|
4802
|
-
"-X",
|
|
4803
|
-
"POST",
|
|
4804
|
-
`/repos/${repo}/pulls`,
|
|
4805
|
-
"-f",
|
|
4806
|
-
`title=Synkro Scan: ${pr.title}`,
|
|
4807
|
-
"-f",
|
|
4808
|
-
`body=${newPrBody}`,
|
|
4809
|
-
"-f",
|
|
4810
|
-
`head=${scanBranch}`,
|
|
4811
|
-
"-f",
|
|
4812
|
-
`base=${pr.base.ref}`
|
|
4813
|
-
]);
|
|
4814
|
-
console.log(`Opened PR #${result.number} for scan findings.
|
|
4815
|
-
`);
|
|
4816
|
-
return { prNumber: result.number, sha: result.head.sha, branched: true };
|
|
4817
|
-
} catch (err) {
|
|
4818
|
-
console.warn(`Failed to open scan PR: ${err.message}`);
|
|
4819
|
-
return { prNumber, sha, branched: false };
|
|
4796
|
+
return { prNumber, sha, branched: false, prState: "unknown", merged: false };
|
|
4820
4797
|
}
|
|
4798
|
+
return { prNumber, sha: pr.head.sha, branched: false, prState: pr.state, merged: pr.merged };
|
|
4821
4799
|
}
|
|
4822
4800
|
function getPrFiles(repo, prNumber) {
|
|
4823
4801
|
const data = ghJson([
|
|
@@ -5094,7 +5072,28 @@ ${linesStr}: ${first.description}
|
|
|
5094
5072
|
severity: maxSeverity
|
|
5095
5073
|
};
|
|
5096
5074
|
}
|
|
5097
|
-
function postPrReview(repo, prNumber, sha, review) {
|
|
5075
|
+
function postPrReview(repo, prNumber, sha, review, skipLineReview = false) {
|
|
5076
|
+
function postIssueComment() {
|
|
5077
|
+
try {
|
|
5078
|
+
const body = `## \u{1F512} Synkro Security Review
|
|
5079
|
+
|
|
5080
|
+
${review.summary}
|
|
5081
|
+
|
|
5082
|
+
` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
|
|
5083
|
+
execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
|
|
5084
|
+
encoding: "utf-8",
|
|
5085
|
+
input: JSON.stringify({ body }),
|
|
5086
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
5087
|
+
});
|
|
5088
|
+
console.log(" \u2713 Posted issue comment with findings.");
|
|
5089
|
+
} catch (err) {
|
|
5090
|
+
console.warn("Failed to post issue comment:", err.message);
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
if (skipLineReview) {
|
|
5094
|
+
postIssueComment();
|
|
5095
|
+
return;
|
|
5096
|
+
}
|
|
5098
5097
|
const preferredEvent = review.severity === "critical" || review.severity === "high" ? "REQUEST_CHANGES" : "COMMENT";
|
|
5099
5098
|
function tryPost(event) {
|
|
5100
5099
|
const body = JSON.stringify({
|
|
@@ -5120,27 +5119,12 @@ ${review.summary}`,
|
|
|
5120
5119
|
if (combined.includes("own pull request") && event === "REQUEST_CHANGES") {
|
|
5121
5120
|
return false;
|
|
5122
5121
|
}
|
|
5123
|
-
console.warn(`Failed to post review: ${(stderr || stdout || err.message).slice(0, 200)}`);
|
|
5124
5122
|
return false;
|
|
5125
5123
|
}
|
|
5126
5124
|
}
|
|
5127
5125
|
if (tryPost(preferredEvent)) return;
|
|
5128
5126
|
if (preferredEvent === "REQUEST_CHANGES" && tryPost("COMMENT")) return;
|
|
5129
|
-
|
|
5130
|
-
const fallbackBody = `## \u{1F512} Synkro Security Review
|
|
5131
|
-
|
|
5132
|
-
${review.summary}
|
|
5133
|
-
|
|
5134
|
-
` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
|
|
5135
|
-
execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
|
|
5136
|
-
encoding: "utf-8",
|
|
5137
|
-
input: JSON.stringify({ body: fallbackBody }),
|
|
5138
|
-
stdio: ["pipe", "ignore", "pipe"]
|
|
5139
|
-
});
|
|
5140
|
-
console.log(" \u2713 Posted fallback issue comment.");
|
|
5141
|
-
} catch (err2) {
|
|
5142
|
-
console.warn("Failed to post fallback comment:", err2.message);
|
|
5143
|
-
}
|
|
5127
|
+
postIssueComment();
|
|
5144
5128
|
}
|
|
5145
5129
|
function postCheckRun(repo, sha, conclusion, findings) {
|
|
5146
5130
|
const summary = findings.length === 0 ? "No security findings." : `${findings.length} finding(s):
|
|
@@ -5295,7 +5279,8 @@ Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${t
|
|
|
5295
5279
|
const review = await spawnOpusConsolidator(allFindings, claudeToken);
|
|
5296
5280
|
console.log(` \u2192 ${review.comments.length} review comment(s), severity: ${review.severity}`);
|
|
5297
5281
|
if (review.comments.length > 0) {
|
|
5298
|
-
|
|
5282
|
+
const skipLineReview = prTarget.prState === "closed" && !prTarget.merged;
|
|
5283
|
+
postPrReview(repo, activePrNumber, activeSha, review, skipLineReview);
|
|
5299
5284
|
}
|
|
5300
5285
|
}
|
|
5301
5286
|
const conclusion = shouldFail(allFindings, failThreshold) ? "failure" : "success";
|
|
@@ -5474,7 +5459,7 @@ Usage:
|
|
|
5474
5459
|
|
|
5475
5460
|
Commands:
|
|
5476
5461
|
install [--force] Install Synkro hooks for detected agents (Claude Code, etc.)
|
|
5477
|
-
login Authenticate with Synkro (browser
|
|
5462
|
+
login Authenticate with Synkro (browser sign-in)
|
|
5478
5463
|
logout Clear local credentials
|
|
5479
5464
|
status Show current setup state
|
|
5480
5465
|
link Link repos to a Synkro project (local git or GitHub OAuth)
|