@synkro-sh/cli 1.6.68 → 1.6.70

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 CHANGED
@@ -1622,7 +1622,12 @@ export function tag(rt: string, config: HookConfig, grader?: string): string {
1622
1622
  if (config.silent) return '[synkro:silent]';
1623
1623
  const rs = config.policyName || 'all';
1624
1624
  const g = grader ? ':' + (grader === 'claude_code' ? 'claude' : grader) : '';
1625
- return '[synkro:' + rt + ':' + rs + g + ']';
1625
+ // route() collapses cloud-container to 'local' for BRANCH selection (the local
1626
+ // path emits the rich systemMessage), but the grade actually ran on the hosted
1627
+ // container. Surface the real location so a cloud grade reads [synkro:cloud:\u2026],
1628
+ // and 'local' is reserved for an actual on-device channel grade.
1629
+ const loc = (rt === 'local' && process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') ? 'cloud' : rt;
1630
+ return '[synkro:' + loc + ':' + rs + g + ']';
1626
1631
  }
1627
1632
 
1628
1633
  // \u2500\u2500\u2500 Local Grading (direct channel call) \u2500\u2500\u2500
@@ -6420,6 +6425,50 @@ function loadMcpJwt(): string {
6420
6425
  try { return readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; }
6421
6426
  }
6422
6427
 
6428
+ let _cfgCache: Record<string, string> | null = null;
6429
+ function cfgVal(key: string): string {
6430
+ if (process.env[key]) return process.env[key] as string;
6431
+ if (!_cfgCache) {
6432
+ _cfgCache = {};
6433
+ try {
6434
+ const txt = readFileSync(join(HOME, '.synkro', 'config.env'), 'utf-8');
6435
+ for (const line of txt.split('\n')) {
6436
+ const mm = line.match(/^([A-Z0-9_]+)=['"]?(.*?)['"]?\s*$/);
6437
+ if (mm) _cfgCache[mm[1]] = mm[2];
6438
+ }
6439
+ } catch {}
6440
+ }
6441
+ return _cfgCache[key] || '';
6442
+ }
6443
+ function deployIsCloud(): boolean { return cfgVal('SYNKRO_DEPLOY_LOCATION') === 'cloud'; }
6444
+
6445
+ // Cloud MCP gate: in cloud mode the local server (:18931) isn't running, so the
6446
+ // gate asks the org policy directly (api /api/mcp/gate, authed by .mcp-jwt).
6447
+ // Fail-open by contract — any error or non-block verdict allows the call.
6448
+ async function cloudMcpGate(payload: any, harness: string): Promise<string> {
6449
+ try {
6450
+ const toolName = String(payload.tool_name || payload.tool || '');
6451
+ const parts = toolName.split('__');
6452
+ const server = (parts.length >= 3 && parts[0] === 'mcp') ? parts[1] : '';
6453
+ if (!server) return failOpen(harness);
6454
+ const base = (cfgVal('SYNKRO_GATEWAY_URL') || 'https://api.synkro.sh').replace(/\/+$/, '');
6455
+ const resp = await fetch(base + '/api/mcp/gate', {
6456
+ method: 'POST',
6457
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
6458
+ body: JSON.stringify({ server: server, tool: parts.slice(2).join('__') }),
6459
+ signal: AbortSignal.timeout(5000),
6460
+ });
6461
+ if (!resp.ok) return failOpen(harness);
6462
+ const d = await resp.json() as any;
6463
+ if (d && d.decision === 'block') {
6464
+ const reason = String(d.reason || ('MCP server "' + server + '" blocked by policy'));
6465
+ if (harness === 'cursor') return JSON.stringify({ permission: 'deny', user_message: reason });
6466
+ return JSON.stringify({ systemMessage: '[synkro:cloud:mcp] ' + reason, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: reason } });
6467
+ }
6468
+ return failOpen(harness);
6469
+ } catch { return failOpen(harness); }
6470
+ }
6471
+
6423
6472
  async function readStdin(): Promise<string> {
6424
6473
  const chunks: Buffer[] = [];
6425
6474
  for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -6479,6 +6528,10 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
6479
6528
  if (!input.trim()) { out(failOpen(harness)); return; }
6480
6529
  const payload = JSON.parse(input);
6481
6530
 
6531
+ // Cloud MCP enforcement: the gate can't reach the local server, so decide
6532
+ // against the org policy directly. Fail-open inside cloudMcpGate.
6533
+ if (surface === 'mcp-gate' && deployIsCloud()) { out(await cloudMcpGate(payload, harness)); return; }
6534
+
6482
6535
  const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots : [];
6483
6536
  const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
6484
6537
  const sessionId = String(payload.session_id || payload.conversation_id || '');
@@ -10331,7 +10384,7 @@ function writeConfigEnv(opts) {
10331
10384
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10332
10385
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10333
10386
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10334
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.68")}`
10387
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.70")}`
10335
10388
  ];
10336
10389
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10337
10390
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -10880,7 +10933,13 @@ async function installCommand(opts = {}) {
10880
10933
  }
10881
10934
  const transcriptConsent = transcriptCC || transcriptCursor;
10882
10935
  ensureSynkroDir();
10883
- const hookMode = opts.hookMode || resolvePersistedHookMode();
10936
+ let hookMode = opts.hookMode || resolvePersistedHookMode();
10937
+ if (target === "cloud-container" && hookMode === "stub") {
10938
+ if (opts.hookMode === "stub") {
10939
+ console.log(" \u24D8 Cloud mode needs full hooks (stub hooks require a local container) \u2014 using full.\n");
10940
+ }
10941
+ hookMode = "full";
10942
+ }
10884
10943
  const scripts = writeHookScripts(hookMode);
10885
10944
  console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
10886
10945
  for (const mode of ["edit", "bash"]) {
@@ -13919,7 +13978,7 @@ var args = process.argv.slice(2);
13919
13978
  var cmd = args[0] || "";
13920
13979
  var subArgs = args.slice(1);
13921
13980
  function printVersion() {
13922
- console.log("1.6.68");
13981
+ console.log("1.6.70");
13923
13982
  }
13924
13983
  function printHelp2() {
13925
13984
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents