@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 +63 -4
- package/dist/bootstrap.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|