@synkro-sh/cli 1.6.90 → 1.6.92

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
@@ -2218,6 +2237,44 @@ function isPlaceholderSecret(v: string): boolean {
2218
2237
  const tail = v.replace(/^(sk-ant-|[rs]k_(?:live|test)_|sk-|AKIA|gh[posru]_|AIza|xox[baprs]-|Bearer[ ]+)/, '');
2219
2238
  return /^(x+|0+|a+|placeholder|example|changeme|your[-_a-z0-9]*|dummy|test|fake|redacted|sample)$/i.test(tail);
2220
2239
  }
2240
+ // \u2500\u2500 Opaque-secret heuristic (catches the FORMAT-LESS tail the regex can't) \u2500\u2500
2241
+ // A high-entropy value sitting in a SECRET CONTEXT \u2014 assigned to a secret-named
2242
+ // variable, in an auth header, or as a connection-string password. Local +
2243
+ // deterministic, no model. Char-class regexes only (template-literal backslash safe).
2244
+ function shannonEntropy(s: string): number {
2245
+ const m: Record<string, number> = {};
2246
+ for (const ch of s) m[ch] = (m[ch] || 0) + 1;
2247
+ let h = 0;
2248
+ for (const k in m) { const p = m[k] / s.length; h -= p * Math.log2(p); }
2249
+ return h;
2250
+ }
2251
+ function looksOpaqueSecret(v: string): boolean {
2252
+ if (v.length < 16) return false;
2253
+ if (/[$<>{}]/.test(v)) return false; // env ref / placeholder
2254
+ if (/^[0-9a-f]{32,}$/i.test(v)) return false; // hex hash / sha
2255
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v)) return false; // uuid
2256
+ if (!(/[a-zA-Z]/.test(v) && /[0-9]/.test(v))) return false; // real tokens mix letters+digits
2257
+ return shannonEntropy(v) >= 3.2;
2258
+ }
2259
+ const OPAQUE_CTX_PATTERNS: RegExp[] = [
2260
+ /[A-Za-z_][A-Za-z0-9_-]*(?:secret|token|key|password|passwd|api[_-]?key|auth|credential)[A-Za-z0-9_-]*[ ]*[=:][ ]*["']?([^ "'&|;]+)/gi,
2261
+ /(?:authorization|x-api-key|x-auth-token|api-key)[ ]*:[ ]*(?:bearer[ ]+)?([^ "']+)/gi,
2262
+ /[a-z][a-z0-9+.-]*:[/][/][^:/@ ]+:([^@/ ]+)@/gi,
2263
+ ];
2264
+ export function detectOpaqueSecrets(text: string): SecretHit[] {
2265
+ if (!text) return [];
2266
+ const hits: SecretHit[] = [];
2267
+ const seen = new Set<string>();
2268
+ for (const re of OPAQUE_CTX_PATTERNS) {
2269
+ re.lastIndex = 0;
2270
+ let m: RegExpExecArray | null;
2271
+ while ((m = re.exec(text)) !== null) {
2272
+ const v = m[1];
2273
+ if (v && !seen.has(v) && looksOpaqueSecret(v)) { seen.add(v); hits.push({ type: 'opaque-secret', value: v }); }
2274
+ }
2275
+ }
2276
+ return hits;
2277
+ }
2221
2278
  export function detectHardSecrets(text: string): SecretHit[] {
2222
2279
  if (!text) return [];
2223
2280
  const hits: SecretHit[] = [];
@@ -2232,6 +2289,10 @@ export function detectHardSecrets(text: string): SecretHit[] {
2232
2289
  hits.push({ type, value: v });
2233
2290
  }
2234
2291
  }
2292
+ // Union the format-less opaque-secret tail (entropy + secret context).
2293
+ for (const h of detectOpaqueSecrets(text)) {
2294
+ if (!seen.has(h.value)) { seen.add(h.value); hits.push(h); }
2295
+ }
2235
2296
  return hits;
2236
2297
  }
2237
2298
  // Shape-preserving redaction for persist/log/grade boundaries: the reader still
@@ -2247,6 +2308,8 @@ export function scrubSecrets(text: string): string {
2247
2308
  if (/[$<>{}]/.test(val)) return full;
2248
2309
  return pfx + '<redacted:credential>';
2249
2310
  });
2311
+ // Format-less opaque secrets (entropy + secret context) the patterns above miss.
2312
+ for (const h of detectOpaqueSecrets(out)) out = out.split(h.value).join('<redacted:opaque-secret>');
2250
2313
  return out;
2251
2314
  }
2252
2315
  function guessEnvName(type: string, i: number): string {
@@ -4320,7 +4383,7 @@ async function main() {
4320
4383
  // Load config and decide route
4321
4384
  const config = await loadConfig(jwt);
4322
4385
  const synkroFile = loadSynkroFile(cwd);
4323
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
4386
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
4324
4387
  const rt = await route(config, synkroFile);
4325
4388
  const tagStr = tag(rt, config, graderPool);
4326
4389
 
@@ -4814,7 +4877,7 @@ async function main() {
4814
4877
 
4815
4878
  const config = await loadConfig(jwt);
4816
4879
  const synkroFile = loadSynkroFile(cwd);
4817
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
4880
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
4818
4881
  const rt = await cweRoute(config, synkroFile);
4819
4882
 
4820
4883
  // Combined-grade mode: the edit-precheck hook runs CWE inside its single
@@ -5542,7 +5605,7 @@ async function main() {
5542
5605
  const config = await loadConfig(jwt);
5543
5606
  const isCursor = isCursorHookFormat();
5544
5607
  const synkroFile = loadSynkroFile(cwd);
5545
- const graderPool = effectiveGraderPool(synkroFile, isCursor ? 'cursor' : 'claude_code');
5608
+ const graderPool = effectiveGraderPool(synkroFile, isCursor ? 'cursor' : 'claude_code', config);
5546
5609
  const rt = await route(config, synkroFile);
5547
5610
  const tagStr = tag(rt, config, graderPool);
5548
5611
 
@@ -5724,7 +5787,7 @@ async function main() {
5724
5787
 
5725
5788
  const config = await loadConfig(jwt);
5726
5789
  const synkroFile = loadSynkroFile(cwd);
5727
- const graderPool = effectiveGraderPool(synkroFile, 'claude_code');
5790
+ const graderPool = effectiveGraderPool(synkroFile, 'claude_code', config);
5728
5791
  const rt = await route(config, synkroFile);
5729
5792
  const tagStr = tag(rt, config, graderPool);
5730
5793
 
@@ -6014,7 +6077,7 @@ async function main() {
6014
6077
 
6015
6078
  const config = await loadConfig(jwt);
6016
6079
  const synkroFile = loadSynkroFile(cwd);
6017
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
6080
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
6018
6081
  const rt = await route(config, synkroFile);
6019
6082
  const tagStr = tag(rt, config, graderPool);
6020
6083
 
@@ -6233,7 +6296,7 @@ async function main() {
6233
6296
 
6234
6297
  const config = await loadConfig(jwt);
6235
6298
  const synkroFile = loadSynkroFile(cwd);
6236
- const graderPool = effectiveGraderPool(synkroFile, agentKind);
6299
+ const graderPool = effectiveGraderPool(synkroFile, agentKind, config);
6237
6300
  const rt = await route(config, synkroFile);
6238
6301
  const tagStr = tag(rt, config, graderPool);
6239
6302
 
@@ -6469,12 +6532,15 @@ async function main() {
6469
6532
  return;
6470
6533
  }
6471
6534
 
6472
- // Sync grading mode to API profile so the dashboard reflects the actual config
6473
- const fastInference = gradingMode === 'byok';
6535
+ // Sync the canonical 3-state grading location to the API profile so the
6536
+ // dashboard reflects the machine's ACTUAL config. byok (own API key) wins;
6537
+ // else the cloud worker pool; else on-device workers. The API derives the
6538
+ // legacy fast_inference boolean from this, so we only send one field.
6539
+ const inferenceMode = gradingMode === 'byok' ? 'byok' : (deployCloud ? 'cloud' : 'local');
6474
6540
  fetch(GATEWAY_URL + '/api/v1/cli/me', {
6475
6541
  method: 'PATCH',
6476
6542
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
6477
- body: JSON.stringify({ fast_inference: fastInference }),
6543
+ body: JSON.stringify({ inference_mode: inferenceMode }),
6478
6544
  signal: AbortSignal.timeout(3000),
6479
6545
  }).catch(() => {});
6480
6546
 
@@ -6829,7 +6895,7 @@ async function main() {
6829
6895
  if (config.silent) finishAllow();
6830
6896
 
6831
6897
  const synkroFile = loadSynkroFile(cwd);
6832
- const graderPool = effectiveGraderPool(synkroFile, 'cursor');
6898
+ const graderPool = effectiveGraderPool(synkroFile, 'cursor', config);
6833
6899
  const rt = await route(config, synkroFile);
6834
6900
  const tagStr = tag(rt, config, graderPool);
6835
6901
 
@@ -11164,7 +11230,7 @@ function writeConfigEnv(opts) {
11164
11230
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11165
11231
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11166
11232
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11167
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.90")}`
11233
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.92")}`
11168
11234
  ];
11169
11235
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11170
11236
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15071,6 +15137,11 @@ function updateConfigValue(key, value) {
15071
15137
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
15072
15138
  writeFileSync13(CONFIG_PATH6, updated.join("\n"), "utf-8");
15073
15139
  }
15140
+ function resolveInferenceMode(cfg) {
15141
+ if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
15142
+ if (cfg.SYNKRO_DEPLOY_LOCATION === "cloud") return "cloud";
15143
+ return "local";
15144
+ }
15074
15145
  async function reconcileContainer() {
15075
15146
  const cfg = readConfigEnv2();
15076
15147
  const cloudOnly = (cfg.SYNKRO_GRADING_MODE || "local") === "byok" && (cfg.SYNKRO_STORAGE_MODE || "local") === "cloud";
@@ -15098,17 +15169,18 @@ async function configCommand(args2) {
15098
15169
  if (args2.length === 0) {
15099
15170
  const config2 = readConfigEnv2();
15100
15171
  console.log("Synkro config:\n");
15101
- console.log(` grading: ${config2.SYNKRO_GRADING_MODE || "local"}`);
15172
+ console.log(` inference: ${resolveInferenceMode(config2)}`);
15102
15173
  console.log(` storage: ${config2.SYNKRO_STORAGE_MODE || "local"}`);
15103
- console.log(` inference: ${config2.SYNKRO_INFERENCE || "fast"}`);
15104
15174
  console.log(` tier: ${config2.SYNKRO_TIER || "pro"}`);
15105
15175
  console.log(` gateway: ${config2.SYNKRO_GATEWAY_URL || "https://api.synkro.sh"}`);
15106
15176
  console.log(` version: ${config2.SYNKRO_VERSION || "?"}`);
15107
15177
  console.log(`
15108
15178
  To change:`);
15109
- console.log(` synkro config grading <local|byok> \u2014 where grading runs`);
15110
- console.log(` synkro config storage <local|cloud> \u2014 where telemetry is stored`);
15111
- console.log(` synkro config --inference fast|standard`);
15179
+ console.log(` synkro config --inference <local|cloud|byok> \u2014 where grading runs`);
15180
+ console.log(` local \u2014 Claude/Cursor workers on this machine (your subscription)`);
15181
+ console.log(` cloud \u2014 Claude/Cursor workers in the Synkro cloud pool`);
15182
+ console.log(` byok \u2014 graded via your own provider API key`);
15183
+ console.log(` synkro config storage <local|cloud> \u2014 where telemetry is stored`);
15112
15184
  return;
15113
15185
  }
15114
15186
  if (args2[0] === "grading") {
@@ -15142,8 +15214,8 @@ To change:`);
15142
15214
  if (a.startsWith("--inference=")) inferenceValue = a.slice("--inference=".length);
15143
15215
  else if (a === "--inference" && args2.indexOf(a) + 1 < args2.length) inferenceValue = args2[args2.indexOf(a) + 1];
15144
15216
  }
15145
- if (!inferenceValue || !["fast", "standard"].includes(inferenceValue)) {
15146
- console.error("Usage: synkro config --inference fast|standard");
15217
+ if (!inferenceValue || !["local", "cloud", "byok"].includes(inferenceValue)) {
15218
+ console.error("Usage: synkro config --inference <local|cloud|byok>");
15147
15219
  process.exit(1);
15148
15220
  }
15149
15221
  if (!isAuthenticated()) {
@@ -15160,7 +15232,7 @@ To change:`);
15160
15232
  "Authorization": `Bearer ${token}`,
15161
15233
  "Content-Type": "application/json"
15162
15234
  },
15163
- body: JSON.stringify({ fast_inference: inferenceValue === "fast" })
15235
+ body: JSON.stringify({ inference_mode: inferenceValue })
15164
15236
  });
15165
15237
  if (!resp.ok) {
15166
15238
  const errText = await resp.text().catch(() => "");
@@ -15171,8 +15243,24 @@ To change:`);
15171
15243
  console.error(`Failed to reach server: ${err.message}`);
15172
15244
  process.exit(1);
15173
15245
  }
15174
- updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
15246
+ if (inferenceValue === "byok") {
15247
+ updateConfigValue("SYNKRO_GRADING_MODE", "byok");
15248
+ updateConfigValue("SYNKRO_DEPLOY_LOCATION", "local");
15249
+ } else if (inferenceValue === "cloud") {
15250
+ updateConfigValue("SYNKRO_GRADING_MODE", "local");
15251
+ updateConfigValue("SYNKRO_DEPLOY_LOCATION", "cloud");
15252
+ } else {
15253
+ updateConfigValue("SYNKRO_GRADING_MODE", "local");
15254
+ updateConfigValue("SYNKRO_DEPLOY_LOCATION", "local");
15255
+ }
15175
15256
  console.log(`\u2713 Inference set to '${inferenceValue}'.`);
15257
+ if (inferenceValue === "byok") {
15258
+ console.log(" BYOK grading uses your own provider key \u2014 register one in the");
15259
+ console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.");
15260
+ } else if (inferenceValue === "cloud") {
15261
+ console.log(" Grading runs on the Synkro cloud worker pool (containers.synkro.sh).");
15262
+ }
15263
+ if (inferenceValue !== "cloud") await reconcileContainer();
15176
15264
  }
15177
15265
  var SYNKRO_DIR7, CONFIG_PATH6;
15178
15266
  var init_config = __esm({
@@ -15207,7 +15295,7 @@ var args = process.argv.slice(2);
15207
15295
  var cmd = args[0] || "";
15208
15296
  var subArgs = args.slice(1);
15209
15297
  function printVersion() {
15210
- console.log("1.6.90");
15298
+ console.log("1.6.92");
15211
15299
  }
15212
15300
  function printHelp2() {
15213
15301
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents