@synkro-sh/cli 1.3.40 → 1.3.42
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 +90 -91
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1988,7 +1988,7 @@ Commands:
|
|
|
1988
1988
|
status - print "running"/"stopped"
|
|
1989
1989
|
"""
|
|
1990
1990
|
|
|
1991
|
-
import os, sys, json, socket, time, signal, fcntl, re, select
|
|
1991
|
+
import os, sys, json, socket, time, signal, fcntl, re, select, queue
|
|
1992
1992
|
import subprocess, threading, urllib.request, urllib.error
|
|
1993
1993
|
from pathlib import Path
|
|
1994
1994
|
|
|
@@ -2119,13 +2119,18 @@ def log(msg):
|
|
|
2119
2119
|
|
|
2120
2120
|
STALL_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_STALL_TIMEOUT", "10"))
|
|
2121
2121
|
|
|
2122
|
-
def _read_response(proc, timeout=45):
|
|
2122
|
+
def _read_response(proc, timeout=45, stop_event=None):
|
|
2123
|
+
"""Read stream-json from proc.stdout until a 'result' message arrives.
|
|
2124
|
+
If stop_event is set externally, returns immediately with empty string \u2014
|
|
2125
|
+
used by the parallel-race grade path to abort the loser."""
|
|
2123
2126
|
acc = []
|
|
2124
2127
|
deadline = time.time() + timeout
|
|
2125
2128
|
last_data = time.time()
|
|
2126
2129
|
fd = proc.stdout.fileno()
|
|
2127
2130
|
buf = ""
|
|
2128
2131
|
while True:
|
|
2132
|
+
if stop_event is not None and stop_event.is_set():
|
|
2133
|
+
return ""
|
|
2129
2134
|
remaining = deadline - time.time()
|
|
2130
2135
|
if remaining <= 0:
|
|
2131
2136
|
log("read timeout")
|
|
@@ -2133,7 +2138,7 @@ def _read_response(proc, timeout=45):
|
|
|
2133
2138
|
if time.time() - last_data > STALL_TIMEOUT_SEC:
|
|
2134
2139
|
log(f"stall timeout: no data for {STALL_TIMEOUT_SEC}s")
|
|
2135
2140
|
return ""
|
|
2136
|
-
ready, _, _ = select.select([fd], [], [], min(remaining,
|
|
2141
|
+
ready, _, _ = select.select([fd], [], [], min(remaining, 1.0))
|
|
2137
2142
|
if not ready:
|
|
2138
2143
|
if proc.poll() is not None:
|
|
2139
2144
|
log("process exited during read")
|
|
@@ -2262,39 +2267,71 @@ class WarmGrader:
|
|
|
2262
2267
|
self._warm_proc = None
|
|
2263
2268
|
self._warm_ready_at = 0.0
|
|
2264
2269
|
|
|
2265
|
-
# Discard warm processes that have been idle too long.
|
|
2266
2270
|
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")
|
|
2271
|
+
# Decide if the warm process is usable for the race.
|
|
2272
|
+
warm_usable = bool(proc) and proc.poll() is None and not proc.stdin.closed
|
|
2273
|
+
if warm_usable and ready_at and (time.time() - ready_at) > WARM_TTL_SEC:
|
|
2274
2274
|
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")
|
|
2275
|
+
warm_usable = False
|
|
2276
|
+
if not warm_usable and proc:
|
|
2280
2277
|
self._kill_proc(proc)
|
|
2281
|
-
proc =
|
|
2282
|
-
warm = False
|
|
2278
|
+
proc = None
|
|
2283
2279
|
|
|
2284
2280
|
wall_limit = int(os.environ.get("SYNKRO_DAEMON_WALL_TIMEOUT", "12"))
|
|
2281
|
+
race_timeout = min(GRADE_TIMEOUT_SEC, wall_limit)
|
|
2282
|
+
|
|
2283
|
+
# Race a warm and a fresh cold process. Whichever returns a non-empty
|
|
2284
|
+
# response first wins; the other is killed. If we have no warm, just
|
|
2285
|
+
# run cold solo (no benefit to racing two cold spawns of the same age).
|
|
2286
|
+
cold_proc = self._make_proc() if warm_usable else proc or self._make_proc()
|
|
2287
|
+
warm_proc = proc if warm_usable else None
|
|
2288
|
+
|
|
2289
|
+
result_q = queue.Queue()
|
|
2290
|
+
stop_event = threading.Event()
|
|
2291
|
+
|
|
2292
|
+
def grade_worker(p, label):
|
|
2293
|
+
try:
|
|
2294
|
+
_send_msg(p, prompt)
|
|
2295
|
+
r = _read_response(p, timeout=race_timeout, stop_event=stop_event)
|
|
2296
|
+
if r:
|
|
2297
|
+
result_q.put((label, r))
|
|
2298
|
+
except Exception as e:
|
|
2299
|
+
log(f"grade {label} error: {e}")
|
|
2300
|
+
|
|
2301
|
+
threads = []
|
|
2302
|
+
if warm_proc is not None:
|
|
2303
|
+
t = threading.Thread(target=grade_worker, args=(warm_proc, "warm"), daemon=True)
|
|
2304
|
+
t.start()
|
|
2305
|
+
threads.append(t)
|
|
2306
|
+
t = threading.Thread(target=grade_worker, args=(cold_proc, "cold"), daemon=True)
|
|
2307
|
+
t.start()
|
|
2308
|
+
threads.append(t)
|
|
2309
|
+
|
|
2285
2310
|
t0 = time.time()
|
|
2311
|
+
winner_label = None
|
|
2312
|
+
resp = ""
|
|
2286
2313
|
try:
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2314
|
+
deadline = time.time() + race_timeout + 2
|
|
2315
|
+
while time.time() < deadline:
|
|
2316
|
+
try:
|
|
2317
|
+
label, r = result_q.get(timeout=0.5)
|
|
2318
|
+
if r:
|
|
2319
|
+
resp = r
|
|
2320
|
+
winner_label = label
|
|
2321
|
+
break
|
|
2322
|
+
except queue.Empty:
|
|
2323
|
+
if all(not th.is_alive() for th in threads):
|
|
2324
|
+
break
|
|
2292
2325
|
finally:
|
|
2293
|
-
|
|
2326
|
+
stop_event.set()
|
|
2327
|
+
if warm_proc is not None:
|
|
2328
|
+
self._kill_proc(warm_proc)
|
|
2329
|
+
self._kill_proc(cold_proc)
|
|
2294
2330
|
|
|
2295
2331
|
elapsed = (time.time() - t0) * 1000
|
|
2296
2332
|
self._total_grades += 1
|
|
2297
|
-
|
|
2333
|
+
winner = winner_label or "none"
|
|
2334
|
+
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
2335
|
|
|
2299
2336
|
self._start_prewarm()
|
|
2300
2337
|
return resp
|
|
@@ -3746,7 +3783,7 @@ function writeConfigEnv(opts) {
|
|
|
3746
3783
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3747
3784
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3748
3785
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
3749
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3786
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.42")}`
|
|
3750
3787
|
];
|
|
3751
3788
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
3752
3789
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -4793,54 +4830,9 @@ async function ensureOpenPr(repo, prNumber, sha) {
|
|
|
4793
4830
|
try {
|
|
4794
4831
|
pr = ghJson(["api", `/repos/${repo}/pulls/${prNumber}`]);
|
|
4795
4832
|
} catch {
|
|
4796
|
-
return { prNumber, sha, branched: false };
|
|
4797
|
-
}
|
|
4798
|
-
if (pr.state === "open") {
|
|
4799
|
-
return { prNumber, sha, branched: false };
|
|
4800
|
-
}
|
|
4801
|
-
if (pr.merged) {
|
|
4802
|
-
console.log(`PR #${prNumber} is merged. Scanning original diff and posting findings there.
|
|
4803
|
-
`);
|
|
4804
|
-
return { prNumber, sha: pr.head.sha, branched: false };
|
|
4805
|
-
}
|
|
4806
|
-
console.log(`PR #${prNumber} is closed. Creating a scan branch...
|
|
4807
|
-
`);
|
|
4808
|
-
const scanBranch = `synkro/scan-${pr.head.ref}-${Date.now()}`;
|
|
4809
|
-
try {
|
|
4810
|
-
execSync5(`gh api -X POST /repos/${repo}/git/refs --input -`, {
|
|
4811
|
-
encoding: "utf-8",
|
|
4812
|
-
input: JSON.stringify({ ref: `refs/heads/${scanBranch}`, sha: pr.head.sha }),
|
|
4813
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4814
|
-
});
|
|
4815
|
-
} catch (err) {
|
|
4816
|
-
console.warn(`Failed to create scan branch: ${err.message}`);
|
|
4817
|
-
return { prNumber, sha, branched: false };
|
|
4818
|
-
}
|
|
4819
|
-
try {
|
|
4820
|
-
const newPrBody = `Security scan of closed PR #${prNumber} (\`${pr.head.ref}\`).
|
|
4821
|
-
|
|
4822
|
-
This PR contains no code changes \u2014 it exists so Synkro can post review findings on an active PR.`;
|
|
4823
|
-
const result = ghJson([
|
|
4824
|
-
"api",
|
|
4825
|
-
"-X",
|
|
4826
|
-
"POST",
|
|
4827
|
-
`/repos/${repo}/pulls`,
|
|
4828
|
-
"-f",
|
|
4829
|
-
`title=Synkro Scan: ${pr.title}`,
|
|
4830
|
-
"-f",
|
|
4831
|
-
`body=${newPrBody}`,
|
|
4832
|
-
"-f",
|
|
4833
|
-
`head=${scanBranch}`,
|
|
4834
|
-
"-f",
|
|
4835
|
-
`base=${pr.base.ref}`
|
|
4836
|
-
]);
|
|
4837
|
-
console.log(`Opened PR #${result.number} for scan findings.
|
|
4838
|
-
`);
|
|
4839
|
-
return { prNumber: result.number, sha: result.head.sha, branched: true };
|
|
4840
|
-
} catch (err) {
|
|
4841
|
-
console.warn(`Failed to open scan PR: ${err.message}`);
|
|
4842
|
-
return { prNumber, sha, branched: false };
|
|
4833
|
+
return { prNumber, sha, branched: false, prState: "unknown", merged: false };
|
|
4843
4834
|
}
|
|
4835
|
+
return { prNumber, sha: pr.head.sha, branched: false, prState: pr.state, merged: pr.merged };
|
|
4844
4836
|
}
|
|
4845
4837
|
function getPrFiles(repo, prNumber) {
|
|
4846
4838
|
const data = ghJson([
|
|
@@ -5117,7 +5109,28 @@ ${linesStr}: ${first.description}
|
|
|
5117
5109
|
severity: maxSeverity
|
|
5118
5110
|
};
|
|
5119
5111
|
}
|
|
5120
|
-
function postPrReview(repo, prNumber, sha, review) {
|
|
5112
|
+
function postPrReview(repo, prNumber, sha, review, skipLineReview = false) {
|
|
5113
|
+
function postIssueComment() {
|
|
5114
|
+
try {
|
|
5115
|
+
const body = `## \u{1F512} Synkro Security Review
|
|
5116
|
+
|
|
5117
|
+
${review.summary}
|
|
5118
|
+
|
|
5119
|
+
` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
|
|
5120
|
+
execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
|
|
5121
|
+
encoding: "utf-8",
|
|
5122
|
+
input: JSON.stringify({ body }),
|
|
5123
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
5124
|
+
});
|
|
5125
|
+
console.log(" \u2713 Posted issue comment with findings.");
|
|
5126
|
+
} catch (err) {
|
|
5127
|
+
console.warn("Failed to post issue comment:", err.message);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
if (skipLineReview) {
|
|
5131
|
+
postIssueComment();
|
|
5132
|
+
return;
|
|
5133
|
+
}
|
|
5121
5134
|
const preferredEvent = review.severity === "critical" || review.severity === "high" ? "REQUEST_CHANGES" : "COMMENT";
|
|
5122
5135
|
function tryPost(event) {
|
|
5123
5136
|
const body = JSON.stringify({
|
|
@@ -5143,27 +5156,12 @@ ${review.summary}`,
|
|
|
5143
5156
|
if (combined.includes("own pull request") && event === "REQUEST_CHANGES") {
|
|
5144
5157
|
return false;
|
|
5145
5158
|
}
|
|
5146
|
-
console.warn(`Failed to post review: ${(stderr || stdout || err.message).slice(0, 200)}`);
|
|
5147
5159
|
return false;
|
|
5148
5160
|
}
|
|
5149
5161
|
}
|
|
5150
5162
|
if (tryPost(preferredEvent)) return;
|
|
5151
5163
|
if (preferredEvent === "REQUEST_CHANGES" && tryPost("COMMENT")) return;
|
|
5152
|
-
|
|
5153
|
-
const fallbackBody = `## \u{1F512} Synkro Security Review
|
|
5154
|
-
|
|
5155
|
-
${review.summary}
|
|
5156
|
-
|
|
5157
|
-
` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
|
|
5158
|
-
execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
|
|
5159
|
-
encoding: "utf-8",
|
|
5160
|
-
input: JSON.stringify({ body: fallbackBody }),
|
|
5161
|
-
stdio: ["pipe", "ignore", "pipe"]
|
|
5162
|
-
});
|
|
5163
|
-
console.log(" \u2713 Posted fallback issue comment.");
|
|
5164
|
-
} catch (err2) {
|
|
5165
|
-
console.warn("Failed to post fallback comment:", err2.message);
|
|
5166
|
-
}
|
|
5164
|
+
postIssueComment();
|
|
5167
5165
|
}
|
|
5168
5166
|
function postCheckRun(repo, sha, conclusion, findings) {
|
|
5169
5167
|
const summary = findings.length === 0 ? "No security findings." : `${findings.length} finding(s):
|
|
@@ -5318,7 +5316,8 @@ Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${t
|
|
|
5318
5316
|
const review = await spawnOpusConsolidator(allFindings, claudeToken);
|
|
5319
5317
|
console.log(` \u2192 ${review.comments.length} review comment(s), severity: ${review.severity}`);
|
|
5320
5318
|
if (review.comments.length > 0) {
|
|
5321
|
-
|
|
5319
|
+
const skipLineReview = prTarget.prState === "closed" && !prTarget.merged;
|
|
5320
|
+
postPrReview(repo, activePrNumber, activeSha, review, skipLineReview);
|
|
5322
5321
|
}
|
|
5323
5322
|
}
|
|
5324
5323
|
const conclusion = shouldFail(allFindings, failThreshold) ? "failure" : "success";
|