@synkro-sh/cli 1.6.56 → 1.6.57
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 +220 -38
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -440,6 +440,7 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
440
440
|
timeout: 10,
|
|
441
441
|
matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command|delete_file"
|
|
442
442
|
});
|
|
443
|
+
pushCcHook(h, "beforeMCPExecution", config.mcpGateScriptPath, { timeout: 30 });
|
|
443
444
|
h.afterAgentThought = h.afterAgentThought ?? [];
|
|
444
445
|
h.afterAgentThought.push({
|
|
445
446
|
command: bunRunCmd(config.agentCaptureScriptPath),
|
|
@@ -497,6 +498,7 @@ var init_cursorHookConfig = __esm({
|
|
|
497
498
|
"preToolUse",
|
|
498
499
|
"afterFileEdit",
|
|
499
500
|
"postToolUse",
|
|
501
|
+
"beforeMCPExecution",
|
|
500
502
|
"afterAgentThought",
|
|
501
503
|
"afterAgentResponse"
|
|
502
504
|
];
|
|
@@ -7514,7 +7516,7 @@ async function runClaudeDesktopTap() {
|
|
|
7514
7516
|
await new Promise((resolve4) => {
|
|
7515
7517
|
const child = spawn("bash", [runnerPath], {
|
|
7516
7518
|
stdio: "inherit",
|
|
7517
|
-
env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH }
|
|
7519
|
+
env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_SCAN_TURN_URL: SCAN_TURN_URL, SYNKRO_DLP_POLICY_URL: DLP_POLICY_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH }
|
|
7518
7520
|
});
|
|
7519
7521
|
const forward = (sig) => {
|
|
7520
7522
|
try {
|
|
@@ -7531,12 +7533,14 @@ async function runClaudeDesktopTap() {
|
|
|
7531
7533
|
});
|
|
7532
7534
|
});
|
|
7533
7535
|
}
|
|
7534
|
-
var CAPTURE_URL, SCAN_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
|
|
7536
|
+
var CAPTURE_URL, SCAN_URL, SCAN_TURN_URL, DLP_POLICY_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
|
|
7535
7537
|
var init_claudeDesktopTap = __esm({
|
|
7536
7538
|
"cli/installer/claudeDesktopTap.ts"() {
|
|
7537
7539
|
"use strict";
|
|
7538
7540
|
CAPTURE_URL = "http://127.0.0.1:18931/api/local/capture";
|
|
7539
7541
|
SCAN_URL = "http://127.0.0.1:18931/api/local/scan-upload";
|
|
7542
|
+
SCAN_TURN_URL = "http://127.0.0.1:18931/api/local/scan-turn";
|
|
7543
|
+
DLP_POLICY_URL = "http://127.0.0.1:18931/api/local/dlp-policy";
|
|
7540
7544
|
JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
|
|
7541
7545
|
ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64
|
|
7542
7546
|
from mitmproxy import http
|
|
@@ -7544,6 +7548,8 @@ from mitmproxy import http
|
|
|
7544
7548
|
CAPTURE_URL = os.environ.get("SYNKRO_CAPTURE_URL", "")
|
|
7545
7549
|
TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
|
|
7546
7550
|
SCAN_URL = os.environ.get("SYNKRO_SCAN_URL", "")
|
|
7551
|
+
SCAN_TURN_URL = os.environ.get("SYNKRO_SCAN_TURN_URL", "")
|
|
7552
|
+
POLICY_URL = os.environ.get("SYNKRO_DLP_POLICY_URL", "")
|
|
7547
7553
|
# Token file path \u2014 the SERVER_TOKEN rotates whenever the container is recreated,
|
|
7548
7554
|
# so we re-read it from disk on a 401 instead of needing a tap restart.
|
|
7549
7555
|
TOKEN_FILE = os.environ.get("SYNKRO_TAP_TOKEN_FILE", "")
|
|
@@ -7609,6 +7615,122 @@ def _fmt_tool_name(raw):
|
|
|
7609
7615
|
pass
|
|
7610
7616
|
return raw
|
|
7611
7617
|
|
|
7618
|
+
# \u2500\u2500 M3 live-hold state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7619
|
+
# Whether to HOLD connector turns (buffer the live completion until graded).
|
|
7620
|
+
# Fetched once from the server: True only when a BLOCK-mode rule exists (+ not
|
|
7621
|
+
# silent). Audit-only orgs get False \u2192 stream + async audit, ZERO added latency.
|
|
7622
|
+
_dlp_hold = None
|
|
7623
|
+
_dlp_lock = threading.Lock()
|
|
7624
|
+
# Shown live in place of a held connector turn while the single grader (the tree
|
|
7625
|
+
# GET) runs; replaced by the real content (if clean) or the block notice.
|
|
7626
|
+
REVIEWING_NOTICE = "\\U0001F6E1\\uFE0F Synkro is reviewing this response for policy compliance\\u2026"
|
|
7627
|
+
|
|
7628
|
+
def _should_hold():
|
|
7629
|
+
global _dlp_hold
|
|
7630
|
+
with _dlp_lock:
|
|
7631
|
+
if _dlp_hold is not None:
|
|
7632
|
+
return _dlp_hold
|
|
7633
|
+
val = False
|
|
7634
|
+
if POLICY_URL:
|
|
7635
|
+
try:
|
|
7636
|
+
with urllib.request.urlopen(urllib.request.Request(POLICY_URL), timeout=4) as resp:
|
|
7637
|
+
val = bool(json.loads(resp.read().decode("utf-8", "replace")).get("hold"))
|
|
7638
|
+
except Exception:
|
|
7639
|
+
val = False
|
|
7640
|
+
with _dlp_lock:
|
|
7641
|
+
_dlp_hold = val
|
|
7642
|
+
return val
|
|
7643
|
+
|
|
7644
|
+
def _scan_turn_call(cid, msgs, connector):
|
|
7645
|
+
# Synchronous DLP grade. Returns the block reason (str) on BLOCK, else None.
|
|
7646
|
+
# Fail-OPEN \u2014 a scan error never blocks the turn.
|
|
7647
|
+
if not (SCAN_TURN_URL and TOKEN): return None
|
|
7648
|
+
try:
|
|
7649
|
+
vb = _post_json(SCAN_TURN_URL, json.dumps({"conversation_id": str(cid)[:80], "messages": msgs, "connector": connector}).encode(), 18)
|
|
7650
|
+
vd = json.loads((vb or b"{}").decode("utf-8", "replace"))
|
|
7651
|
+
if vd.get("block"):
|
|
7652
|
+
return str(vd.get("reason", "restricted data"))[:300]
|
|
7653
|
+
except Exception as e:
|
|
7654
|
+
print("[synkro] turn scan error (fail-open): " + str(e))
|
|
7655
|
+
return None
|
|
7656
|
+
|
|
7657
|
+
def _scan_sse_line(line, st):
|
|
7658
|
+
# Extract governance metadata (usage window + web-search sources/citations)
|
|
7659
|
+
# from one completion SSE line. Shared by the streaming + buffered paths.
|
|
7660
|
+
if st["usage"] is None:
|
|
7661
|
+
um = re.search(r'"5h":\\{"status":"([^"]*)","resets_at":(\\d+),"utilization":([0-9.]+)', line)
|
|
7662
|
+
if um:
|
|
7663
|
+
st["usage"] = {"window": "5h", "status": um.group(1), "resets_at": int(um.group(2)), "utilization": float(um.group(3))}
|
|
7664
|
+
if len(st["sources"]) < 50:
|
|
7665
|
+
for sm in re.finditer(r'"webpage_metadata","site_domain":"([^"]*)"(?:,"favicon_url":"[^"]*")?,"site_name":"([^"]*)"', line):
|
|
7666
|
+
d = sm.group(1)
|
|
7667
|
+
if d and d not in st["seen"] and len(st["sources"]) < 50:
|
|
7668
|
+
st["seen"].add(d)
|
|
7669
|
+
st["sources"].append({"domain": d, "name": sm.group(2)})
|
|
7670
|
+
if "citation_start_delta" in line and line.startswith("data: ") and len(st["citations"]) < 50:
|
|
7671
|
+
try:
|
|
7672
|
+
cit = json.loads(line[6:]).get("delta", {}).get("citation", {})
|
|
7673
|
+
url = cit.get("url", "")
|
|
7674
|
+
if url and url not in st["cseen"]:
|
|
7675
|
+
st["cseen"].add(url)
|
|
7676
|
+
md = cit.get("metadata") or {}
|
|
7677
|
+
st["citations"].append({"title": cit.get("title", ""), "url": url, "domain": md.get("site_domain", "")})
|
|
7678
|
+
except Exception:
|
|
7679
|
+
pass
|
|
7680
|
+
|
|
7681
|
+
def _new_scan_state():
|
|
7682
|
+
return {"carry": "", "usage": None, "sources": [], "seen": set(), "citations": [], "cseen": set()}
|
|
7683
|
+
|
|
7684
|
+
def _extract_sse_text(body):
|
|
7685
|
+
# Pull all visible text out of a buffered completion SSE (assistant text +
|
|
7686
|
+
# tool_result content) for grading, and detect whether a tool/connector ran.
|
|
7687
|
+
parts = []
|
|
7688
|
+
tool = ("tool_use" in body) or ("tool_result" in body)
|
|
7689
|
+
total = 0
|
|
7690
|
+
for line in body.split("\\n"):
|
|
7691
|
+
if total > 2000000: break
|
|
7692
|
+
if not line.startswith("data: "): continue
|
|
7693
|
+
try: obj = json.loads(line[6:])
|
|
7694
|
+
except Exception: continue
|
|
7695
|
+
d = obj.get("delta")
|
|
7696
|
+
if isinstance(d, dict) and isinstance(d.get("text"), str):
|
|
7697
|
+
parts.append(d["text"]); total += len(d["text"])
|
|
7698
|
+
cb = obj.get("content_block")
|
|
7699
|
+
if isinstance(cb, dict):
|
|
7700
|
+
if isinstance(cb.get("text"), str): parts.append(cb["text"]); total += len(cb["text"])
|
|
7701
|
+
c = cb.get("content")
|
|
7702
|
+
if isinstance(c, str): parts.append(c); total += len(c)
|
|
7703
|
+
elif isinstance(c, list):
|
|
7704
|
+
for x in c:
|
|
7705
|
+
if isinstance(x, dict) and isinstance(x.get("text"), str): parts.append(x["text"]); total += len(x["text"])
|
|
7706
|
+
return ("".join(parts)[:2000000], tool)
|
|
7707
|
+
|
|
7708
|
+
def _neutralize_sse(body, notice):
|
|
7709
|
+
# Rewrite a buffered completion SSE: blank every text/content field (inject the
|
|
7710
|
+
# notice into the FIRST text delta), leaving all structural events intact so
|
|
7711
|
+
# Claude Desktop still renders a valid \u2014 but content-free \u2014 turn.
|
|
7712
|
+
out = []
|
|
7713
|
+
injected = [False]
|
|
7714
|
+
for line in body.split("\\n"):
|
|
7715
|
+
if not line.startswith("data: "):
|
|
7716
|
+
out.append(line); continue
|
|
7717
|
+
try: obj = json.loads(line[6:])
|
|
7718
|
+
except Exception:
|
|
7719
|
+
out.append(line); continue
|
|
7720
|
+
changed = False
|
|
7721
|
+
d = obj.get("delta")
|
|
7722
|
+
if isinstance(d, dict) and isinstance(d.get("text"), str):
|
|
7723
|
+
d["text"] = (notice if not injected[0] else "")
|
|
7724
|
+
injected[0] = True; changed = True
|
|
7725
|
+
cb = obj.get("content_block")
|
|
7726
|
+
if isinstance(cb, dict):
|
|
7727
|
+
if isinstance(cb.get("text"), str) and cb["text"]:
|
|
7728
|
+
cb["text"] = ""; changed = True
|
|
7729
|
+
if cb.get("content"):
|
|
7730
|
+
cb["content"] = ""; changed = True
|
|
7731
|
+
out.append(("data: " + json.dumps(obj)) if changed else line)
|
|
7732
|
+
return "\\n".join(out)
|
|
7733
|
+
|
|
7612
7734
|
class SynkroTap:
|
|
7613
7735
|
def request(self, flow):
|
|
7614
7736
|
# \u2500\u2500 Upload DLP gate \u2500\u2500 Scan file uploads BEFORE they reach claude.ai and
|
|
@@ -7662,10 +7784,10 @@ class SynkroTap:
|
|
|
7662
7784
|
print("[synkro] upload scan error (fail-open): " + str(e))
|
|
7663
7785
|
return
|
|
7664
7786
|
if verdict.get("block"):
|
|
7665
|
-
reason = str(verdict.get("reason", "Blocked by Synkro
|
|
7787
|
+
reason = str(verdict.get("reason", "Blocked by Synkro policy"))[:300]
|
|
7666
7788
|
flow.response = http.Response.make(
|
|
7667
7789
|
403,
|
|
7668
|
-
json.dumps({"error": {"type": "
|
|
7790
|
+
json.dumps({"error": {"type": "blocked_by_synkro", "message": "Blocked by Synkro: " + reason}}).encode(),
|
|
7669
7791
|
{"Content-Type": "application/json"},
|
|
7670
7792
|
)
|
|
7671
7793
|
print("[synkro] BLOCKED upload (size=" + str(payload.get("size", 0)) + " rule=" + str(verdict.get("ruleId", "")) + ")")
|
|
@@ -7685,34 +7807,19 @@ class SynkroTap:
|
|
|
7685
7807
|
if r.method == "POST" and "completion" in r.path:
|
|
7686
7808
|
cm = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
|
|
7687
7809
|
cid = cm.group(1) if cm else ""
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
#
|
|
7691
|
-
#
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
if d and d not in _st["seen"] and len(_st["sources"]) < 50:
|
|
7702
|
-
_st["seen"].add(d)
|
|
7703
|
-
_st["sources"].append({"domain": d, "name": sm.group(2)})
|
|
7704
|
-
# Citations: the specific pages the model cited (title + url +
|
|
7705
|
-
# domain) \u2014 the real "tool result" for web search.
|
|
7706
|
-
if "citation_start_delta" in line and line.startswith("data: ") and len(_st["citations"]) < 50:
|
|
7707
|
-
try:
|
|
7708
|
-
cit = json.loads(line[6:]).get("delta", {}).get("citation", {})
|
|
7709
|
-
url = cit.get("url", "")
|
|
7710
|
-
if url and url not in _st["cseen"]:
|
|
7711
|
-
_st["cseen"].add(url)
|
|
7712
|
-
md = cit.get("metadata") or {}
|
|
7713
|
-
_st["citations"].append({"title": cit.get("title", ""), "url": url, "domain": md.get("site_domain", "")})
|
|
7714
|
-
except Exception:
|
|
7715
|
-
pass
|
|
7810
|
+
with _mcp_lock:
|
|
7811
|
+
has_servers = len(_mcp_registry) > 0
|
|
7812
|
+
# M3 LIVE HOLD: only when a BLOCK-mode rule exists AND connectors are
|
|
7813
|
+
# connected, BUFFER the whole completion (leave .stream unset) so
|
|
7814
|
+
# response() can grade it and neutralize restricted content BEFORE the
|
|
7815
|
+
# user sees a single token. Audit-only / no-connector turns skip this
|
|
7816
|
+
# entirely and stream as before \u2014 zero added latency.
|
|
7817
|
+
if has_servers and _should_hold():
|
|
7818
|
+
flow.metadata["synkro_hold_cid"] = cid
|
|
7819
|
+
return
|
|
7820
|
+
# Streaming path (unchanged behaviour): tap chunk-by-chunk, extract
|
|
7821
|
+
# governance metadata only, never buffer the body. Audit stays async.
|
|
7822
|
+
st = _new_scan_state()
|
|
7716
7823
|
def _tap(data, _st=st, _cid=cid):
|
|
7717
7824
|
if data:
|
|
7718
7825
|
try:
|
|
@@ -7722,9 +7829,9 @@ class SynkroTap:
|
|
|
7722
7829
|
lines = txt.split("\\n")
|
|
7723
7830
|
_st["carry"] = lines[-1][-200000:] # bound the carried partial line (CWE-400)
|
|
7724
7831
|
for ln in lines[:-1]:
|
|
7725
|
-
|
|
7832
|
+
_scan_sse_line(ln, _st)
|
|
7726
7833
|
else: # end of stream \u2014 flush carry + publish
|
|
7727
|
-
|
|
7834
|
+
_scan_sse_line(_st["carry"], _st)
|
|
7728
7835
|
if (_st["usage"] or _st["sources"] or _st["citations"]) and _cid:
|
|
7729
7836
|
with _sse_lock:
|
|
7730
7837
|
_sse_meta[_cid] = {"usage": _st["usage"], "sources": _st["sources"], "citations": _st["citations"]}
|
|
@@ -7738,6 +7845,36 @@ class SynkroTap:
|
|
|
7738
7845
|
def response(self, flow):
|
|
7739
7846
|
r = flow.request
|
|
7740
7847
|
if "claude.ai" not in (r.pretty_host or ""): return
|
|
7848
|
+
# \u2500\u2500 M3: a HELD completion (buffered for inline grading) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7849
|
+
# We have the full SSE body now. Re-derive governance metadata (same as the
|
|
7850
|
+
# streaming path), then \u2014 if the turn used a tool/connector \u2014 grade it
|
|
7851
|
+
# SYNCHRONOUSLY and, on a block, NEUTRALIZE the response so the user never
|
|
7852
|
+
# sees the restricted content. Verdict is cached for the tree GET to reuse.
|
|
7853
|
+
hold_cid = (flow.metadata.get("synkro_hold_cid") if hasattr(flow, "metadata") else None)
|
|
7854
|
+
if hold_cid is not None and "completion" in r.path and flow.response:
|
|
7855
|
+
try:
|
|
7856
|
+
body = flow.response.get_text() or ""
|
|
7857
|
+
except Exception:
|
|
7858
|
+
body = ""
|
|
7859
|
+
st = _new_scan_state()
|
|
7860
|
+
for ln in body.split("\\n"):
|
|
7861
|
+
_scan_sse_line(ln, st)
|
|
7862
|
+
if (st["usage"] or st["sources"] or st["citations"]) and hold_cid:
|
|
7863
|
+
with _sse_lock:
|
|
7864
|
+
_sse_meta[hold_cid] = {"usage": st["usage"], "sources": st["sources"], "citations": st["citations"]}
|
|
7865
|
+
text, has_tool = _extract_sse_text(body)
|
|
7866
|
+
if has_tool:
|
|
7867
|
+
# The turn used a connector \u2014 HOLD the live view: replace the streamed
|
|
7868
|
+
# content with a "reviewing" placeholder so the user never sees the raw
|
|
7869
|
+
# response. The tree GET is the SINGLE grader and renders the final
|
|
7870
|
+
# state (real content if clean, block notice if restricted) \u2014 so there
|
|
7871
|
+
# is exactly one full-document grade per turn (no double-grade race).
|
|
7872
|
+
try:
|
|
7873
|
+
flow.response.text = _neutralize_sse(body, REVIEWING_NOTICE)
|
|
7874
|
+
print("[synkro] HELD completion -> reviewing placeholder")
|
|
7875
|
+
except Exception as e:
|
|
7876
|
+
print("[synkro] hold placeholder failed (fail-open): " + str(e))
|
|
7877
|
+
return
|
|
7741
7878
|
# MCP server registry from the bootstrap SSE stream.
|
|
7742
7879
|
if "mcp" in r.path and "bootstrap" in r.path and flow.response:
|
|
7743
7880
|
try:
|
|
@@ -7854,6 +7991,35 @@ class SynkroTap:
|
|
|
7854
7991
|
ntools += len(mm.get("tool_calls") or [])
|
|
7855
7992
|
nsrc = len((sm or {}).get("sources") or [])
|
|
7856
7993
|
print("[synkro] captured chat (msgs=" + str(len(messages)) + " tools=" + str(ntools) + " sources=" + str(nsrc) + " model=" + model + ")")
|
|
7994
|
+
|
|
7995
|
+
# \u2500\u2500 Tier-2 connector DLP gate \u2500\u2500 If this turn pulled content via an MCP
|
|
7996
|
+
# connector, scan it SYNCHRONOUSLY against blocking org rules. On a block
|
|
7997
|
+
# verdict we HOLD this tree GET (the authoritative render) until the verdict
|
|
7998
|
+
# and then SCRUB the turn from the tree the renderer sees \u2014 the employee
|
|
7999
|
+
# never sees the restricted content. The REAL content is still captured
|
|
8000
|
+
# below for audit (record-everything-even-when-blocked). Fail-OPEN.
|
|
8001
|
+
blocked_reason = None
|
|
8002
|
+
has_connector = any(m.get("role") == "tool" and m.get("content") for m in messages)
|
|
8003
|
+
# SINGLE grader: the held completion only shows a "reviewing" placeholder, so
|
|
8004
|
+
# the tree GET is the one and only place a turn is graded \u2014 exactly one
|
|
8005
|
+
# full-document grade per turn (no double-grade). Gated on a BLOCK-mode rule
|
|
8006
|
+
# existing; audit-only orgs (_should_hold False) skip this and stay async.
|
|
8007
|
+
if _should_hold() and SCAN_TURN_URL and TOKEN and has_connector:
|
|
8008
|
+
# CWE-20/CWE-400: bounded, shape-validated payload (<=200 msgs, <=2MB).
|
|
8009
|
+
scan_msgs = []
|
|
8010
|
+
budget = 2000000
|
|
8011
|
+
for m in messages[:200]:
|
|
8012
|
+
if budget <= 0: break
|
|
8013
|
+
c = str(m.get("content", ""))[:min(500000, budget)]
|
|
8014
|
+
budget -= len(c)
|
|
8015
|
+
scan_msgs.append({"role": str(m.get("role", "user"))[:20], "content": c, "name": str(m.get("name", ""))[:200]})
|
|
8016
|
+
connector = ""
|
|
8017
|
+
for m in scan_msgs:
|
|
8018
|
+
if m["role"] == "tool" and m["name"]:
|
|
8019
|
+
nm = m["name"]; connector = (nm.split(":")[0] if ":" in nm else nm); break
|
|
8020
|
+
blocked_reason = _scan_turn_call(convo_id, scan_msgs, connector)
|
|
8021
|
+
|
|
8022
|
+
# Capture the REAL content for audit regardless of the block decision.
|
|
7857
8023
|
def send(_d=data):
|
|
7858
8024
|
try:
|
|
7859
8025
|
_post_json(CAPTURE_URL, json.dumps(_d).encode(), 5)
|
|
@@ -7861,6 +8027,20 @@ class SynkroTap:
|
|
|
7861
8027
|
print("[synkro] capture error: " + str(e))
|
|
7862
8028
|
threading.Thread(target=send, daemon=True).start()
|
|
7863
8029
|
|
|
8030
|
+
# Block the rendered view: replace this turn's assistant message content in
|
|
8031
|
+
# the tree (everything after the last human turn) with a Synkro notice, so
|
|
8032
|
+
# the restricted content + any model output derived from it is never shown.
|
|
8033
|
+
if blocked_reason is not None:
|
|
8034
|
+
notice = "[Blocked by Synkro] " + blocked_reason
|
|
8035
|
+
for i in range(last_human + 1, len(all_msgs)):
|
|
8036
|
+
if all_msgs[i].get("sender") != "human":
|
|
8037
|
+
all_msgs[i]["content"] = [{"type": "text", "text": notice}]
|
|
8038
|
+
try:
|
|
8039
|
+
flow.response.text = json.dumps(convo)
|
|
8040
|
+
print("[synkro] BLOCKED turn view (reason=" + blocked_reason[:80] + ")")
|
|
8041
|
+
except Exception as e:
|
|
8042
|
+
print("[synkro] block scrub failed (fail-open): " + str(e))
|
|
8043
|
+
|
|
7864
8044
|
addons = [SynkroTap()]
|
|
7865
8045
|
`;
|
|
7866
8046
|
MCP_PROXY_PY = `#!/usr/bin/env python3
|
|
@@ -9515,7 +9695,7 @@ function writeConfigEnv(opts) {
|
|
|
9515
9695
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
9516
9696
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
9517
9697
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
9518
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
9698
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.57")}`
|
|
9519
9699
|
];
|
|
9520
9700
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
9521
9701
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -9814,7 +9994,8 @@ async function installCommand(opts = {}) {
|
|
|
9814
9994
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
9815
9995
|
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
9816
9996
|
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
9817
|
-
installScanScriptPath: scripts.installScanScript
|
|
9997
|
+
installScanScriptPath: scripts.installScanScript,
|
|
9998
|
+
mcpGateScriptPath: scripts.mcpGateScript
|
|
9818
9999
|
});
|
|
9819
10000
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
9820
10001
|
}
|
|
@@ -10298,7 +10479,8 @@ function reconcileHarness() {
|
|
|
10298
10479
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
10299
10480
|
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
10300
10481
|
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
10301
|
-
installScanScriptPath: scripts.installScanScript
|
|
10482
|
+
installScanScriptPath: scripts.installScanScript,
|
|
10483
|
+
mcpGateScriptPath: scripts.mcpGateScript
|
|
10302
10484
|
});
|
|
10303
10485
|
console.log(" \u2713 Cursor hooks registered");
|
|
10304
10486
|
try {
|
|
@@ -12771,7 +12953,7 @@ var args = process.argv.slice(2);
|
|
|
12771
12953
|
var cmd = args[0] || "";
|
|
12772
12954
|
var subArgs = args.slice(1);
|
|
12773
12955
|
function printVersion() {
|
|
12774
|
-
console.log("1.6.
|
|
12956
|
+
console.log("1.6.57");
|
|
12775
12957
|
}
|
|
12776
12958
|
function printHelp2() {
|
|
12777
12959
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|