claude-flow 3.15.0 → 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.
@@ -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
@@ -27,4 +27,7 @@ export { autopilotTools } from './autopilot-tools.js';
27
27
  export { metaharnessTools } from './metaharness-tools.js';
28
28
  export { testgenTools } from './testgen-tools.js';
29
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';
30
33
  //# sourceMappingURL=index.d.ts.map
@@ -29,4 +29,10 @@ export { metaharnessTools } from './metaharness-tools.js';
29
29
  export { testgenTools } from './testgen-tools.js';
30
30
  // agenticow@~0.2.3 — Copy-On-Write memory branching (162-byte branches)
31
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';
32
38
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.15.0",
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,7 @@
106
106
  "yaml": "^2.8.0"
107
107
  },
108
108
  "optionalDependencies": {
109
+ "agentbbs": "~0.1.0",
109
110
  "agenticow": "~0.2.3",
110
111
  "@claude-flow/aidefence": "^3.0.2",
111
112
  "@claude-flow/codex": "^3.0.0-alpha.8",