@synkro-sh/cli 1.7.2 → 1.7.4

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
@@ -1426,7 +1426,7 @@ export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind:
1426
1426
  // Falls back to synkro.toml whenever the server pool is absent, so a missing
1427
1427
  // value never breaks grading \u2014 it just reverts to today's behavior.
1428
1428
  const managed = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud'
1429
- || (process.env.SYNKRO_GRADING_MODE || synkroFile.grader?.mode || config?.gradingMode) === 'byok';
1429
+ || (process.env.SYNKRO_GRADING_MODE || config?.gradingMode || synkroFile.grader?.mode) === 'byok';
1430
1430
  const pool = (managed && config?.graderPool)
1431
1431
  ? normalizePoolToken(config.graderPool)
1432
1432
  : normalizePoolToken(synkroFile.grader.pool);
@@ -1632,7 +1632,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1632
1632
  // \u2500\u2500\u2500 Routing \u2500\u2500\u2500
1633
1633
 
1634
1634
  export async function route(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'cloud'> {
1635
- const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
1635
+ const gradingMode = process.env.SYNKRO_GRADING_MODE || config.gradingMode || synkroFile?.grader?.mode || 'local';
1636
1636
  if (gradingMode === 'byok') return 'cloud';
1637
1637
  // Cloud-container deploy ([grader] location = cloud): grade via localGrade(),
1638
1638
  // which routes to the hosted container (containers.synkro.sh) and emits the
@@ -1653,7 +1653,7 @@ export async function route(config: HookConfig, synkroFile?: SynkroFileConfig):
1653
1653
  }
1654
1654
 
1655
1655
  export async function cweRoute(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'byok' | 'skip'> {
1656
- const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
1656
+ const gradingMode = process.env.SYNKRO_GRADING_MODE || config.gradingMode || synkroFile?.grader?.mode || 'local';
1657
1657
  if (gradingMode === 'byok') return 'byok';
1658
1658
  // Cloud-container: CWE scanning runs on the hosted container via localGradeCwe()
1659
1659
  // (which honors SYNKRO_DEPLOY_LOCATION). Without this, cweChannelUp() is false in
@@ -1951,6 +1951,62 @@ export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'clau
1951
1951
  return channelGrade('grade-cwe', prompt, jwt, 18930, timeoutMs, agentKind);
1952
1952
  }
1953
1953
 
1954
+ // \u2500\u2500\u2500 Fix-poll directive (ask-mode human-preference capture) \u2500\u2500\u2500
1955
+ // Turns an ask-mode flag into a labeled DPO pair: candidate A is the grader's
1956
+ // suggested fix; candidate B is a best-effort second grade nudged toward a
1957
+ // different technique. Registers a pending fix_labels item and returns an agent
1958
+ // directive to poll the user + call record_fix_choice. PURELY ADDITIVE \u2014 any
1959
+ // failure returns '' so enforcement behaves exactly as before.
1960
+ export async function buildFixPollDirective(o: { candA: string; code: string; ruleId: string; sessionId: string; filePath: string; model: string; graderPrompt: string; graderPool?: AgentKind }): Promise<string> {
1961
+ try {
1962
+ const candA = (o.candA || '').trim();
1963
+ if (!candA) return '';
1964
+ const cands: string[] = [candA];
1965
+ try {
1966
+ // Candidate B from the SAME grader that produced A (the edit/rule grader),
1967
+ // reusing its prompt + rules \u2014 nudged toward a different remediation. Using
1968
+ // grade-cwe here would mis-grade an org-rule violation ('no rules' / wrong
1969
+ // surface), so we re-grade the edit surface. Best-effort: 1 candidate is fine.
1970
+ const altPrompt = (o.graderPrompt || '') + '\\n\\n// SECURITY REVIEWER: also propose an ALTERNATIVE remediation that uses a DIFFERENT technique than the most obvious one.';
1971
+ const rawB = await localGrade('edit', altPrompt, 12000, o.graderPool);
1972
+ const candB = (parseVerdict(rawB).suggestedFix || '').trim();
1973
+ if (candB && candB !== candA) cands.push(candB);
1974
+ } catch (e) { /* best-effort: one candidate is fine */ }
1975
+ // Route by deploy location: cloud -> the gateway (Timescale, org-scoped, auth'd);
1976
+ // local -> the on-device server (PGLite). GATEWAY_URL is host-allowlisted; the
1977
+ // loopback port is range-validated (CWE-918) so neither is an arbitrary host.
1978
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
1979
+ let url: string;
1980
+ if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
1981
+ const jwt = loadJwt();
1982
+ if (!jwt) return '';
1983
+ headers['Authorization'] = 'Bearer ' + jwt;
1984
+ url = GATEWAY_URL + '/api/v1/cli/fix-poll';
1985
+ } else {
1986
+ const rawPort = parseInt(process.env.SYNKRO_MCP_PORT || '18931', 10);
1987
+ const mcpPort = (rawPort > 0 && rawPort < 65536) ? rawPort : 18931;
1988
+ url = 'http://127.0.0.1:' + mcpPort + '/api/local/fix-poll';
1989
+ }
1990
+ const resp = await fetch(url, {
1991
+ method: 'POST',
1992
+ headers,
1993
+ body: JSON.stringify({
1994
+ session_id: o.sessionId, file_path: o.filePath, cwe: o.ruleId,
1995
+ vulnerable_code: (o.code || '').slice(0, 8000), candidates: cands, generator_model: o.model || '',
1996
+ }),
1997
+ signal: AbortSignal.timeout(5000),
1998
+ });
1999
+ if (!resp.ok) return '';
2000
+ const j = await resp.json() as { item_id?: string };
2001
+ if (!j || !j.item_id) return '';
2002
+ let optsStr = '';
2003
+ for (let i = 0; i < cands.length; i++) optsStr += '\\n [' + i + '] ' + cands[i];
2004
+ return '\\n\\nSYNKRO FIX POLL (item_id=' + j.item_id + '): Before retrying, present these candidate fixes to the user with the AskUserQuestion tool (one option per candidate, plus a "None of the above" option), then call the record_fix_choice tool with item_id="' + j.item_id + '" and chosen_idx set to the 0-based option the user picked (-1 for none of the above). Candidates:' + optsStr;
2005
+ } catch (e) {
2006
+ return '';
2007
+ }
2008
+ }
2009
+
1954
2010
  // \u2500\u2500\u2500 Rule Pre-Filter (embedding-based) \u2500\u2500\u2500
1955
2011
 
1956
2012
  /** User message + action for embedding search \u2014 intent surfaces boundary/consent rules the command alone misses. */
@@ -4524,6 +4580,11 @@ async function main() {
4524
4580
 
4525
4581
  if (rt === 'local') {
4526
4582
  // \u2500\u2500\u2500 Local grading: org rules ONLY (channel 1, port 18929) \u2500\u2500\u2500
4583
+ // CAP COUPLING: 4000 chars keeps the grade payload small enough to finish
4584
+ // well inside the timing budget (hook abort 45s < channel 120s < dispatch
4585
+ // 125s). Raising this materially grows per-grade inference time and, under
4586
+ // concurrency, pushes the latency tail toward those ceilings \u2014 so if you
4587
+ // raise it, revisit WORKER_DISPATCH_TIMEOUT_MS / the channel timeout first.
4527
4588
  const proposedShort = proposed.slice(0, 4000);
4528
4589
  // The TASK grade sees the DIFF for large files, so a contradiction or drift DEEP
4529
4590
  // in a big file is visible \u2014 the old "first 4KB of the whole file" window missed
@@ -4715,8 +4776,12 @@ async function main() {
4715
4776
  const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
4716
4777
  const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
4717
4778
 
4779
+ // fix mode: the grader still produces a fix, but the MODEL applies it (no
4780
+ // human poll) \u2014 surface our suggested fix so the agent picks it up rather
4781
+ // than re-deriving. ask mode: no fix in the deny text; the poll directive
4782
+ // (below) lets the HUMAN choose among candidates.
4718
4783
  const denyReason = mode === 'fix'
4719
- ? 'Guard: ' + guardReason + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
4784
+ ? 'Guard: ' + guardReason + (verdict.suggestedFix ? '\\nSuggested fix (apply this): ' + verdict.suggestedFix : '') + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
4720
4785
  : 'Guard: ' + guardReason + '\\nAsk the user for explicit consent before retrying.';
4721
4786
  dispatchCapture(jwt, 'edit', 'block', verdict.severity || 'critical', verdict.category || 'security',
4722
4787
  toolName, gitRepo, sessionId, config.captureDepth, {
@@ -4724,9 +4789,17 @@ async function main() {
4724
4789
  rulesChecked: relevantRules, violatedRules,
4725
4790
  ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
4726
4791
  });
4792
+ // Ask-mode only: turn this flag into a human-preference label (additive; '' on any failure).
4793
+ let pollDirective = '';
4794
+ if (mode !== 'fix' && verdict.suggestedFix) {
4795
+ pollDirective = await buildFixPollDirective({
4796
+ candA: verdict.suggestedFix, code: proposed, ruleId: verdict.ruleId,
4797
+ sessionId, filePath, model: captureModel, graderPrompt, graderPool,
4798
+ });
4799
+ }
4727
4800
  outputJson({
4728
4801
  systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason,
4729
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
4802
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason + pollDirective },
4730
4803
  });
4731
4804
  return;
4732
4805
  }
@@ -11538,7 +11611,7 @@ function writeConfigEnv(opts) {
11538
11611
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11539
11612
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11540
11613
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11541
- `SYNKRO_VERSION=${shellQuoteSingle("1.7.2")}`
11614
+ `SYNKRO_VERSION=${shellQuoteSingle("1.7.4")}`
11542
11615
  ];
11543
11616
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11544
11617
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15601,7 +15674,7 @@ var args = process.argv.slice(2);
15601
15674
  var cmd = args[0] || "";
15602
15675
  var subArgs = args.slice(1);
15603
15676
  function printVersion() {
15604
- console.log("1.7.2");
15677
+ console.log("1.7.4");
15605
15678
  }
15606
15679
  function printHelp2() {
15607
15680
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents