@synkro-sh/cli 1.6.91 → 1.6.93

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
@@ -1415,8 +1415,19 @@ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
1415
1415
  }
1416
1416
  }
1417
1417
 
1418
- export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind: AgentKind): AgentKind {
1419
- const pool = normalizePoolToken(synkroFile.grader.pool);
1418
+ export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind: AgentKind, config?: HookConfig): AgentKind {
1419
+ // Source of truth for the pool SPLIT depends on grading location:
1420
+ // \u2022 cloud / byok \u2192 SERVER-managed (the user's dashboard choice, delivered as
1421
+ // config.graderPool from /v1/hook/config). synkro.toml isn't even written
1422
+ // for cloud installs, so it can't be the source there.
1423
+ // \u2022 local \u2192 synkro.toml [grader] pool (on-device, user-owned).
1424
+ // Falls back to synkro.toml whenever the server pool is absent, so a missing
1425
+ // value never breaks grading \u2014 it just reverts to today's behavior.
1426
+ const managed = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud'
1427
+ || (process.env.SYNKRO_GRADING_MODE || synkroFile.grader?.mode || config?.gradingMode) === 'byok';
1428
+ const pool = (managed && config?.graderPool)
1429
+ ? normalizePoolToken(config.graderPool)
1430
+ : normalizePoolToken(synkroFile.grader.pool);
1420
1431
  if (pool === 'auto') return hookAgentKind;
1421
1432
  if (pool === 'claude') return 'claude_code';
1422
1433
  return 'cursor';
@@ -1517,6 +1528,11 @@ export interface HookConfig {
1517
1528
  // value from /v1/hook/config is only a fallback when the env var is unset.
1518
1529
  gradingMode: string;
1519
1530
  storageMode: string;
1531
+ // Server-managed pool SPLIT (auto | claude | cursor) + canonical grading
1532
+ // location, from /v1/hook/config. Authoritative ONLY for cloud/byok grading
1533
+ // (synkro.toml governs local). Undefined when the gateway didn't report one.
1534
+ graderPool?: string;
1535
+ inferenceMode?: string;
1520
1536
  }
1521
1537
 
1522
1538
  /** True when telemetry + rules must stay on-machine (PGLite). Default: local. */
@@ -1597,6 +1613,9 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1597
1613
  config.silent = data.silent_mode === true || data.silent_mode === 'true';
1598
1614
  if (!process.env.SYNKRO_GRADING_MODE && data.grading_mode) config.gradingMode = data.grading_mode;
1599
1615
  if (!process.env.SYNKRO_STORAGE_MODE && data.storage_mode) config.storageMode = data.storage_mode;
1616
+ // Server-managed pool split + location (cloud/byok only \u2014 see effectiveGraderPool).
1617
+ if (data.grader_pool) config.graderPool = data.grader_pool;
1618
+ if (data.inference_mode) config.inferenceMode = data.inference_mode;
1600
1619
  config.policyName = data.active_policy_name || '';
1601
1620
  if (Array.isArray(data.scan_exemptions)) {
1602
1621
  config.scanExemptions = data.scan_exemptions
@@ -4364,7 +4383,7 @@ async function main() {
4364
4383
  // Load config and decide route
4365
4384
  const config = await loadConfig(jwt);
4366
4385
  const synkroFile = loadSynkroFile(cwd);
4367
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
4386
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
4368
4387
  const rt = await route(config, synkroFile);
4369
4388
  const tagStr = tag(rt, config, graderPool);
4370
4389
 
@@ -4858,7 +4877,7 @@ async function main() {
4858
4877
 
4859
4878
  const config = await loadConfig(jwt);
4860
4879
  const synkroFile = loadSynkroFile(cwd);
4861
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
4880
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
4862
4881
  const rt = await cweRoute(config, synkroFile);
4863
4882
 
4864
4883
  // Combined-grade mode: the edit-precheck hook runs CWE inside its single
@@ -5586,7 +5605,7 @@ async function main() {
5586
5605
  const config = await loadConfig(jwt);
5587
5606
  const isCursor = isCursorHookFormat();
5588
5607
  const synkroFile = loadSynkroFile(cwd);
5589
- const graderPool = effectiveGraderPool(synkroFile, isCursor ? 'cursor' : 'claude_code');
5608
+ const graderPool = effectiveGraderPool(synkroFile, isCursor ? 'cursor' : 'claude_code', config);
5590
5609
  const rt = await route(config, synkroFile);
5591
5610
  const tagStr = tag(rt, config, graderPool);
5592
5611
 
@@ -5768,7 +5787,7 @@ async function main() {
5768
5787
 
5769
5788
  const config = await loadConfig(jwt);
5770
5789
  const synkroFile = loadSynkroFile(cwd);
5771
- const graderPool = effectiveGraderPool(synkroFile, 'claude_code');
5790
+ const graderPool = effectiveGraderPool(synkroFile, 'claude_code', config);
5772
5791
  const rt = await route(config, synkroFile);
5773
5792
  const tagStr = tag(rt, config, graderPool);
5774
5793
 
@@ -5777,35 +5796,14 @@ async function main() {
5777
5796
  return;
5778
5797
  }
5779
5798
 
5780
- if (isSafeInRepoRead(toolName, command, cwd)) {
5781
- log('bashGuard ' + cmdShort + ' → instant allow (safe in-repo read)');
5782
- appendLocalTelemetry({
5783
- capture_type: 'local_verdict',
5784
- verdict: 'pass',
5785
- hook_type: 'bash',
5786
- category: 'safe_read',
5787
- tool_name: toolName,
5788
- command: command.slice(0, 200),
5789
- session_id: sessionId,
5790
- repo: gitRepo || cwd,
5791
- cc_model: transcript.ccModel,
5792
- reasoning: 'Safe in-repo read — auto-allowed without an LLM grade.',
5793
- });
5794
- outputJson({
5795
- systemMessage: tagStr + ' bashGuard → pass: safe in-repo read',
5796
- hookSpecificOutput: {
5797
- hookEventName: 'PreToolUse',
5798
- additionalContext: tagStr + ' bashGuard pass: safe in-repo read.',
5799
- },
5800
- });
5801
- return;
5802
- }
5803
-
5804
5799
  // ─── Secret firewall: BLOCK a literal credential in the command BEFORE it runs
5805
5800
  // or reaches the grader — the value never lands in a file, shell history, OR the
5806
5801
  // LLM provider (no leak to Anthropic/Cursor). Deterministic + local, no model
5807
5802
  // call. High-confidence formats only (a false block stops the user), and
5808
- // $VAR / placeholder refs never trip it. ───
5803
+ // $VAR / placeholder refs never trip it. MUST run BEFORE the safe-read fast-path
5804
+ // below — otherwise an echo/printf of a literal secret is treated as a safe read
5805
+ // and waved through. A genuine read carries no literal secret, so this never
5806
+ // false-blocks legitimate reads. ───
5809
5807
  const secretHits = detectHardSecrets(command);
5810
5808
  if (secretHits.length) {
5811
5809
  const types = secretHits.map(h => h.type).join(', ');
@@ -5830,6 +5828,32 @@ async function main() {
5830
5828
  return;
5831
5829
  }
5832
5830
 
5831
+ // Safe in-repo read fast-path — runs AFTER the secret firewall above so a
5832
+ // literal credential is never waved through as a "safe read".
5833
+ if (isSafeInRepoRead(toolName, command, cwd)) {
5834
+ log('bashGuard ' + cmdShort + ' → instant allow (safe in-repo read)');
5835
+ appendLocalTelemetry({
5836
+ capture_type: 'local_verdict',
5837
+ verdict: 'pass',
5838
+ hook_type: 'bash',
5839
+ category: 'safe_read',
5840
+ tool_name: toolName,
5841
+ command: command.slice(0, 200),
5842
+ session_id: sessionId,
5843
+ repo: gitRepo || cwd,
5844
+ cc_model: transcript.ccModel,
5845
+ reasoning: 'Safe in-repo read — auto-allowed without an LLM grade.',
5846
+ });
5847
+ outputJson({
5848
+ systemMessage: tagStr + ' bashGuard → pass: safe in-repo read',
5849
+ hookSpecificOutput: {
5850
+ hookEventName: 'PreToolUse',
5851
+ additionalContext: tagStr + ' bashGuard pass: safe in-repo read.',
5852
+ },
5853
+ });
5854
+ return;
5855
+ }
5856
+
5833
5857
  // ─── Install protection: install-scan runs first and owns block traces ───
5834
5858
  let scanConcern = '';
5835
5859
  let scanBlockContext = '';
@@ -6058,7 +6082,7 @@ async function main() {
6058
6082
 
6059
6083
  const config = await loadConfig(jwt);
6060
6084
  const synkroFile = loadSynkroFile(cwd);
6061
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
6085
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
6062
6086
  const rt = await route(config, synkroFile);
6063
6087
  const tagStr = tag(rt, config, graderPool);
6064
6088
 
@@ -6277,7 +6301,7 @@ async function main() {
6277
6301
 
6278
6302
  const config = await loadConfig(jwt);
6279
6303
  const synkroFile = loadSynkroFile(cwd);
6280
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
6304
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
6281
6305
  const rt = await route(config, synkroFile);
6282
6306
  const tagStr = tag(rt, config, graderPool);
6283
6307
 
@@ -6513,12 +6537,15 @@ async function main() {
6513
6537
  return;
6514
6538
  }
6515
6539
 
6516
- // Sync grading mode to API profile so the dashboard reflects the actual config
6517
- const fastInference = gradingMode === 'byok';
6540
+ // Sync the canonical 3-state grading location to the API profile so the
6541
+ // dashboard reflects the machine's ACTUAL config. byok (own API key) wins;
6542
+ // else the cloud worker pool; else on-device workers. The API derives the
6543
+ // legacy fast_inference boolean from this, so we only send one field.
6544
+ const inferenceMode = gradingMode === 'byok' ? 'byok' : (deployCloud ? 'cloud' : 'local');
6518
6545
  fetch(GATEWAY_URL + '/api/v1/cli/me', {
6519
6546
  method: 'PATCH',
6520
6547
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
6521
- body: JSON.stringify({ fast_inference: fastInference }),
6548
+ body: JSON.stringify({ inference_mode: inferenceMode }),
6522
6549
  signal: AbortSignal.timeout(3000),
6523
6550
  }).catch(() => {});
6524
6551
 
@@ -6850,6 +6877,25 @@ async function main() {
6850
6877
 
6851
6878
  // Instant-allow read-only tool calls + safe in-repo bash reads \u2014 no grade,
6852
6879
  // no network. Critical under Cursor's tight 15s beforeShellExecution budget.
6880
+ // \u2500\u2500\u2500 Secret firewall: BLOCK a literal credential in the command BEFORE it runs
6881
+ // or reaches the grader \u2014 never leaks to a file, shell history, or the model
6882
+ // (Anthropic/Cursor). Local + deterministic; $VAR / placeholder refs never trip
6883
+ // it. MUST run BEFORE the safe-read fast-path below \u2014 else an echo/printf of a
6884
+ // literal secret is waved through as a "safe read". \u2500\u2500\u2500
6885
+ const secretHits = detectHardSecrets(command);
6886
+ if (secretHits.length) {
6887
+ const types = secretHits.map(h => h.type).join(', ');
6888
+ const remediation = secretRemediation(command, secretHits);
6889
+ log('bashGuard ' + cmdShort + ' \u2192 BLOCKED: literal credential (' + types + ')');
6890
+ appendLocalTelemetry({
6891
+ capture_type: 'local_verdict', verdict: 'block', hook_type: 'bash',
6892
+ category: 'secret_exposure', tool_name: toolName, command: scrubSecrets(command).slice(0, 200),
6893
+ session_id: sessionId, repo: repo || cwd, cc_model: model,
6894
+ reasoning: 'Literal credential (' + types + ') in the proposed command \u2014 blocked locally before grading; never sent to the model.',
6895
+ });
6896
+ finishWith({ permission: 'deny', user_message: '[synkro] bashGuard \u2192 BLOCKED: literal credential in command (' + types + '). Use an env var reference instead.', agent_message: remediation });
6897
+ }
6898
+
6853
6899
  if (isSafeInRepoRead(toolName, command, cwd)) {
6854
6900
  log('bashGuard ' + cmdShort + ' \u2192 instant allow (safe in-repo read)');
6855
6901
  appendLocalTelemetry({
@@ -6873,27 +6919,10 @@ async function main() {
6873
6919
  if (config.silent) finishAllow();
6874
6920
 
6875
6921
  const synkroFile = loadSynkroFile(cwd);
6876
- const graderPool = effectiveGraderPool(synkroFile, 'cursor');
6922
+ const graderPool = effectiveGraderPool(synkroFile, 'cursor', config);
6877
6923
  const rt = await route(config, synkroFile);
6878
6924
  const tagStr = tag(rt, config, graderPool);
6879
6925
 
6880
- // Secret firewall: BLOCK a literal credential in the command BEFORE it runs or
6881
- // reaches the grader \u2014 never leaks to a file, shell history, or the model
6882
- // (Anthropic/Cursor). Local + deterministic; $VAR / placeholder refs never trip it.
6883
- const secretHits = detectHardSecrets(command);
6884
- if (secretHits.length) {
6885
- const types = secretHits.map(h => h.type).join(', ');
6886
- const remediation = secretRemediation(command, secretHits);
6887
- log('bashGuard ' + cmdShort + ' \u2192 BLOCKED: literal credential (' + types + ')');
6888
- appendLocalTelemetry({
6889
- capture_type: 'local_verdict', verdict: 'block', hook_type: 'bash',
6890
- category: 'secret_exposure', tool_name: toolName, command: scrubSecrets(command).slice(0, 200),
6891
- session_id: sessionId, repo: repo || cwd, cc_model: model,
6892
- reasoning: 'Literal credential (' + types + ') in the proposed command \u2014 blocked locally before grading; never sent to the model.',
6893
- });
6894
- finishWith({ permission: 'deny', user_message: tagStr + ' bashGuard \u2192 BLOCKED: literal credential in command (' + types + '). Use an env var reference instead.', agent_message: remediation });
6895
- }
6896
-
6897
6926
  // Install protection \u2014 install-scan runs first and owns block traces.
6898
6927
  let scanConcern = '';
6899
6928
  let scanBlockContext = '';
@@ -11208,7 +11237,7 @@ function writeConfigEnv(opts) {
11208
11237
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11209
11238
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11210
11239
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11211
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.91")}`
11240
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.93")}`
11212
11241
  ];
11213
11242
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11214
11243
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15115,6 +15144,11 @@ function updateConfigValue(key, value) {
15115
15144
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
15116
15145
  writeFileSync13(CONFIG_PATH6, updated.join("\n"), "utf-8");
15117
15146
  }
15147
+ function resolveInferenceMode(cfg) {
15148
+ if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
15149
+ if (cfg.SYNKRO_DEPLOY_LOCATION === "cloud") return "cloud";
15150
+ return "local";
15151
+ }
15118
15152
  async function reconcileContainer() {
15119
15153
  const cfg = readConfigEnv2();
15120
15154
  const cloudOnly = (cfg.SYNKRO_GRADING_MODE || "local") === "byok" && (cfg.SYNKRO_STORAGE_MODE || "local") === "cloud";
@@ -15142,17 +15176,18 @@ async function configCommand(args2) {
15142
15176
  if (args2.length === 0) {
15143
15177
  const config2 = readConfigEnv2();
15144
15178
  console.log("Synkro config:\n");
15145
- console.log(` grading: ${config2.SYNKRO_GRADING_MODE || "local"}`);
15179
+ console.log(` inference: ${resolveInferenceMode(config2)}`);
15146
15180
  console.log(` storage: ${config2.SYNKRO_STORAGE_MODE || "local"}`);
15147
- console.log(` inference: ${config2.SYNKRO_INFERENCE || "fast"}`);
15148
15181
  console.log(` tier: ${config2.SYNKRO_TIER || "pro"}`);
15149
15182
  console.log(` gateway: ${config2.SYNKRO_GATEWAY_URL || "https://api.synkro.sh"}`);
15150
15183
  console.log(` version: ${config2.SYNKRO_VERSION || "?"}`);
15151
15184
  console.log(`
15152
15185
  To change:`);
15153
- console.log(` synkro config grading <local|byok> \u2014 where grading runs`);
15154
- console.log(` synkro config storage <local|cloud> \u2014 where telemetry is stored`);
15155
- console.log(` synkro config --inference fast|standard`);
15186
+ console.log(` synkro config --inference <local|cloud|byok> \u2014 where grading runs`);
15187
+ console.log(` local \u2014 Claude/Cursor workers on this machine (your subscription)`);
15188
+ console.log(` cloud \u2014 Claude/Cursor workers in the Synkro cloud pool`);
15189
+ console.log(` byok \u2014 graded via your own provider API key`);
15190
+ console.log(` synkro config storage <local|cloud> \u2014 where telemetry is stored`);
15156
15191
  return;
15157
15192
  }
15158
15193
  if (args2[0] === "grading") {
@@ -15186,8 +15221,8 @@ To change:`);
15186
15221
  if (a.startsWith("--inference=")) inferenceValue = a.slice("--inference=".length);
15187
15222
  else if (a === "--inference" && args2.indexOf(a) + 1 < args2.length) inferenceValue = args2[args2.indexOf(a) + 1];
15188
15223
  }
15189
- if (!inferenceValue || !["fast", "standard"].includes(inferenceValue)) {
15190
- console.error("Usage: synkro config --inference fast|standard");
15224
+ if (!inferenceValue || !["local", "cloud", "byok"].includes(inferenceValue)) {
15225
+ console.error("Usage: synkro config --inference <local|cloud|byok>");
15191
15226
  process.exit(1);
15192
15227
  }
15193
15228
  if (!isAuthenticated()) {
@@ -15204,7 +15239,7 @@ To change:`);
15204
15239
  "Authorization": `Bearer ${token}`,
15205
15240
  "Content-Type": "application/json"
15206
15241
  },
15207
- body: JSON.stringify({ fast_inference: inferenceValue === "fast" })
15242
+ body: JSON.stringify({ inference_mode: inferenceValue })
15208
15243
  });
15209
15244
  if (!resp.ok) {
15210
15245
  const errText = await resp.text().catch(() => "");
@@ -15215,8 +15250,24 @@ To change:`);
15215
15250
  console.error(`Failed to reach server: ${err.message}`);
15216
15251
  process.exit(1);
15217
15252
  }
15218
- updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
15253
+ if (inferenceValue === "byok") {
15254
+ updateConfigValue("SYNKRO_GRADING_MODE", "byok");
15255
+ updateConfigValue("SYNKRO_DEPLOY_LOCATION", "local");
15256
+ } else if (inferenceValue === "cloud") {
15257
+ updateConfigValue("SYNKRO_GRADING_MODE", "local");
15258
+ updateConfigValue("SYNKRO_DEPLOY_LOCATION", "cloud");
15259
+ } else {
15260
+ updateConfigValue("SYNKRO_GRADING_MODE", "local");
15261
+ updateConfigValue("SYNKRO_DEPLOY_LOCATION", "local");
15262
+ }
15219
15263
  console.log(`\u2713 Inference set to '${inferenceValue}'.`);
15264
+ if (inferenceValue === "byok") {
15265
+ console.log(" BYOK grading uses your own provider key \u2014 register one in the");
15266
+ console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.");
15267
+ } else if (inferenceValue === "cloud") {
15268
+ console.log(" Grading runs on the Synkro cloud worker pool (containers.synkro.sh).");
15269
+ }
15270
+ if (inferenceValue !== "cloud") await reconcileContainer();
15220
15271
  }
15221
15272
  var SYNKRO_DIR7, CONFIG_PATH6;
15222
15273
  var init_config = __esm({
@@ -15251,7 +15302,7 @@ var args = process.argv.slice(2);
15251
15302
  var cmd = args[0] || "";
15252
15303
  var subArgs = args.slice(1);
15253
15304
  function printVersion() {
15254
- console.log("1.6.91");
15305
+ console.log("1.6.93");
15255
15306
  }
15256
15307
  function printHelp2() {
15257
15308
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents