@synkro-sh/cli 1.4.72 → 1.4.74
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 +385 -41
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -536,21 +536,15 @@ function installMcpConfig(opts) {
|
|
|
536
536
|
if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
|
|
537
537
|
}
|
|
538
538
|
if (opts.local) {
|
|
539
|
-
const
|
|
540
|
-
const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
|
|
541
|
-
let localToken = "";
|
|
542
|
-
try {
|
|
543
|
-
localToken = readFileSync3(tokenPath, "utf-8").trim();
|
|
544
|
-
} catch {
|
|
545
|
-
}
|
|
539
|
+
const proxyScript = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
|
|
546
540
|
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
547
|
-
type: "
|
|
548
|
-
|
|
549
|
-
|
|
541
|
+
type: "stdio",
|
|
542
|
+
command: "bun",
|
|
543
|
+
args: ["run", proxyScript],
|
|
550
544
|
[SYNKRO_MARKER3]: true
|
|
551
545
|
};
|
|
552
546
|
writeClaudeJsonAtomic(config);
|
|
553
|
-
return { path: CC_CONFIG_PATH, url:
|
|
547
|
+
return { path: CC_CONFIG_PATH, url: `stdio://${proxyScript}` };
|
|
554
548
|
}
|
|
555
549
|
const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
|
|
556
550
|
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
@@ -612,15 +606,15 @@ function installCursorMcpConfig(opts) {
|
|
|
612
606
|
}
|
|
613
607
|
if (opts.local) {
|
|
614
608
|
const url2 = "http://127.0.0.1:8931/";
|
|
615
|
-
const
|
|
616
|
-
let
|
|
609
|
+
const jwtPath = join2(homedir3(), ".synkro", ".mcp-jwt");
|
|
610
|
+
let jwt2 = "";
|
|
617
611
|
try {
|
|
618
|
-
|
|
612
|
+
jwt2 = readFileSync3(jwtPath, "utf-8").trim();
|
|
619
613
|
} catch {
|
|
620
614
|
}
|
|
621
615
|
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
622
616
|
url: url2,
|
|
623
|
-
...
|
|
617
|
+
...jwt2 ? { headers: { Authorization: `Bearer ${jwt2}` } } : {},
|
|
624
618
|
[SYNKRO_MARKER3]: true
|
|
625
619
|
};
|
|
626
620
|
writeCursorMcpJsonAtomic(config);
|
|
@@ -6267,6 +6261,7 @@ function writeHookScripts() {
|
|
|
6267
6261
|
const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
6268
6262
|
const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
6269
6263
|
const mcpLocalServerPath = join11(HOOKS_DIR, "mcp-local-server.ts");
|
|
6264
|
+
const mcpStdioProxyPath = join11(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
6270
6265
|
writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
6271
6266
|
writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
6272
6267
|
writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
@@ -6286,32 +6281,33 @@ function writeHookScripts() {
|
|
|
6286
6281
|
/**
|
|
6287
6282
|
* Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
|
|
6288
6283
|
* JSON-RPC 2.0 over HTTP, same protocol as the cloud MCP server.
|
|
6289
|
-
*
|
|
6284
|
+
* Bearer token auth (file-based shared secret), localhost only, no embedding API, no Inngest.
|
|
6290
6285
|
*/
|
|
6291
6286
|
import { existsSync, readFileSync, writeFileSync, renameSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
6292
6287
|
import { homedir } from 'node:os';
|
|
6293
6288
|
import { join } from 'node:path';
|
|
6294
6289
|
|
|
6295
|
-
import { randomBytes } from 'node:crypto';
|
|
6296
|
-
|
|
6297
6290
|
const PORT = parseInt(process.env.SYNKRO_MCP_PORT || '8931', 10);
|
|
6298
6291
|
const HOME = homedir();
|
|
6299
6292
|
const RULES_PATH = join(HOME, '.synkro', 'rules.json');
|
|
6300
6293
|
const TELEMETRY_PATH = join(HOME, '.synkro', 'telemetry.jsonl');
|
|
6301
|
-
const
|
|
6294
|
+
const JWT_TOKEN_PATH = join(HOME, '.synkro', '.mcp-jwt');
|
|
6302
6295
|
|
|
6303
|
-
//
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
mkdirSync(join(HOME, '.synkro'), { recursive: true });
|
|
6310
|
-
writeFileSync(TOKEN_PATH, token + '\\n', { mode: 0o600 });
|
|
6311
|
-
return token;
|
|
6312
|
-
}
|
|
6296
|
+
// Synkro-signed long-lived JWT \u2014 minted during \`synkro install\`, required on all POST requests.
|
|
6297
|
+
// If missing, the server still starts (for GET health checks) but rejects all tool calls.
|
|
6298
|
+
let SERVER_TOKEN = '';
|
|
6299
|
+
try { SERVER_TOKEN = readFileSync(JWT_TOKEN_PATH, 'utf-8').trim(); } catch {}
|
|
6300
|
+
if (!SERVER_TOKEN) console.warn('[synkro] \u26A0 No MCP JWT found \u2014 run \`synkro install\` to authenticate.');
|
|
6301
|
+
const MAX_BODY_BYTES = 1_048_576;
|
|
6313
6302
|
|
|
6314
|
-
|
|
6303
|
+
let _writeLock: Promise<void> = Promise.resolve();
|
|
6304
|
+
function serialized<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
6305
|
+
let release: () => void;
|
|
6306
|
+
const next = new Promise<void>(r => { release = r; });
|
|
6307
|
+
const prev = _writeLock;
|
|
6308
|
+
_writeLock = next;
|
|
6309
|
+
return prev.then(() => fn()).finally(() => release!());
|
|
6310
|
+
}
|
|
6315
6311
|
|
|
6316
6312
|
// \u2500\u2500\u2500 Storage \u2500\u2500\u2500
|
|
6317
6313
|
|
|
@@ -6698,6 +6694,219 @@ function handleListExemptions(): any {
|
|
|
6698
6694
|
return { exemptions: data.scanExemptions, total: data.scanExemptions.length };
|
|
6699
6695
|
}
|
|
6700
6696
|
|
|
6697
|
+
// \u2500\u2500\u2500 Findings \u2500\u2500\u2500
|
|
6698
|
+
|
|
6699
|
+
const CONFIG_PATH = join(HOME, '.synkro', 'config.json');
|
|
6700
|
+
const JWT_PATH = join(HOME, '.synkro', '.jwt');
|
|
6701
|
+
|
|
6702
|
+
interface Finding {
|
|
6703
|
+
id: string;
|
|
6704
|
+
session_id: string;
|
|
6705
|
+
file_path: string;
|
|
6706
|
+
finding_type: string;
|
|
6707
|
+
finding_id: string;
|
|
6708
|
+
severity: string;
|
|
6709
|
+
status: string;
|
|
6710
|
+
detail?: string;
|
|
6711
|
+
package_name?: string;
|
|
6712
|
+
package_version?: string;
|
|
6713
|
+
fixed_version?: string;
|
|
6714
|
+
created_at: string;
|
|
6715
|
+
resolved_at?: string;
|
|
6716
|
+
}
|
|
6717
|
+
|
|
6718
|
+
function getCloudConfig(): { apiUrl: string; jwt: string } | null {
|
|
6719
|
+
try {
|
|
6720
|
+
const jwt = readFileSync(JWT_PATH, 'utf-8').trim();
|
|
6721
|
+
if (!jwt) return null;
|
|
6722
|
+
let apiUrl = process.env.SYNKRO_API_URL || '';
|
|
6723
|
+
if (!apiUrl) {
|
|
6724
|
+
try {
|
|
6725
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
6726
|
+
apiUrl = cfg.api_url || cfg.apiUrl || '';
|
|
6727
|
+
} catch {}
|
|
6728
|
+
}
|
|
6729
|
+
if (!apiUrl) apiUrl = 'https://api.synkro.sh';
|
|
6730
|
+
return { apiUrl, jwt };
|
|
6731
|
+
} catch {
|
|
6732
|
+
return null;
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
|
|
6736
|
+
function readLocalFindings(): Finding[] {
|
|
6737
|
+
if (!existsSync(TELEMETRY_PATH)) return [];
|
|
6738
|
+
let lines: string[];
|
|
6739
|
+
try {
|
|
6740
|
+
lines = readFileSync(TELEMETRY_PATH, 'utf-8').split('\\n').filter(Boolean);
|
|
6741
|
+
} catch { return []; }
|
|
6742
|
+
const findingMap = new Map<string, Finding>();
|
|
6743
|
+
|
|
6744
|
+
for (const line of lines) {
|
|
6745
|
+
try {
|
|
6746
|
+
const event = JSON.parse(line);
|
|
6747
|
+
if (event.capture_type !== 'scan_finding') continue;
|
|
6748
|
+
const key = \`\${event.file_path}:\${event.finding_id}\`;
|
|
6749
|
+
const existing = findingMap.get(key);
|
|
6750
|
+
const ts = event._ts || event.created_at || '';
|
|
6751
|
+
|
|
6752
|
+
findingMap.set(key, {
|
|
6753
|
+
id: event.id || existing?.id || \`sf_\${event.session_id}_\${event.finding_id}_\${Date.now()}\`,
|
|
6754
|
+
session_id: event.session_id || existing?.session_id || '',
|
|
6755
|
+
file_path: event.file_path,
|
|
6756
|
+
finding_type: event.finding_type,
|
|
6757
|
+
finding_id: event.finding_id,
|
|
6758
|
+
severity: event.severity || 'unknown',
|
|
6759
|
+
status: event.status || 'open',
|
|
6760
|
+
detail: event.detail || existing?.detail,
|
|
6761
|
+
package_name: event.package_name || existing?.package_name,
|
|
6762
|
+
package_version: event.package_version || existing?.package_version,
|
|
6763
|
+
fixed_version: event.fixed_version || existing?.fixed_version,
|
|
6764
|
+
created_at: existing?.created_at || ts,
|
|
6765
|
+
resolved_at: event.status === 'resolved' ? ts : existing?.resolved_at,
|
|
6766
|
+
});
|
|
6767
|
+
} catch {}
|
|
6768
|
+
}
|
|
6769
|
+
|
|
6770
|
+
return Array.from(findingMap.values());
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
async function fetchCloudFindings(params: Record<string, string>): Promise<Finding[] | null> {
|
|
6774
|
+
const cloud = getCloudConfig();
|
|
6775
|
+
if (!cloud) return null;
|
|
6776
|
+
try {
|
|
6777
|
+
const qs = new URLSearchParams(params).toString();
|
|
6778
|
+
const resp = await fetch(\`\${cloud.apiUrl}/api/v1/scan-findings?\${qs}\`, {
|
|
6779
|
+
headers: { Authorization: \`Bearer \${cloud.jwt}\` },
|
|
6780
|
+
signal: AbortSignal.timeout(5000),
|
|
6781
|
+
});
|
|
6782
|
+
if (!resp.ok) return null;
|
|
6783
|
+
const data = await resp.json() as any;
|
|
6784
|
+
return data.findings || data.data || [];
|
|
6785
|
+
} catch {
|
|
6786
|
+
return null;
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
function dispatchResolution(finding: Finding): void {
|
|
6791
|
+
const cloud = getCloudConfig();
|
|
6792
|
+
if (!cloud) return;
|
|
6793
|
+
fetch(\`\${cloud.apiUrl}/api/v1/hook/finding\`, {
|
|
6794
|
+
method: 'POST',
|
|
6795
|
+
headers: { Authorization: \`Bearer \${cloud.jwt}\`, 'Content-Type': 'application/json' },
|
|
6796
|
+
body: JSON.stringify({
|
|
6797
|
+
session_id: finding.session_id,
|
|
6798
|
+
file_path: finding.file_path,
|
|
6799
|
+
finding_type: finding.finding_type,
|
|
6800
|
+
finding_id: finding.finding_id,
|
|
6801
|
+
severity: finding.severity,
|
|
6802
|
+
status: 'resolved',
|
|
6803
|
+
}),
|
|
6804
|
+
signal: AbortSignal.timeout(3000),
|
|
6805
|
+
}).catch(() => {});
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
async function handleListFindings(args: any): Promise<any> {
|
|
6809
|
+
const cloudParams: Record<string, string> = {};
|
|
6810
|
+
if (args.status) cloudParams.status = args.status;
|
|
6811
|
+
if (args.finding_type) cloudParams.finding_type = args.finding_type;
|
|
6812
|
+
if (args.severity) cloudParams.severity = args.severity;
|
|
6813
|
+
if (args.file_path) cloudParams.file_path = args.file_path;
|
|
6814
|
+
if (args.limit) cloudParams.limit = String(args.limit);
|
|
6815
|
+
|
|
6816
|
+
const cloudFindings = await fetchCloudFindings(cloudParams);
|
|
6817
|
+
|
|
6818
|
+
let findings: Finding[];
|
|
6819
|
+
let source: string;
|
|
6820
|
+
if (cloudFindings) {
|
|
6821
|
+
findings = cloudFindings;
|
|
6822
|
+
source = 'cloud';
|
|
6823
|
+
} else {
|
|
6824
|
+
findings = readLocalFindings();
|
|
6825
|
+
source = 'local';
|
|
6826
|
+
if (args.status) findings = findings.filter(f => f.status === args.status);
|
|
6827
|
+
if (args.finding_type) findings = findings.filter(f => f.finding_type === args.finding_type);
|
|
6828
|
+
if (args.severity) findings = findings.filter(f => f.severity === args.severity);
|
|
6829
|
+
if (args.file_path) findings = findings.filter(f => f.file_path.includes(args.file_path));
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
findings.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
|
|
6833
|
+
const limit = Math.min(args.limit || 25, 50);
|
|
6834
|
+
return {
|
|
6835
|
+
source,
|
|
6836
|
+
findings: findings.slice(0, limit).map(f => ({
|
|
6837
|
+
id: f.id,
|
|
6838
|
+
finding_type: f.finding_type,
|
|
6839
|
+
finding_id: f.finding_id,
|
|
6840
|
+
file_path: f.file_path,
|
|
6841
|
+
severity: f.severity,
|
|
6842
|
+
status: f.status,
|
|
6843
|
+
package_name: f.package_name,
|
|
6844
|
+
package_version: f.package_version,
|
|
6845
|
+
created_at: f.created_at,
|
|
6846
|
+
})),
|
|
6847
|
+
total: findings.length,
|
|
6848
|
+
open: findings.filter(f => f.status === 'open').length,
|
|
6849
|
+
resolved: findings.filter(f => f.status === 'resolved').length,
|
|
6850
|
+
};
|
|
6851
|
+
}
|
|
6852
|
+
|
|
6853
|
+
async function handleGetFindingDetail(args: any): Promise<any> {
|
|
6854
|
+
const findings = readLocalFindings();
|
|
6855
|
+
const match = findings.find(f => {
|
|
6856
|
+
if (args.id && f.id === args.id) return true;
|
|
6857
|
+
if (args.file_path && args.finding_id) {
|
|
6858
|
+
return f.file_path.includes(args.file_path) && f.finding_id.toUpperCase() === (args.finding_id || '').toUpperCase();
|
|
6859
|
+
}
|
|
6860
|
+
return false;
|
|
6861
|
+
});
|
|
6862
|
+
if (!match) return { found: false, error: 'Finding not found' };
|
|
6863
|
+
return { found: true, ...match };
|
|
6864
|
+
}
|
|
6865
|
+
|
|
6866
|
+
async function handleResolveFinding(args: any): Promise<any> {
|
|
6867
|
+
const findings = readLocalFindings();
|
|
6868
|
+
const toResolve = findings.filter(f => {
|
|
6869
|
+
if (f.status !== 'open') return false;
|
|
6870
|
+
if (args.id && f.id === args.id) return true;
|
|
6871
|
+
if (args.file_path && args.finding_id) {
|
|
6872
|
+
return f.file_path.includes(args.file_path) && f.finding_id.toUpperCase() === (args.finding_id || '').toUpperCase();
|
|
6873
|
+
}
|
|
6874
|
+
if (args.file_path) return f.file_path.includes(args.file_path);
|
|
6875
|
+
return false;
|
|
6876
|
+
});
|
|
6877
|
+
|
|
6878
|
+
if (toResolve.length === 0) return { resolved: 0, error: 'No matching open findings' };
|
|
6879
|
+
|
|
6880
|
+
const now = new Date().toISOString();
|
|
6881
|
+
for (const f of toResolve) {
|
|
6882
|
+
const event = {
|
|
6883
|
+
capture_type: 'scan_finding',
|
|
6884
|
+
id: f.id,
|
|
6885
|
+
session_id: f.session_id,
|
|
6886
|
+
file_path: f.file_path,
|
|
6887
|
+
finding_type: f.finding_type,
|
|
6888
|
+
finding_id: f.finding_id,
|
|
6889
|
+
severity: f.severity,
|
|
6890
|
+
status: 'resolved',
|
|
6891
|
+
detail: f.detail,
|
|
6892
|
+
package_name: f.package_name,
|
|
6893
|
+
package_version: f.package_version,
|
|
6894
|
+
fixed_version: f.fixed_version,
|
|
6895
|
+
resolved_at: now,
|
|
6896
|
+
_ts: now,
|
|
6897
|
+
};
|
|
6898
|
+
try {
|
|
6899
|
+
appendFileSync(TELEMETRY_PATH, JSON.stringify(event) + '\\n', 'utf-8');
|
|
6900
|
+
} catch {}
|
|
6901
|
+
dispatchResolution(f);
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6904
|
+
return {
|
|
6905
|
+
resolved: toResolve.length,
|
|
6906
|
+
findings: toResolve.map(f => ({ finding_id: f.finding_id, file_path: f.file_path })),
|
|
6907
|
+
};
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6701
6910
|
// \u2500\u2500\u2500 Tool Descriptors \u2500\u2500\u2500
|
|
6702
6911
|
|
|
6703
6912
|
const TOOL_DESCRIPTORS = [
|
|
@@ -6861,6 +7070,47 @@ const TOOL_DESCRIPTORS = [
|
|
|
6861
7070
|
description: "List all scan exemptions.",
|
|
6862
7071
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
6863
7072
|
},
|
|
7073
|
+
{
|
|
7074
|
+
name: 'list_findings',
|
|
7075
|
+
description: "List CWE/CVE scan findings. Shows security issues found by Synkro hooks. Use to review what needs fixing.",
|
|
7076
|
+
inputSchema: {
|
|
7077
|
+
type: 'object',
|
|
7078
|
+
properties: {
|
|
7079
|
+
status: { type: 'string', enum: ['open', 'resolved', 'exempted'], description: 'Filter by status (default: all).' },
|
|
7080
|
+
finding_type: { type: 'string', enum: ['cwe', 'cve'], description: 'Filter by finding type.' },
|
|
7081
|
+
severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
|
|
7082
|
+
file_path: { type: 'string', description: 'Filter by file path substring.' },
|
|
7083
|
+
limit: { type: 'integer', default: 25, description: 'Max results (default 25, max 50).' },
|
|
7084
|
+
},
|
|
7085
|
+
required: [],
|
|
7086
|
+
},
|
|
7087
|
+
},
|
|
7088
|
+
{
|
|
7089
|
+
name: 'get_finding_detail',
|
|
7090
|
+
description: "Get full detail of a specific finding including remediation context.",
|
|
7091
|
+
inputSchema: {
|
|
7092
|
+
type: 'object',
|
|
7093
|
+
properties: {
|
|
7094
|
+
id: { type: 'string', description: 'Finding ID (e.g. sf_...).' },
|
|
7095
|
+
file_path: { type: 'string', description: 'File path (used with finding_id).' },
|
|
7096
|
+
finding_id: { type: 'string', description: 'CWE/CVE ID like CWE-89 or CVE-2024-1234.' },
|
|
7097
|
+
},
|
|
7098
|
+
required: [],
|
|
7099
|
+
},
|
|
7100
|
+
},
|
|
7101
|
+
{
|
|
7102
|
+
name: 'resolve_finding',
|
|
7103
|
+
description: "Mark finding(s) as resolved after the underlying issue is fixed. Can target by ID, file+finding_id, or all findings for a file.",
|
|
7104
|
+
inputSchema: {
|
|
7105
|
+
type: 'object',
|
|
7106
|
+
properties: {
|
|
7107
|
+
id: { type: 'string', description: 'Specific finding ID to resolve.' },
|
|
7108
|
+
file_path: { type: 'string', description: 'Resolve all open findings matching this file path.' },
|
|
7109
|
+
finding_id: { type: 'string', description: 'CWE/CVE ID (used with file_path for targeted resolution).' },
|
|
7110
|
+
},
|
|
7111
|
+
required: [],
|
|
7112
|
+
},
|
|
7113
|
+
},
|
|
6864
7114
|
];
|
|
6865
7115
|
|
|
6866
7116
|
const MCP_INSTRUCTIONS =
|
|
@@ -6871,7 +7121,12 @@ const MCP_INSTRUCTIONS =
|
|
|
6871
7121
|
"'we need a rule about\u2026' \u2014 route to THIS server's tools.\\n\\n" +
|
|
6872
7122
|
"TOOL ROUTING:\\n" +
|
|
6873
7123
|
" \u2022 get_guardrails(query) \u2014 keyword search. Use to check if a rule exists.\\n" +
|
|
6874
|
-
" \u2022 list_guardrails \u2014 full enumeration. Use for listings.\\n
|
|
7124
|
+
" \u2022 list_guardrails \u2014 full enumeration. Use for listings.\\n" +
|
|
7125
|
+
" \u2022 list_findings \u2014 show CWE/CVE scan findings (open, resolved, all).\\n" +
|
|
7126
|
+
" \u2022 get_finding_detail \u2014 get full detail + remediation context for a finding.\\n" +
|
|
7127
|
+
" \u2022 resolve_finding \u2014 mark findings resolved after fixing the code.\\n\\n" +
|
|
7128
|
+
"When the user asks about security issues, vulnerabilities, or scan results, " +
|
|
7129
|
+
"use list_findings first. After fixing code, call resolve_finding to update status.\\n\\n" +
|
|
6875
7130
|
"Do NOT use Claude Code's \`update-config\` skill for these requests.\\n\\n" +
|
|
6876
7131
|
"Rules are stored locally in ~/.synkro/rules.json and enforced by hooks.";
|
|
6877
7132
|
|
|
@@ -6924,6 +7179,9 @@ async function handleRpc(body: any): Promise<any> {
|
|
|
6924
7179
|
case 'exempt_path': result = handleExemptPath(args); break;
|
|
6925
7180
|
case 'remove_exemption': result = handleRemoveExemption(args); break;
|
|
6926
7181
|
case 'list_exemptions': result = handleListExemptions(); break;
|
|
7182
|
+
case 'list_findings': result = await handleListFindings(args); break;
|
|
7183
|
+
case 'get_finding_detail': result = await handleGetFindingDetail(args); break;
|
|
7184
|
+
case 'resolve_finding': result = await handleResolveFinding(args); break;
|
|
6927
7185
|
default: return jsonRpcError(id, -32601, \`Unknown tool: \${toolName}\`);
|
|
6928
7186
|
}
|
|
6929
7187
|
return jsonRpcOk(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
@@ -7063,14 +7321,25 @@ const server = Bun.serve({
|
|
|
7063
7321
|
}
|
|
7064
7322
|
|
|
7065
7323
|
if (req.method === 'POST') {
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7324
|
+
const authHeader = req.headers.get('authorization') || '';
|
|
7325
|
+
const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
|
|
7326
|
+
if (bearer !== SERVER_TOKEN) {
|
|
7327
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401, headers: cors });
|
|
7328
|
+
}
|
|
7329
|
+
const raw = await req.arrayBuffer();
|
|
7330
|
+
if (raw.byteLength > MAX_BODY_BYTES) {
|
|
7331
|
+
return Response.json(jsonRpcError(null, -32600, 'Request too large'), { status: 413, headers: cors });
|
|
7073
7332
|
}
|
|
7333
|
+
return serialized(async () => {
|
|
7334
|
+
try {
|
|
7335
|
+
const body = JSON.parse(new TextDecoder().decode(raw));
|
|
7336
|
+
const result = await handleRpc(body);
|
|
7337
|
+
if (result === null) return new Response('', { status: 204, headers: cors });
|
|
7338
|
+
return Response.json(result, { headers: cors });
|
|
7339
|
+
} catch {
|
|
7340
|
+
return Response.json(jsonRpcError(null, -32700, 'Parse error'), { status: 400, headers: cors });
|
|
7341
|
+
}
|
|
7342
|
+
});
|
|
7074
7343
|
}
|
|
7075
7344
|
|
|
7076
7345
|
if (req.method === 'OPTIONS') {
|
|
@@ -7083,6 +7352,7 @@ const server = Bun.serve({
|
|
|
7083
7352
|
|
|
7084
7353
|
console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1:\${server.port}\`);
|
|
7085
7354
|
`, "utf-8");
|
|
7355
|
+
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
7086
7356
|
chmodSync2(bashScriptPath, 493);
|
|
7087
7357
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
7088
7358
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -7099,6 +7369,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
|
|
|
7099
7369
|
chmodSync2(cursorBashJudgePath, 493);
|
|
7100
7370
|
chmodSync2(cursorEditCapturePath, 493);
|
|
7101
7371
|
chmodSync2(mcpLocalServerPath, 493);
|
|
7372
|
+
chmodSync2(mcpStdioProxyPath, 493);
|
|
7102
7373
|
return {
|
|
7103
7374
|
bashScript: bashScriptPath,
|
|
7104
7375
|
bashFollowupScript: bashFollowupScriptPath,
|
|
@@ -7145,7 +7416,7 @@ function writeConfigEnv(opts) {
|
|
|
7145
7416
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
7146
7417
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
7147
7418
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
7148
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
7419
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.74")}`
|
|
7149
7420
|
];
|
|
7150
7421
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
7151
7422
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7628,7 +7899,20 @@ async function installCommand(opts = {}) {
|
|
|
7628
7899
|
if (hasClaudeCode && !opts.noMcp) {
|
|
7629
7900
|
if (useLocalMcp) {
|
|
7630
7901
|
try {
|
|
7631
|
-
const
|
|
7902
|
+
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
7903
|
+
method: "POST",
|
|
7904
|
+
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
7905
|
+
body: "{}"
|
|
7906
|
+
});
|
|
7907
|
+
let mcpJwt = "";
|
|
7908
|
+
if (mintResp.ok) {
|
|
7909
|
+
const minted = await mintResp.json();
|
|
7910
|
+
mcpJwt = minted.token;
|
|
7911
|
+
writeFileSync7(join11(SYNKRO_DIR2, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
|
|
7912
|
+
} else {
|
|
7913
|
+
console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
|
|
7914
|
+
}
|
|
7915
|
+
const mcp = installMcpConfig({ gatewayUrl, bearerToken: mcpJwt, local: true });
|
|
7632
7916
|
console.log(`Registered local MCP guardrails server in ${mcp.path}`);
|
|
7633
7917
|
console.log(` url: ${mcp.url}`);
|
|
7634
7918
|
console.log(" (rules stored in ~/.synkro/rules.json)");
|
|
@@ -7655,6 +7939,7 @@ async function installCommand(opts = {}) {
|
|
|
7655
7939
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
7656
7940
|
}
|
|
7657
7941
|
const minted = await mintResp.json();
|
|
7942
|
+
writeFileSync7(join11(SYNKRO_DIR2, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
7658
7943
|
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
7659
7944
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
7660
7945
|
console.log(` url: ${mcp.url}`);
|
|
@@ -7671,6 +7956,18 @@ async function installCommand(opts = {}) {
|
|
|
7671
7956
|
if (hasCursor && !opts.noMcp) {
|
|
7672
7957
|
try {
|
|
7673
7958
|
if (useLocalMcp) {
|
|
7959
|
+
const jwtPath = join11(SYNKRO_DIR2, ".mcp-jwt");
|
|
7960
|
+
if (!existsSync10(jwtPath)) {
|
|
7961
|
+
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
7962
|
+
method: "POST",
|
|
7963
|
+
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
7964
|
+
body: "{}"
|
|
7965
|
+
});
|
|
7966
|
+
if (mintResp.ok) {
|
|
7967
|
+
const minted = await mintResp.json();
|
|
7968
|
+
writeFileSync7(jwtPath, minted.token + "\n", { mode: 384 });
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7674
7971
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
|
|
7675
7972
|
console.log(`Registered local MCP guardrails server in ${mcp.path}`);
|
|
7676
7973
|
console.log(` url: ${mcp.url}`);
|
|
@@ -7688,6 +7985,7 @@ async function installCommand(opts = {}) {
|
|
|
7688
7985
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
7689
7986
|
}
|
|
7690
7987
|
const minted = await mintResp.json();
|
|
7988
|
+
writeFileSync7(join11(SYNKRO_DIR2, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
7691
7989
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
7692
7990
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
7693
7991
|
console.log(` url: ${mcp.url}`);
|
|
@@ -8027,7 +8325,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
8027
8325
|
}
|
|
8028
8326
|
return { sessions: totalSessions, messages: totalMessages };
|
|
8029
8327
|
}
|
|
8030
|
-
var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH3, OFFSETS_DIR, RULES_PATH, MCP_LOCAL_PORT;
|
|
8328
|
+
var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH3, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, RULES_PATH, MCP_LOCAL_PORT;
|
|
8031
8329
|
var init_install2 = __esm({
|
|
8032
8330
|
"cli/commands/install.ts"() {
|
|
8033
8331
|
"use strict";
|
|
@@ -8050,6 +8348,52 @@ var init_install2 = __esm({
|
|
|
8050
8348
|
HOOKS_DIR = join11(SYNKRO_DIR2, "hooks");
|
|
8051
8349
|
BIN_DIR = join11(SYNKRO_DIR2, "bin");
|
|
8052
8350
|
CONFIG_PATH3 = join11(SYNKRO_DIR2, "config.env");
|
|
8351
|
+
MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
|
|
8352
|
+
import { readFileSync } from 'node:fs';
|
|
8353
|
+
import { homedir } from 'node:os';
|
|
8354
|
+
import { join } from 'node:path';
|
|
8355
|
+
import { createInterface } from 'node:readline';
|
|
8356
|
+
|
|
8357
|
+
const HOME = homedir();
|
|
8358
|
+
const TOKEN_PATH = join(HOME, '.synkro', '.mcp-jwt');
|
|
8359
|
+
const PORT = parseInt(process.env.SYNKRO_MCP_PORT || '8931', 10);
|
|
8360
|
+
const URL = \`http://127.0.0.1:\${PORT}\`;
|
|
8361
|
+
|
|
8362
|
+
let token = '';
|
|
8363
|
+
try { token = readFileSync(TOKEN_PATH, 'utf-8').trim(); } catch {}
|
|
8364
|
+
|
|
8365
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
8366
|
+
|
|
8367
|
+
rl.on('line', async (line) => {
|
|
8368
|
+
if (!line.trim()) return;
|
|
8369
|
+
let msg;
|
|
8370
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
8371
|
+
if (!msg.id && msg.method?.startsWith('notifications/')) return;
|
|
8372
|
+
|
|
8373
|
+
try {
|
|
8374
|
+
const resp = await fetch(URL, {
|
|
8375
|
+
method: 'POST',
|
|
8376
|
+
headers: {
|
|
8377
|
+
'Content-Type': 'application/json',
|
|
8378
|
+
'Authorization': \`Bearer \${token}\`,
|
|
8379
|
+
},
|
|
8380
|
+
body: line,
|
|
8381
|
+
signal: AbortSignal.timeout(30000),
|
|
8382
|
+
});
|
|
8383
|
+
if (resp.status === 204) return;
|
|
8384
|
+
const body = await resp.text();
|
|
8385
|
+
process.stdout.write(body + '\\n');
|
|
8386
|
+
} catch (err) {
|
|
8387
|
+
if (msg.id != null) {
|
|
8388
|
+
process.stdout.write(JSON.stringify({
|
|
8389
|
+
jsonrpc: '2.0',
|
|
8390
|
+
id: msg.id,
|
|
8391
|
+
error: { code: -32603, message: 'MCP proxy: HTTP server unreachable' },
|
|
8392
|
+
}) + '\\n');
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
});
|
|
8396
|
+
`;
|
|
8053
8397
|
OFFSETS_DIR = join11(SYNKRO_DIR2, ".transcript-offsets");
|
|
8054
8398
|
RULES_PATH = join11(SYNKRO_DIR2, "rules.json");
|
|
8055
8399
|
MCP_LOCAL_PORT = 8931;
|