@synkro-sh/cli 1.7.1 → 1.7.3

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
@@ -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. */
@@ -1966,7 +2022,7 @@ export async function filterRules(commandText: string, allRules: Rule[]): Promis
1966
2022
  if (allRules.length <= 3) return allRules;
1967
2023
 
1968
2024
  // Cloud: the embedding index lives server-side (the container grade path can't
1969
- // reach 127.0.0.1), so ask the gateway for the top-3 relevant rules \u2014 same
2025
+ // reach 127.0.0.1), so ask the gateway for the top-5 relevant rules \u2014 same
1970
2026
  // CF/BYOK bge-base vectors, same behavior as local. Without this, cloud grades
1971
2027
  // dumped EVERY rule into the prompt (9KB+), the dominant cause of grade
1972
2028
  // timeouts. Any failure falls back to all rules (status quo), never blocks.
@@ -1977,7 +2033,7 @@ export async function filterRules(commandText: string, allRules: Rule[]): Promis
1977
2033
  const resp = await fetch(GATEWAY_URL + '/api/v1/hook/filter-rules', {
1978
2034
  method: 'POST',
1979
2035
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1980
- body: JSON.stringify({ text: commandText, top_k: 3 }),
2036
+ body: JSON.stringify({ text: commandText, top_k: 5 }),
1981
2037
  signal: AbortSignal.timeout(2000),
1982
2038
  });
1983
2039
  if (!resp.ok) return allRules;
@@ -1996,7 +2052,7 @@ export async function filterRules(commandText: string, allRules: Rule[]): Promis
1996
2052
  const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/local/filter-rules', {
1997
2053
  method: 'POST',
1998
2054
  headers: { 'Content-Type': 'application/json' },
1999
- body: JSON.stringify({ text: commandText, top_k: 3 }),
2055
+ body: JSON.stringify({ text: commandText, top_k: 5 }),
2000
2056
  signal: AbortSignal.timeout(500),
2001
2057
  });
2002
2058
  if (!resp.ok) return allRules;
@@ -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.1")}`
11614
+ `SYNKRO_VERSION=${shellQuoteSingle("1.7.3")}`
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.1");
15677
+ console.log("1.7.3");
15605
15678
  }
15606
15679
  function printHelp2() {
15607
15680
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents