@synkro-sh/cli 1.6.94 → 1.6.96
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 +120 -23
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1781,7 +1781,21 @@ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeout
|
|
|
1781
1781
|
// break grading. The payload IS a ScanEnvelope; the container runs getActiveTask
|
|
1782
1782
|
// (resolved per (org,user,repo) via active_tasks) + taskDrift and returns the
|
|
1783
1783
|
// decision. Secrets are scrubbed before the envelope crosses the network.
|
|
1784
|
-
|
|
1784
|
+
// Cloud task-drift returns BOTH channels so the hook can STEER the agent, not just
|
|
1785
|
+
// narrate to the user: systemMessage \u2192 user terminal; additionalContext \u2192 the agent;
|
|
1786
|
+
// decision='deny' \u2192 a contradiction the agent must fix (hard block, matching local).
|
|
1787
|
+
// The container ships formatDecision()'s shape ({ systemMessage, hookSpecificOutput:
|
|
1788
|
+
// { additionalContext, permissionDecision } }); we surface all three.
|
|
1789
|
+
export interface TaskDriftResult { systemMessage: string; additionalContext: string; decision: 'allow' | 'deny' }
|
|
1790
|
+
const TASK_DRIFT_NONE: TaskDriftResult = { systemMessage: '', additionalContext: '', decision: 'allow' };
|
|
1791
|
+
|
|
1792
|
+
export async function cloudTaskDrift(envelope: Record<string, unknown>, jwt: string, timeoutMs = CLOUD_GRADE_TIMEOUT_MS): Promise<TaskDriftResult> {
|
|
1793
|
+
const sid = typeof envelope.sessionId === 'string' ? envelope.sessionId : undefined;
|
|
1794
|
+
const repo = typeof envelope.cwd === 'string' ? envelope.cwd : undefined;
|
|
1795
|
+
// What was being graded (the action), captured for the failure record so a dead call
|
|
1796
|
+
// still shows WHAT timed out. The fully-assembled grader prompt is captured separately
|
|
1797
|
+
// server-side (grader_failures.grade_input); this is the client's view.
|
|
1798
|
+
const gradeInput = scrubSecrets(JSON.stringify(envelope.payload ?? envelope)).slice(0, 6000);
|
|
1785
1799
|
try {
|
|
1786
1800
|
const body = JSON.stringify({ role: 'task-drift', payload: scrubSecrets(JSON.stringify(envelope)), content: '' });
|
|
1787
1801
|
const resp = await fetch(CONTAINERS_URL + '/grade/submit', {
|
|
@@ -1790,10 +1804,32 @@ export async function cloudTaskDrift(envelope: Record<string, unknown>, jwt: str
|
|
|
1790
1804
|
body,
|
|
1791
1805
|
signal: AbortSignal.timeout(timeoutMs),
|
|
1792
1806
|
});
|
|
1793
|
-
if (!resp.ok)
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1807
|
+
if (!resp.ok) {
|
|
1808
|
+
// Non-2xx \u2014 record it (rule grades log the same way) so the task message
|
|
1809
|
+
// silently not firing is observable, not invisible. Fire-and-forget per
|
|
1810
|
+
// logGraderUnavailable's contract; still fail-OPEN.
|
|
1811
|
+
void logGraderUnavailable('taskGuard', 'task-drift', 'http ' + resp.status, gradeInput, { sessionId: sid, repo }).catch(() => {});
|
|
1812
|
+
return TASK_DRIFT_NONE;
|
|
1813
|
+
}
|
|
1814
|
+
const j = await resp.json() as { systemMessage?: unknown; hookSpecificOutput?: { additionalContext?: unknown; permissionDecision?: unknown } };
|
|
1815
|
+
const hso = j?.hookSpecificOutput || {};
|
|
1816
|
+
return {
|
|
1817
|
+
systemMessage: typeof j?.systemMessage === 'string' ? j.systemMessage : '',
|
|
1818
|
+
additionalContext: typeof hso.additionalContext === 'string' ? hso.additionalContext : '',
|
|
1819
|
+
decision: hso.permissionDecision === 'deny' ? 'deny' : 'allow',
|
|
1820
|
+
};
|
|
1821
|
+
} catch (e) {
|
|
1822
|
+
// Track the failure \u2014 ESP. the 45s timeout, the most likely reason a task message
|
|
1823
|
+
// doesn't fire. AbortSignal.timeout throws TimeoutError on expiry; everything else
|
|
1824
|
+
// (network, parse) carries its own message. Fire-and-forget (don't await \u2014 would
|
|
1825
|
+
// add ~3s on the timeout path and blow the watchdog); fail-open: never block.
|
|
1826
|
+
const err = e as { name?: string; message?: string };
|
|
1827
|
+
const reason = err?.name === 'TimeoutError'
|
|
1828
|
+
? 'task-drift timed out after ' + timeoutMs + 'ms'
|
|
1829
|
+
: (err?.message || 'task-drift failed');
|
|
1830
|
+
void logGraderUnavailable('taskGuard', 'task-drift', reason, gradeInput, { sessionId: sid, repo }).catch(() => {});
|
|
1831
|
+
return TASK_DRIFT_NONE;
|
|
1832
|
+
}
|
|
1797
1833
|
}
|
|
1798
1834
|
|
|
1799
1835
|
// Option B: when a cloud grade fails (esp. a timeout), the REAL reason lives
|
|
@@ -3687,6 +3723,31 @@ export function countEditLineDelta(source: {
|
|
|
3687
3723
|
return { linesAdded, linesRemoved };
|
|
3688
3724
|
}
|
|
3689
3725
|
|
|
3726
|
+
// Build the recorded edit diff (code_change) for the audit trail, same format as
|
|
3727
|
+
// the routed hook's summarizeEdit: trim the common prefix/suffix between the file
|
|
3728
|
+
// before the edit and the proposed full content, then emit minus/plus prefixed,
|
|
3729
|
+
// line-numbered rows (the dashboard CodeChangeCard colors each row by its leading
|
|
3730
|
+
// sign). Without this the event detail falls back to the raw file/content dump
|
|
3731
|
+
// instead of an added/removed diff.
|
|
3732
|
+
export function summarizeEditDiff(baseContent: string, proposed: string): string {
|
|
3733
|
+
const EDIT_AUDIT_CAP = 16000;
|
|
3734
|
+
const a = baseContent ? baseContent.split('\\n') : [];
|
|
3735
|
+
const b = proposed ? proposed.split('\\n') : [];
|
|
3736
|
+
let start = 0;
|
|
3737
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
3738
|
+
let endA = a.length;
|
|
3739
|
+
let endB = b.length;
|
|
3740
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA--; endB--; }
|
|
3741
|
+
const removed = a.slice(start, endA);
|
|
3742
|
+
const added = b.slice(start, endB);
|
|
3743
|
+
const out: string[] = [];
|
|
3744
|
+
for (let i = 0; i < removed.length; i++) out.push('-' + String(start + 1 + i).padStart(5) + ' ' + removed[i]);
|
|
3745
|
+
for (let i = 0; i < added.length; i++) out.push('+' + String(start + 1 + i).padStart(5) + ' ' + added[i]);
|
|
3746
|
+
let diff = out.join('\\n');
|
|
3747
|
+
if (diff.length > EDIT_AUDIT_CAP) diff = diff.slice(0, EDIT_AUDIT_CAP) + '\\n... [truncated]';
|
|
3748
|
+
return diff;
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3690
3751
|
/** Normalize model names so the same Cursor session doesn't split across agent buckets. */
|
|
3691
3752
|
export function normalizeCaptureModel(model: string): string {
|
|
3692
3753
|
const m = (model || '').trim();
|
|
@@ -4291,7 +4352,7 @@ import {
|
|
|
4291
4352
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
4292
4353
|
appendSessionAction, readSessionLog, compressSessionLog, sessionLogForGrade, log,
|
|
4293
4354
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
|
|
4294
|
-
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
4355
|
+
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta, summarizeEditDiff,
|
|
4295
4356
|
captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4296
4357
|
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4297
4358
|
type HookConfig, type Rule,
|
|
@@ -4386,6 +4447,11 @@ async function main() {
|
|
|
4386
4447
|
try { fileBefore = readFileSync(fullPath, 'utf-8').slice(0, 65536); } catch {}
|
|
4387
4448
|
}
|
|
4388
4449
|
|
|
4450
|
+
// Audit diff (code_change): added/removed lines for the event detail's Code Change
|
|
4451
|
+
// card. Base = the file before this edit (Write \u2192 file on disk if any). Captured on
|
|
4452
|
+
// every edit verdict below so cloud-graded events show the diff, not a raw dump.
|
|
4453
|
+
const editDiff = summarizeEditDiff(toolName === 'Write' ? existingContent : fileBefore, proposed);
|
|
4454
|
+
|
|
4389
4455
|
// Extract transcript context
|
|
4390
4456
|
const transcript = extractTranscript(transcriptPath);
|
|
4391
4457
|
const lastPrompt = readLastPrompt(sessionId);
|
|
@@ -4424,6 +4490,13 @@ async function main() {
|
|
|
4424
4490
|
if (rt === 'local') {
|
|
4425
4491
|
// \u2500\u2500\u2500 Local grading: org rules ONLY (channel 1, port 18929) \u2500\u2500\u2500
|
|
4426
4492
|
const proposedShort = proposed.slice(0, 4000);
|
|
4493
|
+
// The TASK grade sees the DIFF for large files, so a contradiction or drift DEEP
|
|
4494
|
+
// in a big file is visible \u2014 the old "first 4KB of the whole file" window missed
|
|
4495
|
+
// any change past ~2KB. Small files keep full content (cleaner for the grader);
|
|
4496
|
+
// editDiff is the already-computed changed-lines summary (- old / + new).
|
|
4497
|
+
const taskGradeContent = (proposed.length <= 3500 || !editDiff)
|
|
4498
|
+
? proposedShort
|
|
4499
|
+
: ('Changed lines of ' + filePath + ' (- old / + new):\\n' + editDiff).slice(0, 4000);
|
|
4427
4500
|
const sessionLog = await sessionLogForGrade(sessionId, config.rules);
|
|
4428
4501
|
const graderContent = 'file=' + filePath + ' content=' + proposedShort;
|
|
4429
4502
|
const relevantRules = await filterRules(
|
|
@@ -4448,9 +4521,9 @@ async function main() {
|
|
|
4448
4521
|
// message into the pass output. Fail-silent ('' on any issue). Sent as a
|
|
4449
4522
|
// synthetic Write of the reconstructed content so the container needs no base
|
|
4450
4523
|
// bytes. cwd = the repo identifier the container keys active_tasks on.
|
|
4451
|
-
const taskDriftP
|
|
4452
|
-
? cloudTaskDrift({ payload: { tool_name: 'Write', tool_input: { file_path: filePath, content:
|
|
4453
|
-
: Promise.resolve('');
|
|
4524
|
+
const taskDriftP = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud'
|
|
4525
|
+
? cloudTaskDrift({ payload: { tool_name: 'Write', tool_input: { file_path: filePath, content: taskGradeContent } }, harness: agentKind, cwd: gitRepo, sessionId }, jwt)
|
|
4526
|
+
: Promise.resolve({ systemMessage: '', additionalContext: '', decision: 'allow' as const });
|
|
4454
4527
|
|
|
4455
4528
|
// \u2500\u2500\u2500 Combined org-rules + CWE in ONE inference (SYNKRO_COMBINED_EDIT_GRADE) \u2500\u2500\u2500
|
|
4456
4529
|
// Self-contained early-return branch \u2014 the default two-grade path below is
|
|
@@ -4518,6 +4591,15 @@ async function main() {
|
|
|
4518
4591
|
dispatchFinding(jwt, { session_id: sessionId, file_path: filePath, finding_type: 'cwe', finding_id: f.id, status: 'open' }, config.captureDepth);
|
|
4519
4592
|
}
|
|
4520
4593
|
|
|
4594
|
+
// Resolve the parallel task grade NOW so a CONTRADICTION folds into the rule/CWE
|
|
4595
|
+
// block too \u2014 the agent gets rules + CWE + task drift in ONE deny instead of
|
|
4596
|
+
// discovering the contradiction a retry later. taskDriftP has been running in
|
|
4597
|
+
// parallel, so this rarely adds wall-clock. Only a contradiction (decision==='deny')
|
|
4598
|
+
// is folded into a block; progress/off-spec messages stay on the pass path below.
|
|
4599
|
+
const cTask = await taskDriftP;
|
|
4600
|
+
const cTaskSys = cTask.systemMessage;
|
|
4601
|
+
const taskDeny = cTask.decision === 'deny' ? (cTask.additionalContext || cTaskSys) : '';
|
|
4602
|
+
|
|
4521
4603
|
// Org-rule violation \u2192 ask/fix enforcement (CWE findings appended to the deny).
|
|
4522
4604
|
if (!ruleVerdict.ok) {
|
|
4523
4605
|
const mode = normalizeMode(ruleVerdict.ruleMode || ruleMode(ruleVerdict.ruleId, config.rules));
|
|
@@ -4526,24 +4608,27 @@ async function main() {
|
|
|
4526
4608
|
? 'Guard: ' + guardReason + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
|
|
4527
4609
|
: 'Guard: ' + guardReason + '\\nAsk the user for explicit consent before retrying.';
|
|
4528
4610
|
if (cweBlock) denyReason += '\\nAlso resolve these CWE weaknesses in code:\\n' + cweBlock;
|
|
4611
|
+
if (taskDeny) denyReason += '\\nThis edit ALSO contradicts the active task \u2014 fix that too:\\n' + taskDeny;
|
|
4529
4612
|
dispatchCapture(jwt, 'edit', 'block', ruleVerdict.severity || 'critical', ruleVerdict.category || 'security',
|
|
4530
4613
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
4531
4614
|
command: editContent, reasoning: guardReason,
|
|
4532
4615
|
rulesChecked: relevantRules, violatedRules,
|
|
4533
|
-
ccModel: captureModel, ...lineMetrics,
|
|
4616
|
+
ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
|
|
4534
4617
|
});
|
|
4535
4618
|
outputJson({
|
|
4536
|
-
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason + (cweFindings.length ? ' (+' + cweFindings.length + ' CWE)' : ''),
|
|
4619
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason + (cweFindings.length ? ' (+' + cweFindings.length + ' CWE)' : '') + (taskDeny ? ' (+task)' : ''),
|
|
4537
4620
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
|
|
4538
4621
|
});
|
|
4539
4622
|
return;
|
|
4540
4623
|
}
|
|
4541
4624
|
|
|
4542
|
-
// Rules clean but CWE weakness(es) \u2192 fix-mode block (resolve in code
|
|
4625
|
+
// Rules clean but CWE weakness(es) \u2192 fix-mode block (resolve in code; task drift
|
|
4626
|
+
// folded in too if this edit also contradicts the active task).
|
|
4543
4627
|
if (cweFindings.length > 0) {
|
|
4544
|
-
|
|
4628
|
+
let ctx = 'CWE: ' + cweBlock + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
|
|
4629
|
+
if (taskDeny) ctx += '\\nThis edit ALSO contradicts the active task \u2014 fix that too:\\n' + taskDeny;
|
|
4545
4630
|
outputJson({
|
|
4546
|
-
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked (' + cweFindings.map(f => f.id).join(', ') + ')',
|
|
4631
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked (' + cweFindings.map(f => f.id).join(', ') + ')' + (taskDeny ? ' (+task)' : ''),
|
|
4547
4632
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
4548
4633
|
});
|
|
4549
4634
|
return;
|
|
@@ -4554,11 +4639,18 @@ async function main() {
|
|
|
4554
4639
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
4555
4640
|
command: editContent, reasoning: ruleVerdict.reason || 'no violations (rules + CWE)',
|
|
4556
4641
|
rulesChecked: relevantRules, violatedRules: [],
|
|
4557
|
-
ccModel: captureModel, ...lineMetrics,
|
|
4642
|
+
ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
|
|
4558
4643
|
});
|
|
4559
4644
|
const cPassLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (ruleVerdict.reason || 'clean (rules + CWE)');
|
|
4560
|
-
|
|
4561
|
-
|
|
4645
|
+
if (cTask.decision === 'deny') {
|
|
4646
|
+
// Cloud parity with local: a task CONTRADICTION hard-blocks the edit and
|
|
4647
|
+
// tells the AGENT (additionalContext) to fix it before proceeding.
|
|
4648
|
+
outputJson({ systemMessage: cPassLine + (cTaskSys ? '\\n' + cTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: cTask.additionalContext || cTaskSys, additionalContext: cTask.additionalContext || cTaskSys } });
|
|
4649
|
+
} else {
|
|
4650
|
+
// On-track / off-spec: pass, but pipe any task additionalContext to the AGENT
|
|
4651
|
+
// (not just systemMessage to the user) so drift/regression actually steers.
|
|
4652
|
+
outputJson({ systemMessage: cPassLine + (cTaskSys ? '\\n' + cTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro combined edit judge (rules + CWE). ' + (ruleVerdict.reason || 'clean') + (cTask.additionalContext ? '\\n[task] ' + cTask.additionalContext : '') } });
|
|
4653
|
+
}
|
|
4562
4654
|
return;
|
|
4563
4655
|
}
|
|
4564
4656
|
|
|
@@ -4587,7 +4679,7 @@ async function main() {
|
|
|
4587
4679
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
4588
4680
|
command: editContent, reasoning: guardReason,
|
|
4589
4681
|
rulesChecked: relevantRules, violatedRules,
|
|
4590
|
-
ccModel: captureModel, ...lineMetrics,
|
|
4682
|
+
ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
|
|
4591
4683
|
});
|
|
4592
4684
|
outputJson({
|
|
4593
4685
|
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason,
|
|
@@ -4601,11 +4693,16 @@ async function main() {
|
|
|
4601
4693
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
4602
4694
|
command: editContent, reasoning: verdict.reason || 'no policy violations detected',
|
|
4603
4695
|
rulesChecked: relevantRules, violatedRules: [],
|
|
4604
|
-
ccModel: captureModel, ...lineMetrics,
|
|
4696
|
+
ccModel: captureModel, codeChange: editDiff, ...lineMetrics,
|
|
4605
4697
|
});
|
|
4606
4698
|
const passLine = tagStr + ' editGuard ' + fileShort + ' \u2192 pass: ' + (verdict.reason || 'no policy violations detected');
|
|
4607
|
-
const
|
|
4608
|
-
|
|
4699
|
+
const rTask = await taskDriftP;
|
|
4700
|
+
const rTaskSys = rTask.systemMessage;
|
|
4701
|
+
if (rTask.decision === 'deny') {
|
|
4702
|
+
outputJson({ systemMessage: passLine + (rTaskSys ? '\\n' + rTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: rTask.additionalContext || rTaskSys, additionalContext: rTask.additionalContext || rTaskSys } });
|
|
4703
|
+
} else {
|
|
4704
|
+
outputJson({ systemMessage: passLine + (rTaskSys ? '\\n' + rTaskSys : ''), hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge. ' + (verdict.reason || 'no policy violations detected') + (rTask.additionalContext ? '\\n[task] ' + rTask.additionalContext : '') } });
|
|
4705
|
+
}
|
|
4609
4706
|
return;
|
|
4610
4707
|
}
|
|
4611
4708
|
|
|
@@ -11269,7 +11366,7 @@ function writeConfigEnv(opts) {
|
|
|
11269
11366
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
11270
11367
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
11271
11368
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
11272
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
11369
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.96")}`
|
|
11273
11370
|
];
|
|
11274
11371
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
11275
11372
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -15334,7 +15431,7 @@ var args = process.argv.slice(2);
|
|
|
15334
15431
|
var cmd = args[0] || "";
|
|
15335
15432
|
var subArgs = args.slice(1);
|
|
15336
15433
|
function printVersion() {
|
|
15337
|
-
console.log("1.6.
|
|
15434
|
+
console.log("1.6.96");
|
|
15338
15435
|
}
|
|
15339
15436
|
function printHelp2() {
|
|
15340
15437
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|