@guava-parity/guard-scanner 9.1.0 → 15.0.0
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/README.md +42 -253
- package/SECURITY.md +12 -4
- package/SKILL.md +121 -59
- package/dist/openclaw-plugin.mjs +41 -0
- package/docs/EVIDENCE_DRIVEN.md +182 -0
- package/docs/banner.png +0 -0
- package/docs/data/corpus-metrics.json +11 -0
- package/docs/data/latest.json +29845 -0
- package/docs/generated/npm-audit-20260312.json +96 -0
- package/docs/generated/openclaw-upstream-status.json +25 -0
- package/docs/glossary.md +46 -0
- package/docs/index.html +1119 -0
- package/docs/logo.png +0 -0
- package/docs/openclaw-compatibility-audit.md +44 -0
- package/docs/openclaw-continuous-compatibility-plan.md +36 -0
- package/docs/rules/a2a-contagion.md +68 -0
- package/docs/rules/advanced-exfil.md +52 -0
- package/docs/rules/agent-protocol.md +108 -0
- package/docs/rules/api-abuse.md +68 -0
- package/docs/rules/autonomous-risk.md +92 -0
- package/docs/rules/config-impact.md +132 -0
- package/docs/rules/credential-handling.md +100 -0
- package/docs/rules/cve-patterns.md +332 -0
- package/docs/rules/data-exposure.md +84 -0
- package/docs/rules/exfiltration.md +36 -0
- package/docs/rules/financial-access.md +84 -0
- package/docs/rules/identity-hijack.md +140 -0
- package/docs/rules/inference-manipulation.md +60 -0
- package/docs/rules/leaky-skills.md +52 -0
- package/docs/rules/malicious-code.md +108 -0
- package/docs/rules/mcp-security.md +148 -0
- package/docs/rules/memory-poisoning.md +84 -0
- package/docs/rules/model-poisoning.md +44 -0
- package/docs/rules/obfuscation.md +60 -0
- package/docs/rules/persistence.md +108 -0
- package/docs/rules/pii-exposure.md +116 -0
- package/docs/rules/prompt-injection.md +148 -0
- package/docs/rules/prompt-worm.md +44 -0
- package/docs/rules/safeguard-bypass.md +44 -0
- package/docs/rules/sandbox-escape.md +100 -0
- package/docs/rules/secret-detection.md +44 -0
- package/docs/rules/supply-chain-v2.md +92 -0
- package/docs/rules/suspicious-download.md +60 -0
- package/docs/rules/trust-boundary.md +76 -0
- package/docs/rules/trust-exploitation.md +92 -0
- package/docs/rules/unverifiable-deps.md +84 -0
- package/docs/rules/vdb-injection.md +84 -0
- package/docs/security-vulnerability-report-20260312.md +53 -0
- package/docs/spec/PRD_V2_ARCHITECTURE.md +55 -0
- package/docs/spec/capabilities.json +42 -0
- package/docs/spec/finding.schema.json +104 -0
- package/docs/spec/integration-manifest.md +39 -0
- package/docs/spec/sbom.json +33 -0
- package/docs/threat-model.md +65 -0
- package/docs/v13-architecture-manifest.md +55 -0
- package/hooks/context.js +305 -0
- package/hooks/guard-scanner/plugin.ts +24 -1
- package/openclaw-plugin.mts +91 -0
- package/openclaw.plugin.json +30 -53
- package/package.json +80 -57
- package/src/cli.js +174 -34
- package/src/core/content-loader.js +42 -0
- package/src/core/inventory.js +73 -0
- package/src/core/report-adapters.js +171 -0
- package/src/core/risk-engine.js +93 -0
- package/src/core/rule-registry.js +73 -0
- package/src/core/semantic-validators.js +85 -0
- package/src/finding-schema.js +191 -0
- package/src/hooks/context.ts +49 -0
- package/src/html-template.js +2 -2
- package/src/mcp-server.js +192 -5
- package/src/openclaw-upstream.js +128 -0
- package/src/patterns.js +519 -157
- package/src/policy-engine.js +32 -0
- package/src/runtime-guard.js +40 -2
- package/src/scanner.js +228 -231
- package/src/skill-crawler.js +254 -0
- package/src/threat-model.js +50 -0
- package/src/validation-layer.js +39 -0
package/src/html-template.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* guard-scanner — HTML Report Template
|
|
3
3
|
* Dark Glassmorphism + Conic-gradient Risk Gauges
|
|
4
|
-
*
|
|
4
|
+
* Lightweight HTML report template. Pure CSS animations.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
'use strict';
|
|
@@ -229,7 +229,7 @@ ${total > 0 ? `<div class="ag">
|
|
|
229
229
|
</div>
|
|
230
230
|
|
|
231
231
|
<div class="ft">
|
|
232
|
-
guard-scanner v${version} —
|
|
232
|
+
guard-scanner v${version} — Lightweight runtime footprint (1 dependency: ws). 🛡️<br>
|
|
233
233
|
Built by <a href="https://github.com/koatora20">Guava 🍈 & Dee</a>
|
|
234
234
|
</div>
|
|
235
235
|
</div>
|
package/src/mcp-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* guard-scanner MCP Server —
|
|
3
|
+
* guard-scanner MCP Server — Lightweight stdio JSON-RPC 2.0
|
|
4
4
|
*
|
|
5
5
|
* @security-manifest
|
|
6
6
|
* env-read: [VT_API_KEY (optional, for audit vt-scan)]
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Implements: initialize, tools/list, tools/call, notifications
|
|
16
16
|
*
|
|
17
17
|
* Tools:
|
|
18
|
-
* scan_skill — Scan a directory for security threats
|
|
18
|
+
* scan_skill — Scan a directory for security threats
|
|
19
19
|
* scan_text — Scan a code/text snippet inline
|
|
20
20
|
* check_tool_call — Runtime check before a tool call (26 checks, 5 layers)
|
|
21
21
|
* audit_assets — Audit npm/GitHub/ClawHub assets for exposure
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
|
|
28
28
|
const { GuardScanner, VERSION, scanToolCall, getCheckStats, LAYER_NAMES } = require('./scanner.js');
|
|
29
29
|
const { AssetAuditor, AUDIT_VERSION } = require('./asset-auditor.js');
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
const CAPABILITIES = require('../docs/spec/capabilities.json');
|
|
30
34
|
|
|
31
35
|
// ── MCP Protocol Constants ──
|
|
32
36
|
|
|
@@ -42,12 +46,85 @@ const SERVER_CAPABILITIES = {
|
|
|
42
46
|
tools: {},
|
|
43
47
|
};
|
|
44
48
|
|
|
49
|
+
const STATIC_SUMMARY = `${CAPABILITIES.static_pattern_count} threat patterns across ${CAPABILITIES.threat_category_count} categories`;
|
|
50
|
+
|
|
51
|
+
// ── Async Task Store (run_async / status / result / cancel) ──
|
|
52
|
+
|
|
53
|
+
const TASK_DIR = process.env.GUARD_SCANNER_TASK_DIR || path.join(os.homedir(), '.openclaw', 'guard-scanner', 'tasks');
|
|
54
|
+
const TASK_FILE = path.join(TASK_DIR, 'tasks.json');
|
|
55
|
+
|
|
56
|
+
function ensureTaskStore() {
|
|
57
|
+
fs.mkdirSync(TASK_DIR, { recursive: true });
|
|
58
|
+
if (!fs.existsSync(TASK_FILE)) fs.writeFileSync(TASK_FILE, '{}');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadTasks() {
|
|
62
|
+
ensureTaskStore();
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(TASK_FILE, 'utf8') || '{}');
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function saveTasks(tasks) {
|
|
71
|
+
ensureTaskStore();
|
|
72
|
+
fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeTaskId() {
|
|
76
|
+
return `task_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nowIso() {
|
|
80
|
+
return new Date().toISOString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runTaskAsync(taskId, toolName, toolArgs) {
|
|
84
|
+
setImmediate(async () => {
|
|
85
|
+
const tasks = loadTasks();
|
|
86
|
+
const t = tasks[taskId];
|
|
87
|
+
if (!t || t.state === 'cancelled') return;
|
|
88
|
+
|
|
89
|
+
t.state = 'running';
|
|
90
|
+
t.startedAt = nowIso();
|
|
91
|
+
t.updatedAt = nowIso();
|
|
92
|
+
saveTasks(tasks);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
let result;
|
|
96
|
+
if (toolName === 'scan_skill') result = handleScanSkill(toolArgs);
|
|
97
|
+
else if (toolName === 'scan_text') result = handleScanText(toolArgs);
|
|
98
|
+
else if (toolName === 'check_tool_call') result = handleCheckToolCall(toolArgs);
|
|
99
|
+
else if (toolName === 'audit_assets') result = await handleAuditAssets(toolArgs);
|
|
100
|
+
else if (toolName === 'get_stats') result = handleGetStats(toolArgs);
|
|
101
|
+
else throw new Error(`Unsupported async tool: ${toolName}`);
|
|
102
|
+
|
|
103
|
+
const latest = loadTasks();
|
|
104
|
+
if (!latest[taskId] || latest[taskId].state === 'cancelled') return;
|
|
105
|
+
latest[taskId].state = result?.isError ? 'failed' : 'succeeded';
|
|
106
|
+
latest[taskId].updatedAt = nowIso();
|
|
107
|
+
latest[taskId].finishedAt = nowIso();
|
|
108
|
+
latest[taskId].result = result;
|
|
109
|
+
saveTasks(latest);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
const latest = loadTasks();
|
|
112
|
+
if (!latest[taskId]) return;
|
|
113
|
+
latest[taskId].state = 'failed';
|
|
114
|
+
latest[taskId].updatedAt = nowIso();
|
|
115
|
+
latest[taskId].finishedAt = nowIso();
|
|
116
|
+
latest[taskId].error = e.message;
|
|
117
|
+
saveTasks(latest);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
45
122
|
// ── Tool Definitions ──
|
|
46
123
|
|
|
47
124
|
const TOOLS = [
|
|
48
125
|
{
|
|
49
126
|
name: 'scan_skill',
|
|
50
|
-
description:
|
|
127
|
+
description: `Scan a directory for agent security threats. Detects prompt injection, data exfiltration, credential theft, reverse shells, and ${STATIC_SUMMARY}. Returns risk score, verdict, and detailed findings.`,
|
|
51
128
|
inputSchema: {
|
|
52
129
|
type: 'object',
|
|
53
130
|
properties: {
|
|
@@ -141,8 +218,49 @@ const TOOLS = [
|
|
|
141
218
|
properties: {},
|
|
142
219
|
},
|
|
143
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: 'experimental.run_async',
|
|
223
|
+
description: '[Experimental] Run a supported guard-scanner tool asynchronously. Returns taskId immediately; use experimental.task_status/experimental.task_result to retrieve output.',
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {
|
|
227
|
+
tool: { type: 'string', enum: ['scan_skill', 'scan_text', 'check_tool_call', 'audit_assets', 'get_stats'] },
|
|
228
|
+
args: { type: 'object', additionalProperties: true, default: {} },
|
|
229
|
+
},
|
|
230
|
+
required: ['tool'],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'experimental.task_status',
|
|
235
|
+
description: '[Experimental] Get async task status by taskId.',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: { taskId: { type: 'string' } },
|
|
239
|
+
required: ['taskId'],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'experimental.task_result',
|
|
244
|
+
description: '[Experimental] Get async task final result by taskId.',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: { taskId: { type: 'string' } },
|
|
248
|
+
required: ['taskId'],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'experimental.task_cancel',
|
|
253
|
+
description: '[Experimental] Cancel async task by taskId (best-effort).',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: { taskId: { type: 'string' }, reason: { type: 'string' } },
|
|
257
|
+
required: ['taskId'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
144
260
|
];
|
|
145
261
|
|
|
262
|
+
// NOTE: cron_glm5_config was removed in v14.0.0 — not guard-scanner's responsibility
|
|
263
|
+
|
|
146
264
|
// ── Tool Handlers ──
|
|
147
265
|
|
|
148
266
|
function handleScanSkill({ path: scanPath, verbose = false, strict = false }) {
|
|
@@ -227,10 +345,11 @@ function handleCheckToolCall({ tool, args, mode = 'enforce' }) {
|
|
|
227
345
|
if (args === undefined) return errorResult('args is required');
|
|
228
346
|
|
|
229
347
|
const result = scanToolCall(tool, args, { mode, auditLog: true });
|
|
348
|
+
const runtimeCheckCount = getCheckStats().total;
|
|
230
349
|
|
|
231
350
|
if (result.detections.length === 0) {
|
|
232
351
|
return successResult(
|
|
233
|
-
`✅ Tool call "${tool}" passed all
|
|
352
|
+
`✅ Tool call "${tool}" passed all ${runtimeCheckCount} runtime checks.\nMode: ${mode}`
|
|
234
353
|
);
|
|
235
354
|
}
|
|
236
355
|
|
|
@@ -286,7 +405,7 @@ function handleGetStats() {
|
|
|
286
405
|
return successResult(
|
|
287
406
|
`🛡️ guard-scanner v${VERSION}\n\n` +
|
|
288
407
|
`Static Analysis:\n` +
|
|
289
|
-
` •
|
|
408
|
+
` • ${STATIC_SUMMARY}\n` +
|
|
290
409
|
` • Entropy-based secret detection\n` +
|
|
291
410
|
` • Data flow analysis (JS)\n` +
|
|
292
411
|
` • Cross-file reference checking\n` +
|
|
@@ -307,6 +426,66 @@ function handleGetStats() {
|
|
|
307
426
|
);
|
|
308
427
|
}
|
|
309
428
|
|
|
429
|
+
function handleRunAsync({ tool, args = {} }) {
|
|
430
|
+
if (!tool) return errorResult('tool is required');
|
|
431
|
+
const supported = new Set(['scan_skill', 'scan_text', 'check_tool_call', 'audit_assets', 'get_stats']);
|
|
432
|
+
if (!supported.has(tool)) return errorResult(`Unsupported async tool: ${tool}`);
|
|
433
|
+
|
|
434
|
+
const taskId = makeTaskId();
|
|
435
|
+
const tasks = loadTasks();
|
|
436
|
+
tasks[taskId] = {
|
|
437
|
+
taskId,
|
|
438
|
+
tool,
|
|
439
|
+
args,
|
|
440
|
+
state: 'queued',
|
|
441
|
+
createdAt: nowIso(),
|
|
442
|
+
updatedAt: nowIso(),
|
|
443
|
+
};
|
|
444
|
+
saveTasks(tasks);
|
|
445
|
+
runTaskAsync(taskId, tool, args);
|
|
446
|
+
|
|
447
|
+
return successResult(`accepted\ntaskId=${taskId}\nstate=queued\npollAfterMs=800`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function handleTaskStatus({ taskId }) {
|
|
451
|
+
if (!taskId) return errorResult('taskId is required');
|
|
452
|
+
const tasks = loadTasks();
|
|
453
|
+
const t = tasks[taskId];
|
|
454
|
+
if (!t) return errorResult(`Task not found: ${taskId}`);
|
|
455
|
+
return successResult(`taskId=${taskId}\nstate=${t.state}\nupdatedAt=${t.updatedAt}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function handleTaskResult({ taskId }) {
|
|
459
|
+
if (!taskId) return errorResult('taskId is required');
|
|
460
|
+
const tasks = loadTasks();
|
|
461
|
+
const t = tasks[taskId];
|
|
462
|
+
if (!t) return errorResult(`Task not found: ${taskId}`);
|
|
463
|
+
if (t.state === 'queued' || t.state === 'running') {
|
|
464
|
+
return successResult(`taskId=${taskId}\nstate=${t.state}\nmessage=not ready`);
|
|
465
|
+
}
|
|
466
|
+
if (t.error) return errorResult(`taskId=${taskId}\nstate=${t.state}\nerror=${t.error}`);
|
|
467
|
+
if (!t.result) return errorResult(`taskId=${taskId}\nstate=${t.state}\nerror=no result`);
|
|
468
|
+
return t.result;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function handleTaskCancel({ taskId, reason = 'user cancel' }) {
|
|
472
|
+
if (!taskId) return errorResult('taskId is required');
|
|
473
|
+
const tasks = loadTasks();
|
|
474
|
+
const t = tasks[taskId];
|
|
475
|
+
if (!t) return errorResult(`Task not found: ${taskId}`);
|
|
476
|
+
if (t.state === 'succeeded' || t.state === 'failed' || t.state === 'cancelled') {
|
|
477
|
+
return successResult(`taskId=${taskId}\nstate=${t.state}\nmessage=already terminal`);
|
|
478
|
+
}
|
|
479
|
+
t.state = 'cancelled';
|
|
480
|
+
t.updatedAt = nowIso();
|
|
481
|
+
t.finishedAt = nowIso();
|
|
482
|
+
t.cancelReason = reason;
|
|
483
|
+
saveTasks(tasks);
|
|
484
|
+
return successResult(`taskId=${taskId}\nstate=cancelled`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// handleCronGlm5Config removed in v14.0.0 — not guard-scanner's responsibility
|
|
488
|
+
|
|
310
489
|
// ── Result helpers ──
|
|
311
490
|
|
|
312
491
|
function successResult(text) {
|
|
@@ -444,6 +623,14 @@ class MCPServer {
|
|
|
444
623
|
return await handleAuditAssets(args);
|
|
445
624
|
case 'get_stats':
|
|
446
625
|
return handleGetStats();
|
|
626
|
+
case 'experimental.run_async':
|
|
627
|
+
return handleRunAsync(args);
|
|
628
|
+
case 'experimental.task_status':
|
|
629
|
+
return handleTaskStatus(args);
|
|
630
|
+
case 'experimental.task_result':
|
|
631
|
+
return handleTaskResult(args);
|
|
632
|
+
case 'experimental.task_cancel':
|
|
633
|
+
return handleTaskCancel(args);
|
|
447
634
|
default:
|
|
448
635
|
return errorResult(`Unknown tool: ${name}`);
|
|
449
636
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const https = require('node:https');
|
|
2
|
+
|
|
3
|
+
function parseVersion(version) {
|
|
4
|
+
const [stable, prerelease = ''] = String(version).split('-', 2);
|
|
5
|
+
const parts = stable.split('.').map((value) => Number.parseInt(value, 10));
|
|
6
|
+
while (parts.length < 3) parts.push(0);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
raw: version,
|
|
10
|
+
parts,
|
|
11
|
+
prerelease,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function compareOpenClawVersions(left, right) {
|
|
16
|
+
const a = parseVersion(left);
|
|
17
|
+
const b = parseVersion(right);
|
|
18
|
+
|
|
19
|
+
for (let index = 0; index < 3; index += 1) {
|
|
20
|
+
if (a.parts[index] > b.parts[index]) return 1;
|
|
21
|
+
if (a.parts[index] < b.parts[index]) return -1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!a.prerelease && b.prerelease) return 1;
|
|
25
|
+
if (a.prerelease && !b.prerelease) return -1;
|
|
26
|
+
if (a.prerelease > b.prerelease) return 1;
|
|
27
|
+
if (a.prerelease < b.prerelease) return -1;
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function evaluateOpenClawBaseline({ pinnedVersion, latestVersion, latestPublishedAt, source }) {
|
|
32
|
+
const comparison = compareOpenClawVersions(pinnedVersion, latestVersion);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
pinnedVersion,
|
|
36
|
+
latestVersion,
|
|
37
|
+
latestPublishedAt,
|
|
38
|
+
source,
|
|
39
|
+
upToDate: comparison === 0,
|
|
40
|
+
ahead: comparison > 0,
|
|
41
|
+
behind: comparison < 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeGitHubReleaseVersion(tagName) {
|
|
46
|
+
return String(tagName).replace(/^v/i, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function evaluateOpenClawSourceParity({ npmLatestVersion, githubLatestVersion }) {
|
|
50
|
+
const normalizedGitHubVersion = normalizeGitHubReleaseVersion(githubLatestVersion);
|
|
51
|
+
return {
|
|
52
|
+
npmLatestVersion,
|
|
53
|
+
githubLatestVersion: normalizedGitHubVersion,
|
|
54
|
+
inParity: compareOpenClawVersions(npmLatestVersion, normalizedGitHubVersion) === 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function httpGetJson(url) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
https
|
|
61
|
+
.get(
|
|
62
|
+
url,
|
|
63
|
+
{
|
|
64
|
+
headers: {
|
|
65
|
+
'user-agent': 'guard-scanner-openclaw-upstream-check',
|
|
66
|
+
accept: 'application/json',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
(response) => {
|
|
70
|
+
let body = '';
|
|
71
|
+
response.setEncoding('utf8');
|
|
72
|
+
response.on('data', (chunk) => {
|
|
73
|
+
body += chunk;
|
|
74
|
+
});
|
|
75
|
+
response.on('end', () => {
|
|
76
|
+
if (response.statusCode && response.statusCode >= 400) {
|
|
77
|
+
reject(new Error(`GET ${url} failed with status ${response.statusCode}`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
resolve(JSON.parse(body));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
reject(error);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
.on('error', reject);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function fetchLatestOpenClawRelease(fetchJson = httpGetJson) {
|
|
93
|
+
const npmMeta = await fetchJson('https://registry.npmjs.org/openclaw');
|
|
94
|
+
const latestVersion = npmMeta['dist-tags']?.latest;
|
|
95
|
+
if (!latestVersion) {
|
|
96
|
+
throw new Error('npm registry metadata missing dist-tags.latest for openclaw');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const githubRelease = await fetchJson('https://api.github.com/repos/openclaw/openclaw/releases/latest');
|
|
100
|
+
const githubLatestVersion = normalizeGitHubReleaseVersion(githubRelease.tag_name || '');
|
|
101
|
+
if (!githubLatestVersion) {
|
|
102
|
+
throw new Error('GitHub releases/latest missing tag_name for openclaw/openclaw');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const parity = evaluateOpenClawSourceParity({
|
|
106
|
+
npmLatestVersion: latestVersion,
|
|
107
|
+
githubLatestVersion,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
latestVersion,
|
|
112
|
+
latestPublishedAt: npmMeta.time?.[latestVersion] ?? null,
|
|
113
|
+
source: 'npm',
|
|
114
|
+
registryModifiedAt: npmMeta.time?.modified ?? null,
|
|
115
|
+
githubLatestVersion,
|
|
116
|
+
githubPublishedAt: githubRelease.published_at ?? null,
|
|
117
|
+
githubUrl: githubRelease.html_url ?? null,
|
|
118
|
+
sourceParity: parity,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
compareOpenClawVersions,
|
|
124
|
+
evaluateOpenClawBaseline,
|
|
125
|
+
evaluateOpenClawSourceParity,
|
|
126
|
+
fetchLatestOpenClawRelease,
|
|
127
|
+
normalizeGitHubReleaseVersion,
|
|
128
|
+
};
|