argusqa-os 9.4.5 → 9.4.6
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/package.json
CHANGED
package/src/mcp-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Argus MCP Server (v9.4.
|
|
3
|
+
* Argus MCP Server (v9.4.6)
|
|
4
4
|
*
|
|
5
5
|
* Exposes Argus as an MCP server so Claude (or any MCP client) can call
|
|
6
6
|
* argus_audit, argus_audit_full, argus_compare, argus_last_report, and
|
|
@@ -125,6 +125,9 @@ async function withMcp(fn) {
|
|
|
125
125
|
const mcp = await createMcpClient();
|
|
126
126
|
try {
|
|
127
127
|
return await fn(mcp);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
logger.error('[ARGUS] MCP tool handler error:', err.message);
|
|
130
|
+
throw err;
|
|
128
131
|
} finally {
|
|
129
132
|
try { mcp.close(); } catch { /* ignore — process already gone */ }
|
|
130
133
|
}
|
|
@@ -283,7 +286,7 @@ async function handleLastReport() {
|
|
|
283
286
|
// ── Server bootstrap ──────────────────────────────────────────────────────────
|
|
284
287
|
|
|
285
288
|
const server = new Server(
|
|
286
|
-
{ name: 'argus', version: '9.4.
|
|
289
|
+
{ name: 'argus', version: '9.4.6' },
|
|
287
290
|
{ capabilities: { tools: {} } },
|
|
288
291
|
);
|
|
289
292
|
|
|
@@ -280,7 +280,7 @@ function classifyConsoleMessage(msg, routeIsCritical) {
|
|
|
280
280
|
function classifyNetworkRequest(req, routeIsCritical) {
|
|
281
281
|
const status = req.status ?? 0;
|
|
282
282
|
if (status >= 500) return 'critical';
|
|
283
|
-
if (status === 401 || status === 403) return 'critical';
|
|
283
|
+
if (status === 401 || status === 403) return routeIsCritical ? 'critical' : 'warning';
|
|
284
284
|
if (status >= 400) return routeIsCritical ? 'warning' : 'info';
|
|
285
285
|
return null;
|
|
286
286
|
}
|
|
@@ -806,16 +806,22 @@ export async function crawlRouteExpensive(route, baseUrl, mcp) {
|
|
|
806
806
|
const linksRaw = await browser.evaluate(INTERNAL_LINKS_SCRIPT);
|
|
807
807
|
const rawLinks = unwrapEval(linksRaw);
|
|
808
808
|
const links = [...new Set(Array.isArray(rawLinks) ? rawLinks.filter(Boolean) : [])];
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
809
|
+
const BROKEN_LINK_BATCH_TIMEOUT_MS = 15000;
|
|
810
|
+
const headResults = await Promise.race([
|
|
811
|
+
Promise.all(
|
|
812
|
+
links.map(async href => {
|
|
813
|
+
try {
|
|
814
|
+
const res = await fetch(href, { method: 'HEAD', signal: AbortSignal.timeout(5000) });
|
|
815
|
+
return { href, status: res.status };
|
|
816
|
+
} catch (err) {
|
|
817
|
+
return { href, status: 0, error: err.message };
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
),
|
|
821
|
+
new Promise((_, reject) =>
|
|
822
|
+
setTimeout(() => reject(new Error('broken-link batch timeout')), BROKEN_LINK_BATCH_TIMEOUT_MS)
|
|
823
|
+
),
|
|
824
|
+
]);
|
|
819
825
|
for (const { href, status } of headResults) {
|
|
820
826
|
if (status === 404) {
|
|
821
827
|
errors.push({
|
|
@@ -22,7 +22,13 @@ import { childLogger } from '../utils/logger.js';
|
|
|
22
22
|
|
|
23
23
|
const logger = childLogger('slack-notifier');
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Lazy-init — avoid constructing WebClient at module load so importing slack-notifier
|
|
26
|
+
// when SLACK_BOT_TOKEN is absent (HTML-report mode) doesn't create a useless client.
|
|
27
|
+
let _slack = null;
|
|
28
|
+
function getSlack() {
|
|
29
|
+
if (!_slack) _slack = new WebClient(process.env.SLACK_BOT_TOKEN);
|
|
30
|
+
return _slack;
|
|
31
|
+
}
|
|
26
32
|
|
|
27
33
|
const CHANNELS = {
|
|
28
34
|
critical: process.env.SLACK_CHANNEL_CRITICAL,
|
|
@@ -43,7 +49,7 @@ const SLACK_RATE_LIMIT_RETRIES = 3;
|
|
|
43
49
|
async function slackPostWithBackoff(args) {
|
|
44
50
|
for (let attempt = 0; attempt < SLACK_RATE_LIMIT_RETRIES; attempt++) {
|
|
45
51
|
try {
|
|
46
|
-
return await
|
|
52
|
+
return await getSlack().chat.postMessage(args);
|
|
47
53
|
} catch (err) {
|
|
48
54
|
const isRateLimit = err.code === 'slack_webapi_rate_limited'
|
|
49
55
|
|| err.message?.toLowerCase().includes('ratelimited');
|
|
@@ -75,7 +81,7 @@ async function uploadFileToSlack(filePath, channelId, filename) {
|
|
|
75
81
|
// Step 1: Get a pre-signed upload URL from Slack
|
|
76
82
|
let uploadUrl, fileId;
|
|
77
83
|
try {
|
|
78
|
-
const urlResponse = await
|
|
84
|
+
const urlResponse = await getSlack().files.getUploadURLExternal({
|
|
79
85
|
filename,
|
|
80
86
|
length: fileSize,
|
|
81
87
|
});
|
|
@@ -106,7 +112,7 @@ async function uploadFileToSlack(filePath, channelId, filename) {
|
|
|
106
112
|
|
|
107
113
|
// Step 3: Complete the upload and share to channel
|
|
108
114
|
try {
|
|
109
|
-
await
|
|
115
|
+
await getSlack().files.completeUploadExternal({
|
|
110
116
|
files: [{ id: fileId, title: filename }],
|
|
111
117
|
channel_id: channelId,
|
|
112
118
|
});
|
|
@@ -299,7 +305,7 @@ export async function acknowledgeMessage(ts, channelId, acknowledgingUser) {
|
|
|
299
305
|
|
|
300
306
|
try {
|
|
301
307
|
// Append an acknowledged context block by updating the message
|
|
302
|
-
const existing = await
|
|
308
|
+
const existing = await getSlack().conversations.history({
|
|
303
309
|
channel: channelId,
|
|
304
310
|
latest: ts,
|
|
305
311
|
inclusive: true,
|
|
@@ -325,7 +331,7 @@ export async function acknowledgeMessage(ts, channelId, acknowledgingUser) {
|
|
|
325
331
|
},
|
|
326
332
|
];
|
|
327
333
|
|
|
328
|
-
await
|
|
334
|
+
await getSlack().chat.update({
|
|
329
335
|
channel: channelId,
|
|
330
336
|
ts,
|
|
331
337
|
blocks: updatedBlocks,
|
|
@@ -108,10 +108,11 @@ export function matchesContract(reqUrl, reqMethod, contract) {
|
|
|
108
108
|
function loadSchema(contract) {
|
|
109
109
|
if (contract.schema) return contract.schema;
|
|
110
110
|
if (contract.schemaFile) {
|
|
111
|
-
// Prevent path traversal — schemaFile must
|
|
111
|
+
// Prevent path traversal — schemaFile must resolve inside the project directory.
|
|
112
|
+
// path.relative() + '..' check is robust across case differences and path separator variants.
|
|
112
113
|
const resolved = path.resolve(contract.schemaFile);
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
114
|
+
const rel = path.relative(process.cwd(), resolved);
|
|
115
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
115
116
|
logger.warn('[ARGUS] contract-validator: schemaFile outside project directory — skipping:', contract.schemaFile);
|
|
116
117
|
return null;
|
|
117
118
|
}
|
package/src/utils/mcp-client.js
CHANGED
|
@@ -119,6 +119,13 @@ export async function createMcpClient() {
|
|
|
119
119
|
} else {
|
|
120
120
|
resolve(msg.result);
|
|
121
121
|
}
|
|
122
|
+
} else if (msg.id !== undefined && !pending.has(msg.id)) {
|
|
123
|
+
// Response arrived after timeout already fired — log for observability
|
|
124
|
+
logger.debug(`[ARGUS] MCP late response for id=${msg.id} (already timed out or unknown)`);
|
|
125
|
+
} else if (msg.method) {
|
|
126
|
+
// Server-initiated notification (e.g. progress) — not an error, no action needed
|
|
127
|
+
} else {
|
|
128
|
+
logger.debug('[ARGUS] MCP unexpected message shape:', JSON.stringify(msg).slice(0, 200));
|
|
122
129
|
}
|
|
123
130
|
} catch {
|
|
124
131
|
// non-JSON line from process — ignore
|