claude-flow 3.14.4 → 3.16.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/.claude/settings.local.json +2 -1
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.d.ts +139 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.js +358 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.d.ts +47 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/domain-affinity-policy.js +65 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.d.ts +96 -0
- package/v3/@claude-flow/cli/dist/src/business-pods/pod-schema.js +225 -0
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +20 -5
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +21 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.js +394 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.d.ts +36 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.js +253 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/business-pod-tools.js +169 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.d.ts +55 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/http-fetch-tools.js +329 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +8 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.js +83 -4
- package/v3/@claude-flow/cli/package.json +4 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http_fetch MCP tool — ADR-164 §5.1.8.
|
|
3
|
+
*
|
|
4
|
+
* The Operations business pod (templates/ops.json) references `http_fetch` as
|
|
5
|
+
* an allowed MCP tool for the synthetic-endpoint availability bench (probe
|
|
6
|
+
* 200/500 from a configured URL, escalate to #ops on 500-rate spikes).
|
|
7
|
+
* Phase 3 left this as a TODO; Phase 4 ships the tool with secure-by-default
|
|
8
|
+
* gating per the §5.1.8 contract: URL allowlist (no file://, ftp://, no
|
|
9
|
+
* RFC-1918 / loopback unless explicitly enabled), header sanitization (no
|
|
10
|
+
* auth pass-through unless explicitly enabled), hard timeout via
|
|
11
|
+
* AbortController, response truncation, default User-Agent.
|
|
12
|
+
*
|
|
13
|
+
* Architectural constraints (load-bearing):
|
|
14
|
+
* - DEFAULT-REFUSES private addresses + loopback (CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 opt-in)
|
|
15
|
+
* - DEFAULT-REFUSES Authorization / Cookie / X-Auth-* (CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 opt-in)
|
|
16
|
+
* - hard timeout 30s default, 60s ceiling
|
|
17
|
+
* - response truncated to 256KB default, 1MB ceiling
|
|
18
|
+
* - no redirects auto-followed beyond fetch's default; status reported as-is
|
|
19
|
+
*
|
|
20
|
+
* @module @claude-flow/cli/mcp-tools/http-fetch
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
23
|
+
const MAX_TIMEOUT_MS = 60_000;
|
|
24
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 256 * 1024; // 256 KB
|
|
25
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024; // 1 MB
|
|
26
|
+
const DEFAULT_USER_AGENT = 'ruflo-http-fetch/1.0';
|
|
27
|
+
const FORBIDDEN_HEADER_PREFIXES = ['x-auth-', 'x-api-key'];
|
|
28
|
+
const FORBIDDEN_HEADERS_EXACT = ['authorization', 'cookie', 'set-cookie', 'proxy-authorization'];
|
|
29
|
+
export class HttpFetchValidationError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
constructor(message, code) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.name = 'HttpFetchValidationError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Decide whether the URL is permitted under the default secure-by-default
|
|
39
|
+
* allowlist. Block file://, ftp://, RFC-1918 private addresses, loopback,
|
|
40
|
+
* link-local — unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 is set.
|
|
41
|
+
*/
|
|
42
|
+
export function validateUrl(rawUrl) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(rawUrl);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new HttpFetchValidationError(`invalid URL: ${rawUrl}`, 'INVALID_URL');
|
|
49
|
+
}
|
|
50
|
+
const proto = parsed.protocol.toLowerCase();
|
|
51
|
+
if (proto !== 'http:' && proto !== 'https:') {
|
|
52
|
+
throw new HttpFetchValidationError(`protocol ${parsed.protocol} not allowed (only http: and https:)`, 'FORBIDDEN_PROTOCOL');
|
|
53
|
+
}
|
|
54
|
+
const host = parsed.hostname.toLowerCase();
|
|
55
|
+
const allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';
|
|
56
|
+
if (!allowPrivate && isPrivateOrLoopback(host)) {
|
|
57
|
+
throw new HttpFetchValidationError(`host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override`, 'PRIVATE_ADDRESS');
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
function isPrivateOrLoopback(host) {
|
|
62
|
+
if (host === 'localhost' || host === 'localhost.localdomain')
|
|
63
|
+
return true;
|
|
64
|
+
// IPv6 loopback
|
|
65
|
+
if (host === '::1' || host === '[::1]')
|
|
66
|
+
return true;
|
|
67
|
+
// IPv4 numeric checks
|
|
68
|
+
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
69
|
+
if (m) {
|
|
70
|
+
const a = Number(m[1]);
|
|
71
|
+
const b = Number(m[2]);
|
|
72
|
+
if (a === 0)
|
|
73
|
+
return true; // 0.0.0.0/8
|
|
74
|
+
if (a === 127)
|
|
75
|
+
return true; // loopback
|
|
76
|
+
if (a === 10)
|
|
77
|
+
return true; // RFC1918
|
|
78
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
79
|
+
return true; // RFC1918
|
|
80
|
+
if (a === 192 && b === 168)
|
|
81
|
+
return true; // RFC1918
|
|
82
|
+
if (a === 169 && b === 254)
|
|
83
|
+
return true; // link-local
|
|
84
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
85
|
+
return true; // CGNAT
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// IPv6 bracketed addresses and other special forms — be conservative and
|
|
89
|
+
// accept only public-looking literals. Reject obvious private/local forms.
|
|
90
|
+
if (host.startsWith('fc') || host.startsWith('fd'))
|
|
91
|
+
return true; // fc00::/7 ULA
|
|
92
|
+
if (host.startsWith('fe80:'))
|
|
93
|
+
return true; // link-local
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
export function validateHeaders(headers) {
|
|
97
|
+
const allowAuth = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH === '1';
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
100
|
+
const lower = key.toLowerCase();
|
|
101
|
+
if (!allowAuth) {
|
|
102
|
+
if (FORBIDDEN_HEADERS_EXACT.includes(lower)) {
|
|
103
|
+
throw new HttpFetchValidationError(`header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`, 'FORBIDDEN_HEADER');
|
|
104
|
+
}
|
|
105
|
+
if (FORBIDDEN_HEADER_PREFIXES.some((p) => lower.startsWith(p))) {
|
|
106
|
+
throw new HttpFetchValidationError(`header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`, 'FORBIDDEN_HEADER');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (typeof value !== 'string') {
|
|
110
|
+
throw new HttpFetchValidationError(`header "${key}" must be a string`, 'INVALID_HEADER_VALUE');
|
|
111
|
+
}
|
|
112
|
+
out[key] = value;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function clampNumber(raw, defaultValue, max) {
|
|
117
|
+
if (raw === undefined || raw === null)
|
|
118
|
+
return defaultValue;
|
|
119
|
+
const n = Number(raw);
|
|
120
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
121
|
+
return defaultValue;
|
|
122
|
+
return Math.min(Math.floor(n), max);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Pure execution path so tests can call it without going through the MCP
|
|
126
|
+
* dispatcher. Returns a result object (does not throw on validation failure
|
|
127
|
+
* — it returns success: false with an errorCode).
|
|
128
|
+
*/
|
|
129
|
+
export async function httpFetchExecute(input) {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
const url = String(input.url ?? '');
|
|
132
|
+
const method = String(input.method ?? 'GET').toUpperCase();
|
|
133
|
+
if (!['GET', 'POST', 'HEAD'].includes(method)) {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
status: 0,
|
|
137
|
+
statusText: '',
|
|
138
|
+
headers: {},
|
|
139
|
+
body: '',
|
|
140
|
+
bodyTruncated: false,
|
|
141
|
+
bytesRead: 0,
|
|
142
|
+
durationMs: Date.now() - startedAt,
|
|
143
|
+
url,
|
|
144
|
+
method,
|
|
145
|
+
error: `method ${method} not allowed (GET, POST, HEAD only)`,
|
|
146
|
+
errorCode: 'INVALID_METHOD',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = validateUrl(url);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
const err = e;
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
status: 0,
|
|
158
|
+
statusText: '',
|
|
159
|
+
headers: {},
|
|
160
|
+
body: '',
|
|
161
|
+
bodyTruncated: false,
|
|
162
|
+
bytesRead: 0,
|
|
163
|
+
durationMs: Date.now() - startedAt,
|
|
164
|
+
url,
|
|
165
|
+
method,
|
|
166
|
+
error: err.message,
|
|
167
|
+
errorCode: err.code ?? 'VALIDATION_ERROR',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
let headers;
|
|
171
|
+
try {
|
|
172
|
+
const raw = (input.headers ?? {});
|
|
173
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
174
|
+
throw new HttpFetchValidationError('headers must be an object', 'INVALID_HEADERS');
|
|
175
|
+
}
|
|
176
|
+
headers = validateHeaders(raw);
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
const err = e;
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
status: 0,
|
|
183
|
+
statusText: '',
|
|
184
|
+
headers: {},
|
|
185
|
+
body: '',
|
|
186
|
+
bodyTruncated: false,
|
|
187
|
+
bytesRead: 0,
|
|
188
|
+
durationMs: Date.now() - startedAt,
|
|
189
|
+
url,
|
|
190
|
+
method,
|
|
191
|
+
error: err.message,
|
|
192
|
+
errorCode: err.code ?? 'VALIDATION_ERROR',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Default UA if not set
|
|
196
|
+
const hasUA = Object.keys(headers).some((k) => k.toLowerCase() === 'user-agent');
|
|
197
|
+
if (!hasUA)
|
|
198
|
+
headers['User-Agent'] = DEFAULT_USER_AGENT;
|
|
199
|
+
const timeoutMs = clampNumber(input.timeoutMs, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
200
|
+
const maxResponseBytes = clampNumber(input.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES);
|
|
201
|
+
const body = input.body !== undefined ? String(input.body) : undefined;
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetch(parsed.toString(), {
|
|
206
|
+
method,
|
|
207
|
+
headers,
|
|
208
|
+
body: method === 'GET' || method === 'HEAD' ? undefined : body,
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
// Keep redirect default ('follow') — caller sees final status/url.
|
|
211
|
+
});
|
|
212
|
+
// Drain the body with byte budget. Use array-buffer first to bound size
|
|
213
|
+
// deterministically before decoding to UTF-8.
|
|
214
|
+
const reader = response.body?.getReader();
|
|
215
|
+
let bytesRead = 0;
|
|
216
|
+
let bodyTruncated = false;
|
|
217
|
+
const chunks = [];
|
|
218
|
+
if (reader) {
|
|
219
|
+
// eslint-disable-next-line no-constant-condition
|
|
220
|
+
while (true) {
|
|
221
|
+
const { value, done } = await reader.read();
|
|
222
|
+
if (done)
|
|
223
|
+
break;
|
|
224
|
+
if (value) {
|
|
225
|
+
if (bytesRead + value.byteLength > maxResponseBytes) {
|
|
226
|
+
const remaining = Math.max(0, maxResponseBytes - bytesRead);
|
|
227
|
+
if (remaining > 0)
|
|
228
|
+
chunks.push(value.slice(0, remaining));
|
|
229
|
+
bytesRead += remaining;
|
|
230
|
+
bodyTruncated = true;
|
|
231
|
+
try {
|
|
232
|
+
await reader.cancel('body-truncated');
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
chunks.push(value);
|
|
238
|
+
bytesRead += value.byteLength;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const combined = new Uint8Array(bytesRead);
|
|
243
|
+
let offset = 0;
|
|
244
|
+
for (const c of chunks) {
|
|
245
|
+
combined.set(c, offset);
|
|
246
|
+
offset += c.byteLength;
|
|
247
|
+
}
|
|
248
|
+
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
249
|
+
const bodyText = decoder.decode(combined);
|
|
250
|
+
const outHeaders = {};
|
|
251
|
+
response.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
status: response.status,
|
|
255
|
+
statusText: response.statusText,
|
|
256
|
+
headers: outHeaders,
|
|
257
|
+
body: bodyText,
|
|
258
|
+
bodyTruncated,
|
|
259
|
+
bytesRead,
|
|
260
|
+
durationMs: Date.now() - startedAt,
|
|
261
|
+
url: parsed.toString(),
|
|
262
|
+
method,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
267
|
+
const aborted = msg.includes('aborted') || e?.name === 'AbortError';
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
status: 0,
|
|
271
|
+
statusText: '',
|
|
272
|
+
headers: {},
|
|
273
|
+
body: '',
|
|
274
|
+
bodyTruncated: false,
|
|
275
|
+
bytesRead: 0,
|
|
276
|
+
durationMs: Date.now() - startedAt,
|
|
277
|
+
url: parsed.toString(),
|
|
278
|
+
method,
|
|
279
|
+
error: aborted ? `request aborted after ${timeoutMs}ms timeout` : msg,
|
|
280
|
+
errorCode: aborted ? 'TIMEOUT' : 'FETCH_ERROR',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export const httpFetchTools = [
|
|
288
|
+
{
|
|
289
|
+
name: 'http_fetch',
|
|
290
|
+
description: 'ADR-164 §5.1.8 — HTTP probe primitive for business-pod ops benches (synthetic 200/500 endpoint checks, third-party status pages). Default-secure: blocks file://, ftp://, RFC-1918 / loopback / link-local hosts unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1, and rejects Authorization / Cookie / X-Auth-* headers unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1. Hard 30s timeout (60s ceiling), response truncated to 256 KB (1 MB ceiling), default User-Agent ruflo-http-fetch/1.0. Use when a pod or smoke contract needs a guarded HTTP probe — calling Node fetch() directly is wrong because it skips the URL allowlist and header sanitization that ADR-164 mandates for autopilot mode. Pair with the ops-pod bench in plugins/ruflo-business-pods/templates/ops.json (the §4.4 synthetic-endpoint test).',
|
|
291
|
+
category: 'business-pods',
|
|
292
|
+
tags: ['business-pods', 'http', 'fetch', 'adr-164', 'security', 'ops-pod'],
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {
|
|
296
|
+
url: {
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'Absolute http:// or https:// URL. file://, ftp://, RFC-1918 / loopback / link-local blocked by default.',
|
|
299
|
+
},
|
|
300
|
+
method: {
|
|
301
|
+
type: 'string',
|
|
302
|
+
enum: ['GET', 'POST', 'HEAD'],
|
|
303
|
+
description: 'HTTP method. Defaults to GET.',
|
|
304
|
+
},
|
|
305
|
+
timeoutMs: {
|
|
306
|
+
type: 'number',
|
|
307
|
+
description: 'Hard timeout in milliseconds. Default 30000, max 60000.',
|
|
308
|
+
},
|
|
309
|
+
maxResponseBytes: {
|
|
310
|
+
type: 'number',
|
|
311
|
+
description: 'Max body bytes to read before truncation. Default 262144 (256 KB), max 1048576 (1 MB).',
|
|
312
|
+
},
|
|
313
|
+
headers: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
description: 'Extra request headers. Authorization / Cookie / X-Auth-* blocked unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1.',
|
|
316
|
+
},
|
|
317
|
+
body: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Request body for POST. Ignored for GET / HEAD.',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
required: ['url'],
|
|
323
|
+
},
|
|
324
|
+
handler: async (input) => {
|
|
325
|
+
return await httpFetchExecute(input);
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
//# sourceMappingURL=http-fetch-tools.js.map
|
|
@@ -26,4 +26,8 @@ export { guidanceTools } from './guidance-tools.js';
|
|
|
26
26
|
export { autopilotTools } from './autopilot-tools.js';
|
|
27
27
|
export { metaharnessTools } from './metaharness-tools.js';
|
|
28
28
|
export { testgenTools } from './testgen-tools.js';
|
|
29
|
+
export { agenticowTools } from './agenticow-tools.js';
|
|
30
|
+
export { agentbbsTools } from './agentbbs-tools.js';
|
|
31
|
+
export { businessPodTools } from './business-pod-tools.js';
|
|
32
|
+
export { httpFetchTools } from './http-fetch-tools.js';
|
|
29
33
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -27,4 +27,12 @@ export { autopilotTools } from './autopilot-tools.js';
|
|
|
27
27
|
export { metaharnessTools } from './metaharness-tools.js';
|
|
28
28
|
// ADR-175-inspired — Test-Driven Repair via headless `claude -p`
|
|
29
29
|
export { testgenTools } from './testgen-tools.js';
|
|
30
|
+
// agenticow@~0.2.3 — Copy-On-Write memory branching (162-byte branches)
|
|
31
|
+
export { agenticowTools } from './agenticow-tools.js';
|
|
32
|
+
// ADR-164 — AgentBBS federated business-domain BBS rooms (Phase 1)
|
|
33
|
+
export { agentbbsTools } from './agentbbs-tools.js';
|
|
34
|
+
// ADR-164 Phase 2 — Business-pod template validation
|
|
35
|
+
export { businessPodTools } from './business-pod-tools.js';
|
|
36
|
+
// ADR-164 Phase 4 §5.1.8 — http_fetch (secure-by-default HTTP probe)
|
|
37
|
+
export { httpFetchTools } from './http-fetch-tools.js';
|
|
30
38
|
//# sourceMappingURL=index.js.map
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* - metaharness_security_bench upstream's "Darwin Shield" (their own ADR-155)
|
|
19
19
|
* - metaharness_bench create/verify bench suites used by evolve --bench
|
|
20
20
|
*
|
|
21
|
+
* @metaharness/redblue integration adds one tool that targets the standalone
|
|
22
|
+
* `@metaharness/redblue` npm package — adversarial red/blue LLM testing
|
|
23
|
+
* for the agents and apps you own:
|
|
24
|
+
*
|
|
25
|
+
* - metaharness_redblue red-team → judge → blue-patch → retest → report
|
|
26
|
+
*
|
|
21
27
|
* Every tool resolves the corresponding plugin script
|
|
22
28
|
* (`plugins/ruflo-metaharness/scripts/<X>.mjs`) via the same locator
|
|
23
29
|
* the commands/metaharness.ts dispatcher uses, then spawns it with
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* - metaharness_security_bench upstream's "Darwin Shield" (their own ADR-155)
|
|
19
19
|
* - metaharness_bench create/verify bench suites used by evolve --bench
|
|
20
20
|
*
|
|
21
|
+
* @metaharness/redblue integration adds one tool that targets the standalone
|
|
22
|
+
* `@metaharness/redblue` npm package — adversarial red/blue LLM testing
|
|
23
|
+
* for the agents and apps you own:
|
|
24
|
+
*
|
|
25
|
+
* - metaharness_redblue red-team → judge → blue-patch → retest → report
|
|
26
|
+
*
|
|
21
27
|
* Every tool resolves the corresponding plugin script
|
|
22
28
|
* (`plugins/ruflo-metaharness/scripts/<X>.mjs`) via the same locator
|
|
23
29
|
* the commands/metaharness.ts dispatcher uses, then spawns it with
|
|
@@ -42,8 +48,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
42
48
|
/**
|
|
43
49
|
* Walk up from this module to find plugins/ruflo-metaharness/scripts/.
|
|
44
50
|
* Handles three install layouts (mirrors commands/metaharness.ts).
|
|
51
|
+
*
|
|
52
|
+
* `requiredScript`, if provided, narrows the match the same way the
|
|
53
|
+
* commands dispatcher does — guards against the publish-artifact mirror
|
|
54
|
+
* (`v3/@claude-flow/cli/plugins/ruflo-metaharness/scripts/`, regenerated
|
|
55
|
+
* by `prepublishOnly`) shadowing the source when it's stale on a new
|
|
56
|
+
* script.
|
|
45
57
|
*/
|
|
46
|
-
function locatePluginScripts() {
|
|
58
|
+
function locatePluginScripts(requiredScript) {
|
|
47
59
|
const candidates = [];
|
|
48
60
|
let p = resolve(__dirname);
|
|
49
61
|
for (let i = 0; i < 8; i++) {
|
|
@@ -55,8 +67,11 @@ function locatePluginScripts() {
|
|
|
55
67
|
candidates.push(join(cwd, 'plugins', 'ruflo-metaharness', 'scripts'));
|
|
56
68
|
candidates.push(join(cwd, 'node_modules', '@claude-flow', 'cli', 'plugins', 'ruflo-metaharness', 'scripts'));
|
|
57
69
|
for (const c of candidates) {
|
|
58
|
-
if (existsSync(join(c, '_harness.mjs')))
|
|
59
|
-
|
|
70
|
+
if (!existsSync(join(c, '_harness.mjs')))
|
|
71
|
+
continue;
|
|
72
|
+
if (requiredScript && !existsSync(join(c, requiredScript)))
|
|
73
|
+
continue;
|
|
74
|
+
return c;
|
|
60
75
|
}
|
|
61
76
|
return null;
|
|
62
77
|
}
|
|
@@ -90,7 +105,7 @@ function locatePluginScripts() {
|
|
|
90
105
|
*/
|
|
91
106
|
function runScript(scriptName, args) {
|
|
92
107
|
return new Promise((resolve) => {
|
|
93
|
-
const dir = locatePluginScripts();
|
|
108
|
+
const dir = locatePluginScripts(scriptName);
|
|
94
109
|
if (!dir) {
|
|
95
110
|
resolve({
|
|
96
111
|
exitCode: 0, stdout: '', json: { degraded: true, reason: 'plugin-not-found' },
|
|
@@ -512,5 +527,69 @@ export const metaharnessTools = [
|
|
|
512
527
|
return { success: r.success, data: r.json, degraded: r.degraded, exitCode: r.exitCode };
|
|
513
528
|
},
|
|
514
529
|
},
|
|
530
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
531
|
+
// @metaharness/redblue integration (1 tool).
|
|
532
|
+
// Backed by `@metaharness/redblue@~0.1.1`. Plugin script shells out via
|
|
533
|
+
// _redblue.mjs. Same {success, data, degraded, exitCode} contract.
|
|
534
|
+
//
|
|
535
|
+
// SAFETY: redblue itself enforces hard boundaries (no real creds, no live
|
|
536
|
+
// targets, no shell, no arbitrary network, no eval) in upstream's
|
|
537
|
+
// src/config/safety.ts at config-load time. The wrapper does NOT relax
|
|
538
|
+
// those — it only forwards argv with shell:false. `--mock-judge` is the
|
|
539
|
+
// $0 marker-fixture CI path; the real model judge requires
|
|
540
|
+
// $OPENROUTER_API_KEY which we never inject.
|
|
541
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
542
|
+
{
|
|
543
|
+
name: 'metaharness_redblue',
|
|
544
|
+
description: 'Adversarial red/blue LLM testing via @metaharness/redblue — generates attacks across OWASP LLM Top-10 / NIST AI RMF families (prompt injection, tool misuse, data leakage, jailbreaks, denial-of-wallet), runs them against an LLM target YOU OWN, judges compromise, optionally applies declarative blue-team patches, retests, and emits a board-readable report with measured failure reduction. Use when shipping an LLM-powered product and you need a repeatable security gate before exposing it to users — eyeballing prompts is wrong because attack surface coverage requires the OWASP/NIST taxonomy and the judge has to be model-driven for jailbreak detection. SAFETY: upstream hard-enforces no-creds / no-live-targets / no-shell / no-network / no-eval at config-load time; cannot be relaxed via flags. For CI / offline use --mockJudge=true ($0 marker fixture). For real model judging set $OPENROUTER_API_KEY and accept the per-run cost capped by max_cost_usd (default $3). ' + MCP_SUCCESS_SEMANTIC,
|
|
545
|
+
category: 'metaharness',
|
|
546
|
+
inputSchema: {
|
|
547
|
+
type: 'object',
|
|
548
|
+
properties: {
|
|
549
|
+
subcommand: {
|
|
550
|
+
type: 'string',
|
|
551
|
+
enum: ['init', 'run', 'patch', 'attack', 'report'],
|
|
552
|
+
description: 'init = scaffold redblue.yaml; run = baseline (+ optional --patch); patch = baseline → patch → retest delta; attack = preview attacks; report = render existing report.json',
|
|
553
|
+
default: 'run',
|
|
554
|
+
},
|
|
555
|
+
config: { type: 'string', description: 'Path to redblue.yaml (default: ./redblue.yaml)' },
|
|
556
|
+
out: { type: 'string', description: 'Output report path for run/patch (default: temp file we read back inline)' },
|
|
557
|
+
in: { type: 'string', description: 'Input report path for `report` subcommand' },
|
|
558
|
+
tests: { type: 'number', description: 'How many test cases (run/patch only)' },
|
|
559
|
+
patch: { type: 'boolean', description: '`run` only — after baseline, apply blue-team patches and retest', default: false },
|
|
560
|
+
mockJudge: { type: 'boolean', description: '$0 TEST-ONLY marker fixture (no model calls). Use for CI / offline. Real judging requires OPENROUTER_API_KEY.', default: false },
|
|
561
|
+
family: { type: 'string', enum: ['prompt', 'tools', 'data', 'all'], description: '`attack` subcommand only — which attack family to preview' },
|
|
562
|
+
count: { type: 'number', description: '`attack` only — how many cases to preview' },
|
|
563
|
+
alertOnFail: { type: 'boolean', description: 'Exit 1 when post-patch verdict is FAIL (gate-style)', default: false },
|
|
564
|
+
timeoutMs: { type: 'number', description: 'Subprocess hard timeout (default 120000; mock-judge runs complete in seconds)' },
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
handler: async (input) => {
|
|
568
|
+
const args = [String(input.subcommand ?? 'run')];
|
|
569
|
+
// attack family is positional
|
|
570
|
+
if (input.subcommand === 'attack' && input.family)
|
|
571
|
+
args.push(String(input.family));
|
|
572
|
+
if (input.config)
|
|
573
|
+
args.push('--config', String(input.config));
|
|
574
|
+
if (input.out)
|
|
575
|
+
args.push('--out', String(input.out));
|
|
576
|
+
if (input.in)
|
|
577
|
+
args.push('--in', String(input.in));
|
|
578
|
+
if (input.tests !== undefined)
|
|
579
|
+
args.push('--tests', String(input.tests));
|
|
580
|
+
if (input.patch === true)
|
|
581
|
+
args.push('--patch');
|
|
582
|
+
if (input.mockJudge === true)
|
|
583
|
+
args.push('--mock-judge');
|
|
584
|
+
if (input.count !== undefined)
|
|
585
|
+
args.push('--count', String(input.count));
|
|
586
|
+
if (input.alertOnFail === true)
|
|
587
|
+
args.push('--alert-on-fail');
|
|
588
|
+
if (input.timeoutMs !== undefined)
|
|
589
|
+
args.push('--timeout-ms', String(input.timeoutMs));
|
|
590
|
+
const r = await runScript('redblue.mjs', args);
|
|
591
|
+
return { success: r.success, data: r.json, degraded: r.degraded, exitCode: r.exitCode };
|
|
592
|
+
},
|
|
593
|
+
},
|
|
515
594
|
];
|
|
516
595
|
//# sourceMappingURL=metaharness-tools.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -106,6 +106,8 @@
|
|
|
106
106
|
"yaml": "^2.8.0"
|
|
107
107
|
},
|
|
108
108
|
"optionalDependencies": {
|
|
109
|
+
"agentbbs": "~0.1.0",
|
|
110
|
+
"agenticow": "~0.2.3",
|
|
109
111
|
"@claude-flow/aidefence": "^3.0.2",
|
|
110
112
|
"@claude-flow/codex": "^3.0.0-alpha.8",
|
|
111
113
|
"@claude-flow/embeddings": "^3.0.0-alpha.18",
|
|
@@ -115,6 +117,7 @@
|
|
|
115
117
|
"@claude-flow/security": "^3.0.0-alpha.10",
|
|
116
118
|
"@metaharness/darwin": "~0.3.1",
|
|
117
119
|
"@metaharness/kernel": "~0.1.0",
|
|
120
|
+
"@metaharness/redblue": "~0.1.1",
|
|
118
121
|
"@metaharness/router": "~0.3.2",
|
|
119
122
|
"metaharness": "~0.2.6",
|
|
120
123
|
"@ruvector/attention": "^0.1.32",
|