claude-flow 3.15.0 → 3.16.1

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,394 @@
1
+ /**
2
+ * AgentBBS MCP Tools — Federated business-domain BBS room surface (ADR-164 Phase 1).
3
+ *
4
+ * Exposes the `agentbbs@~0.1.0` (a sibling BBS-style federation peer by the same
5
+ * author as ruflo) as MCP tools so ruflo agents can register business rooms
6
+ * (#sales, #finance, #marketing, ...), publish/watch typed envelopes, and
7
+ * mint single-use human-join tokens for SSH/web cockpit access.
8
+ *
9
+ * Motivation:
10
+ * ADR-164 wires the "business autopilot" cockpit on top of the existing
11
+ * ruflo federation primitives (FederationEnvelope, PII pipeline, budget
12
+ * circuit breaker). The 4 tools here are the ruflo-side handles into the
13
+ * agentbbs Rust workspace (`crates/agentbbs-federation/` and
14
+ * `crates/agentbbs-mcp/`) that already implement Ed25519-signed envelopes
15
+ * and an MCP transport. Phase 1 ships the surface; Phases 2-5 wire deeper.
16
+ *
17
+ * Architectural constraint (mirrors metaharness-tools.ts / agenticow-tools.ts):
18
+ * - `agentbbs` lives in `optionalDependencies` — must NOT be a hard runtime dep
19
+ * - When the package is missing, every tool returns
20
+ * `{success: true, degraded: true, reason: 'agentbbs-not-found'}`
21
+ * so callers see one contract regardless of install state
22
+ * - Phase 1: polling-based watch (streaming subscriptions are Phase 4)
23
+ *
24
+ * @module @claude-flow/cli/mcp-tools/agentbbs
25
+ */
26
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
27
+ import { resolve, isAbsolute, join } from 'node:path';
28
+ import { randomBytes, createHash } from 'node:crypto';
29
+ import { getProjectCwd } from './types.js';
30
+ const PACKAGE_NAME = 'agentbbs';
31
+ // Cache: amortize dynamic-import cost across handler calls.
32
+ // null = not yet attempted; false = unavailable; module = loaded.
33
+ let _agentbbsMod = null;
34
+ let _loadAttempted = false;
35
+ async function loadAgentbbs() {
36
+ if (_loadAttempted)
37
+ return _agentbbsMod || null;
38
+ _loadAttempted = true;
39
+ try {
40
+ _agentbbsMod = await import(PACKAGE_NAME);
41
+ return _agentbbsMod;
42
+ }
43
+ catch (err) {
44
+ if (err && (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND' ||
45
+ /Cannot find (module|package)/i.test(String(err?.message)))) {
46
+ _agentbbsMod = false;
47
+ return null;
48
+ }
49
+ throw err;
50
+ }
51
+ }
52
+ function degradedResult(reason) {
53
+ return { success: true, degraded: true, reason };
54
+ }
55
+ function resolveBasePath(input) {
56
+ const p = input && typeof input === 'string' && input.length > 0
57
+ ? input
58
+ : '.agentbbs';
59
+ if (/\.\.[\\/]|\0/.test(p))
60
+ throw new Error('basePath contains disallowed characters');
61
+ const abs = isAbsolute(p) ? p : resolve(getProjectCwd(), p);
62
+ return abs;
63
+ }
64
+ function validateRoomLabel(label) {
65
+ if (!label || typeof label !== 'string')
66
+ throw new Error('roomLabel is required');
67
+ if (label.length > 128)
68
+ throw new Error('roomLabel exceeds 128 chars');
69
+ // Rooms are conventionally `#sales`, `#finance`, etc. — keep `#` in the allow-list.
70
+ if (!/^[A-Za-z0-9_.\-:/@#]+$/.test(label)) {
71
+ throw new Error('roomLabel may only contain [A-Za-z0-9_.\\-:/@#]');
72
+ }
73
+ return label;
74
+ }
75
+ function validateRoomId(roomId) {
76
+ if (!roomId || typeof roomId !== 'string')
77
+ throw new Error('roomId is required');
78
+ if (roomId.length > 128)
79
+ throw new Error('roomId exceeds 128 chars');
80
+ if (!/^[A-Za-z0-9_.\-:/@#]+$/.test(roomId)) {
81
+ throw new Error('roomId may only contain [A-Za-z0-9_.\\-:/@#]');
82
+ }
83
+ return roomId;
84
+ }
85
+ function ensureDir(dir) {
86
+ if (!existsSync(dir))
87
+ mkdirSync(dir, { recursive: true });
88
+ }
89
+ function roomIdFromLabel(label) {
90
+ // Stable, deterministic roomId — strip leading `#`, lowercase, and append a
91
+ // short hash so we don't collide across two rooms with the same canonical
92
+ // label but different policies. Phase 1: deterministic over (label).
93
+ const norm = label.replace(/^#/, '').toLowerCase();
94
+ const h = createHash('sha256').update(`agentbbs:room:${norm}`).digest('hex').slice(0, 8);
95
+ return `${norm}-${h}`;
96
+ }
97
+ function roomLogPath(basePath, roomId) {
98
+ return join(basePath, `room-${roomId}.jsonl`);
99
+ }
100
+ function roomsRegistryPath(basePath) {
101
+ return join(basePath, 'rooms.json');
102
+ }
103
+ function readEnvelopes(path) {
104
+ if (!existsSync(path))
105
+ return [];
106
+ const raw = readFileSync(path, 'utf-8');
107
+ const out = [];
108
+ for (const line of raw.split(/\r?\n/)) {
109
+ if (!line.trim())
110
+ continue;
111
+ try {
112
+ out.push(JSON.parse(line));
113
+ }
114
+ catch { /* skip malformed line */ }
115
+ }
116
+ return out;
117
+ }
118
+ function nextSeq(path) {
119
+ const env = readEnvelopes(path);
120
+ if (env.length === 0)
121
+ return 1;
122
+ return (env[env.length - 1].seq ?? env.length) + 1;
123
+ }
124
+ /**
125
+ * Ephemeral per-process Ed25519 keypair for human-join token signing.
126
+ * Phase 1 contract: keys are NOT persisted across process restart — every
127
+ * invocation can mint tokens but cross-process replay protection is the
128
+ * agentbbs server's responsibility (nonce JTI tracking, per ADR-164 §3.2.4).
129
+ * Phase 2+ will wire this to the existing federation Ed25519 keypair.
130
+ */
131
+ let _signingKey = null;
132
+ async function getSigningKey() {
133
+ if (_signingKey)
134
+ return _signingKey;
135
+ // Use @noble/ed25519 (already a hard dep of @claude-flow/cli for IPFS signing).
136
+ const ed = await import('@noble/ed25519');
137
+ const priv = ed.utils.randomPrivateKey
138
+ ? ed.utils.randomPrivateKey()
139
+ : randomBytes(32);
140
+ const pub = await (ed.getPublicKeyAsync ?? ed.getPublicKey)(priv);
141
+ _signingKey = { priv, pub };
142
+ return _signingKey;
143
+ }
144
+ function base64url(buf) {
145
+ return Buffer.from(buf).toString('base64')
146
+ .replace(/\+/g, '-')
147
+ .replace(/\//g, '_')
148
+ .replace(/=+$/, '');
149
+ }
150
+ export const agentbbsTools = [
151
+ {
152
+ name: 'federation_bbs_register',
153
+ description: 'agentbbs@~0.1.0 — Register a BBS room as a named federation peer (ADR-164 Phase 1). Maps a business-domain label like "#sales" or "#finance" to a stable roomId and emits an attested PeerHello envelope. Use when you are scaffolding the business-autopilot cockpit and need a room handle that subsequent publish/watch calls can target. Calling FederationCoordinator.joinPeer() directly is wrong because it bypasses the room policy bag (PII mode, budget cap, preferLocal routing) that the BBS plugin layers on top. Optional dep — degrades to {degraded:true} when missing.',
154
+ category: 'federation',
155
+ tags: ['agentbbs', 'federation', 'bbs', 'register', 'adr-164'],
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ basePath: {
160
+ type: 'string',
161
+ description: 'Directory where BBS room state is persisted (defaults to <cwd>/.agentbbs).',
162
+ },
163
+ roomLabel: {
164
+ type: 'string',
165
+ description: 'Human-readable room label, e.g. "#sales" or "finance". May include alnum + _.-:/@# .',
166
+ },
167
+ agentbbsBin: {
168
+ type: 'string',
169
+ description: 'Optional explicit path to the agentbbs binary. Reserved for Phase 2 wire-up; ignored in Phase 1.',
170
+ },
171
+ },
172
+ required: ['roomLabel'],
173
+ },
174
+ handler: async (input) => {
175
+ // Validate inputs FIRST so callers see deterministic errors regardless of
176
+ // optional-dep state. Degradation is a runtime-state branch, not a way
177
+ // to mask malformed requests.
178
+ const roomLabel = validateRoomLabel(String(input.roomLabel));
179
+ const basePath = resolveBasePath(input.basePath);
180
+ const api = await loadAgentbbs();
181
+ if (!api)
182
+ return degradedResult('agentbbs-not-found');
183
+ ensureDir(basePath);
184
+ const roomId = roomIdFromLabel(roomLabel);
185
+ const registryPath = roomsRegistryPath(basePath);
186
+ const registry = existsSync(registryPath) ? JSON.parse(readFileSync(registryPath, 'utf-8')) : {};
187
+ // Idempotent — re-registering the same label updates timestamp but keeps id stable.
188
+ const entry = {
189
+ roomId,
190
+ roomLabel,
191
+ registeredAt: new Date().toISOString(),
192
+ trustLevel: 'attested',
193
+ };
194
+ registry[roomId] = entry;
195
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2));
196
+ // Emit a synthetic PeerHello envelope into the room log so watchers see the join.
197
+ const logPath = roomLogPath(basePath, roomId);
198
+ const env = {
199
+ envelopeId: base64url(randomBytes(12)),
200
+ roomId,
201
+ seq: nextSeq(logPath),
202
+ msgType: 'PeerHello',
203
+ payload: { roomLabel, trustLevel: 'attested' },
204
+ timestamp: entry.registeredAt,
205
+ };
206
+ appendFileSync(logPath, JSON.stringify(env) + '\n');
207
+ // nodeId: deterministic per (cwd, roomId) so re-registers reuse identity.
208
+ const nodeId = createHash('sha256')
209
+ .update(`agentbbs:node:${basePath}:${roomId}`)
210
+ .digest('hex')
211
+ .slice(0, 16);
212
+ return {
213
+ success: true,
214
+ roomId,
215
+ nodeId,
216
+ trustLevel: 'attested',
217
+ };
218
+ },
219
+ },
220
+ {
221
+ name: 'federation_bbs_publish',
222
+ description: 'agentbbs — Publish a domain event from a pod agent to a BBS room (ADR-164 Phase 1). Wraps the payload in a ReplicateMessage envelope (envelopeId, seq, ts, msgType, payload) and appends it to the room log. Use when an agent has produced a typed event (pod-status, task-result, alert, human-override-ack, bench-result) that the human cockpit or other pods need to see. Storing into raw memory_store is wrong because it skips the room-scoped budget cap, PII pipeline gating, and monotonic seq numbering that ADR-164 §3.2.2 requires. Optional dep — degrades to {degraded:true} when missing.',
223
+ category: 'federation',
224
+ tags: ['agentbbs', 'federation', 'bbs', 'publish', 'adr-164'],
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ basePath: {
229
+ type: 'string',
230
+ description: 'Directory where BBS room state is persisted (defaults to <cwd>/.agentbbs).',
231
+ },
232
+ roomId: {
233
+ type: 'string',
234
+ description: 'Target room identifier as returned by federation_bbs_register.',
235
+ },
236
+ msgType: {
237
+ type: 'string',
238
+ description: 'Typed event kind (pod-status / task-result / alert / human-override-ack / bench-result).',
239
+ },
240
+ payload: {
241
+ type: 'object',
242
+ description: 'Event-specific JSON-serializable payload.',
243
+ },
244
+ signature: {
245
+ type: 'string',
246
+ description: 'Optional Ed25519 signature over the canonical envelope bytes. Phase 1: pass-through.',
247
+ },
248
+ },
249
+ required: ['roomId', 'msgType', 'payload'],
250
+ },
251
+ handler: async (input) => {
252
+ const basePath = resolveBasePath(input.basePath);
253
+ const roomId = validateRoomId(String(input.roomId));
254
+ const msgType = String(input.msgType ?? '');
255
+ if (!msgType)
256
+ throw new Error('msgType is required');
257
+ if (msgType.length > 64 || !/^[A-Za-z0-9_-]+$/.test(msgType)) {
258
+ throw new Error('msgType must be alnum + _ - and ≤64 chars');
259
+ }
260
+ if (typeof input.payload !== 'object' || input.payload === null) {
261
+ throw new Error('payload must be a JSON object');
262
+ }
263
+ const api = await loadAgentbbs();
264
+ if (!api)
265
+ return degradedResult('agentbbs-not-found');
266
+ ensureDir(basePath);
267
+ const logPath = roomLogPath(basePath, roomId);
268
+ const env = {
269
+ envelopeId: base64url(randomBytes(12)),
270
+ roomId,
271
+ seq: nextSeq(logPath),
272
+ msgType,
273
+ payload: input.payload,
274
+ timestamp: new Date().toISOString(),
275
+ signature: input.signature ? String(input.signature) : undefined,
276
+ };
277
+ appendFileSync(logPath, JSON.stringify(env) + '\n');
278
+ // Phase 1: recipientHopCount is always 0 (single-node). Phase 4+ will
279
+ // surface real hop counts from the WG mesh transport.
280
+ return {
281
+ success: true,
282
+ envelopeId: env.envelopeId,
283
+ recipientHopCount: 0,
284
+ };
285
+ },
286
+ },
287
+ {
288
+ name: 'federation_bbs_watch',
289
+ description: 'agentbbs — Poll recent envelopes from a BBS room (ADR-164 Phase 1). Returns envelopes newer than the optional sinceEnvelopeId, up to limit. Use when a pod agent needs to see incoming human overrides, new tasks, or peer events posted to its room. Polling memory_search for the room namespace is wrong because it loses the monotonic seq ordering and re-runs PII gating per query; this tool reads the canonical envelope log directly. Phase 1 is polling — Phase 4 layers streaming on the same surface. Optional dep — degrades to {degraded:true} when missing.',
290
+ category: 'federation',
291
+ tags: ['agentbbs', 'federation', 'bbs', 'watch', 'adr-164'],
292
+ inputSchema: {
293
+ type: 'object',
294
+ properties: {
295
+ basePath: {
296
+ type: 'string',
297
+ description: 'Directory where BBS room state is persisted (defaults to <cwd>/.agentbbs).',
298
+ },
299
+ roomId: {
300
+ type: 'string',
301
+ description: 'Room identifier to watch.',
302
+ },
303
+ sinceEnvelopeId: {
304
+ type: 'string',
305
+ description: 'Only return envelopes strictly after this id. Omit to return the most recent window.',
306
+ },
307
+ limit: {
308
+ type: 'integer',
309
+ description: 'Maximum envelopes to return (default 50, max 500).',
310
+ },
311
+ },
312
+ required: ['roomId'],
313
+ },
314
+ handler: async (input) => {
315
+ const basePath = resolveBasePath(input.basePath);
316
+ const roomId = validateRoomId(String(input.roomId));
317
+ const api = await loadAgentbbs();
318
+ if (!api)
319
+ return degradedResult('agentbbs-not-found');
320
+ const limitRaw = typeof input.limit === 'number' ? input.limit : 50;
321
+ const limit = Math.max(1, Math.min(500, Math.trunc(limitRaw)));
322
+ const sinceEnvelopeId = input.sinceEnvelopeId ? String(input.sinceEnvelopeId) : undefined;
323
+ const logPath = roomLogPath(basePath, roomId);
324
+ const all = readEnvelopes(logPath);
325
+ let slice = all;
326
+ if (sinceEnvelopeId) {
327
+ const idx = all.findIndex(e => e.envelopeId === sinceEnvelopeId);
328
+ slice = idx >= 0 ? all.slice(idx + 1) : all;
329
+ }
330
+ const envelopes = slice.slice(-limit);
331
+ return {
332
+ success: true,
333
+ roomId,
334
+ envelopes,
335
+ count: envelopes.length,
336
+ hasMore: slice.length > envelopes.length,
337
+ };
338
+ },
339
+ },
340
+ {
341
+ name: 'federation_bbs_human_join',
342
+ description: 'agentbbs — Mint a single-use Ed25519-signed token a human business owner presents to the agentbbs SSH/web front door to join a room (ADR-164 §3.2.4). Returns webUrl + sshCommand + handshakeToken + expiresAt. Use when the cockpit operator needs scoped, time-limited access to a room without sharing the federation root keypair. Issuing a permanent bearer token is wrong because the agentbbs server enforces single-use JTI replay protection and the 15-min default TTL — a long-lived token defeats both. Optional dep — degrades to {degraded:true} when missing.',
343
+ category: 'federation',
344
+ tags: ['agentbbs', 'federation', 'bbs', 'human-join', 'token', 'adr-164'],
345
+ inputSchema: {
346
+ type: 'object',
347
+ properties: {
348
+ roomId: {
349
+ type: 'string',
350
+ description: 'Room the token authorizes.',
351
+ },
352
+ ttlSeconds: {
353
+ type: 'integer',
354
+ description: 'Token lifetime in seconds. Max 900 (15 min). Default 300.',
355
+ },
356
+ },
357
+ required: ['roomId'],
358
+ },
359
+ handler: async (input) => {
360
+ const roomId = validateRoomId(String(input.roomId));
361
+ const api = await loadAgentbbs();
362
+ if (!api)
363
+ return degradedResult('agentbbs-not-found');
364
+ const ttlRaw = typeof input.ttlSeconds === 'number' ? input.ttlSeconds : 300;
365
+ const ttlSeconds = Math.max(30, Math.min(900, Math.trunc(ttlRaw)));
366
+ const now = Date.now();
367
+ const expiresAt = new Date(now + ttlSeconds * 1000).toISOString();
368
+ const nonce = base64url(randomBytes(16));
369
+ const payload = { roomId, nonce, expiresAt };
370
+ const canonical = JSON.stringify(payload);
371
+ const ed = await import('@noble/ed25519');
372
+ const { priv, pub } = await getSigningKey();
373
+ const sigBytes = await (ed.signAsync ?? ed.sign)(new TextEncoder().encode(canonical), priv);
374
+ // Token format: base64url(JSON{payload, sig, pub}). Single string, easy
375
+ // to paste into an SSH command line. The verifier reconstructs canonical
376
+ // bytes from payload and checks sig against pub.
377
+ const token = base64url(Buffer.from(JSON.stringify({
378
+ payload,
379
+ sig: base64url(sigBytes),
380
+ pub: base64url(pub),
381
+ })));
382
+ const webUrl = `https://agentbbs.local/rooms/${encodeURIComponent(roomId)}?token=${token}`;
383
+ const sshCommand = `ssh -p 2222 agentbbs.local -- join ${roomId} ${token}`;
384
+ return {
385
+ success: true,
386
+ webUrl,
387
+ sshCommand,
388
+ handshakeToken: token,
389
+ expiresAt,
390
+ };
391
+ },
392
+ },
393
+ ];
394
+ //# sourceMappingURL=agentbbs-tools.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Business-pod MCP tools — ADR-164 Phase 2 + Phase 3.
3
+ *
4
+ * Phase 2 surfaces pod-template validation (`business_pod_validate`).
5
+ * Phase 3 adds the domain-affinity routing decision (`business_pod_route_backend`)
6
+ * so any caller (agent, /loop driver, CI workflow) can ask "which backend
7
+ * should @metaharness/router prefer for this pod?" without re-implementing
8
+ * the §3.4 rules. The decision is structural and deterministic — no learned
9
+ * weights, no schedule, just (preferLocalExecution, budgetUsdMonthly).
10
+ *
11
+ * Both tools accept either a pod template *object* or a string *path* to a
12
+ * JSON file. The path form is provided so /loop drivers can avoid loading
13
+ * the template themselves; the object form is for callers that already have
14
+ * the validated template in hand.
15
+ *
16
+ * @module @claude-flow/cli/mcp-tools/business-pod
17
+ */
18
+ import type { MCPTool } from './types.js';
19
+ export declare const businessPodTools: MCPTool[];
20
+ //# sourceMappingURL=business-pod-tools.d.ts.map
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Business-pod MCP tools — ADR-164 Phase 2 + Phase 3.
3
+ *
4
+ * Phase 2 surfaces pod-template validation (`business_pod_validate`).
5
+ * Phase 3 adds the domain-affinity routing decision (`business_pod_route_backend`)
6
+ * so any caller (agent, /loop driver, CI workflow) can ask "which backend
7
+ * should @metaharness/router prefer for this pod?" without re-implementing
8
+ * the §3.4 rules. The decision is structural and deterministic — no learned
9
+ * weights, no schedule, just (preferLocalExecution, budgetUsdMonthly).
10
+ *
11
+ * Both tools accept either a pod template *object* or a string *path* to a
12
+ * JSON file. The path form is provided so /loop drivers can avoid loading
13
+ * the template themselves; the object form is for callers that already have
14
+ * the validated template in hand.
15
+ *
16
+ * @module @claude-flow/cli/mcp-tools/business-pod
17
+ */
18
+ import { readFileSync, existsSync } from 'node:fs';
19
+ import { resolve, isAbsolute } from 'node:path';
20
+ import { validatePodTemplate, PodTemplateValidationError, KNOWN_AGENT_TYPES, } from '../business-pods/pod-schema.js';
21
+ import { selectAgentBackend, } from '../business-pods/domain-affinity-policy.js';
22
+ /**
23
+ * Load a pod template from either an in-memory object or a JSON-file path.
24
+ * Returns either the parsed (unvalidated) JSON or an `error` shape suitable
25
+ * for direct return from an MCP handler.
26
+ */
27
+ function loadTemplate(input) {
28
+ if (input.podTemplate !== undefined && input.podTemplate !== null) {
29
+ if (typeof input.podTemplate !== 'object' || Array.isArray(input.podTemplate)) {
30
+ return { ok: false, error: 'podTemplate must be a JSON object', path: '/podTemplate' };
31
+ }
32
+ return { ok: true, raw: input.podTemplate };
33
+ }
34
+ if (typeof input.podTemplatePath === 'string' && input.podTemplatePath.length > 0) {
35
+ const abs = isAbsolute(input.podTemplatePath)
36
+ ? input.podTemplatePath
37
+ : resolve(process.cwd(), input.podTemplatePath);
38
+ if (!existsSync(abs)) {
39
+ return { ok: false, error: `pod template file not found: ${abs}`, path: '/podTemplatePath' };
40
+ }
41
+ try {
42
+ const raw = JSON.parse(readFileSync(abs, 'utf-8'));
43
+ return { ok: true, raw };
44
+ }
45
+ catch (e) {
46
+ const msg = e instanceof Error ? e.message : String(e);
47
+ return { ok: false, error: `failed to parse pod template JSON: ${msg}`, path: '/podTemplatePath' };
48
+ }
49
+ }
50
+ return {
51
+ ok: false,
52
+ error: 'either podTemplate (object) or podTemplatePath (string) must be provided',
53
+ path: '/',
54
+ };
55
+ }
56
+ export const businessPodTools = [
57
+ {
58
+ name: 'business_pod_validate',
59
+ description: 'ADR-164 Phase 2 — Validate a business-pod template JSON against the schema in ADR-164 §3.3 (name, agents[], allowedMcpTools, bench, piiPolicy, budgets, cronSchedule, auditReadView, reservationExpiryMs bounded by ADR-164.1 §3.2). Use when a /loop driver or CI workflow needs to pre-flight a pod template before pod-tick.mjs reaches it — surfacing validation as JSON keeps the optional-dep degraded path clean. Hand-parsing the JSON in the caller is wrong because it skips the JSON-pointer error path and the reservationExpiryMs [5000, 300000] ms bound check that ADR-164.1 mandates. Pair with business_pod_validate -> pod-tick.mjs in the sales-pod smoke contract.',
60
+ category: 'business-pods',
61
+ tags: ['business-pods', 'pod-template', 'validation', 'adr-164', 'adr-164.1'],
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ podTemplate: {
66
+ type: 'object',
67
+ description: 'The pod template object to validate. Must conform to the PodTemplate interface from ADR-164 §3.3.',
68
+ },
69
+ },
70
+ required: ['podTemplate'],
71
+ },
72
+ handler: async (input) => {
73
+ if (typeof input.podTemplate !== 'object' || input.podTemplate === null) {
74
+ return {
75
+ success: false,
76
+ valid: false,
77
+ error: 'podTemplate must be a JSON object',
78
+ path: '/',
79
+ };
80
+ }
81
+ try {
82
+ const template = validatePodTemplate(input.podTemplate);
83
+ // Lightweight agent-type sanity check — surface unknown types as a
84
+ // warning rather than a hard failure so operators can prototype with
85
+ // not-yet-registered roles. pod-tick.mjs enforces the hard check.
86
+ const unknownAgents = template.agents
87
+ .map((a) => a.agentType)
88
+ .filter((t) => !KNOWN_AGENT_TYPES.includes(t));
89
+ return {
90
+ success: true,
91
+ valid: true,
92
+ template,
93
+ warnings: unknownAgents.length > 0
94
+ ? [`unknown agent types (pod-tick.mjs will reject): ${unknownAgents.join(', ')}`]
95
+ : [],
96
+ };
97
+ }
98
+ catch (err) {
99
+ if (err instanceof PodTemplateValidationError) {
100
+ return {
101
+ success: false,
102
+ valid: false,
103
+ error: err.message,
104
+ path: err.path,
105
+ };
106
+ }
107
+ throw err;
108
+ }
109
+ },
110
+ },
111
+ {
112
+ name: 'business_pod_route_backend',
113
+ description: 'ADR-164 Phase 3 — Compute the domain-affinity routing decision for a business pod per ADR-164 §3.4 and return {backend, reason}. The three backends are local-stdio (preferLocalExecution=true), cloud-managed (preferLocalExecution=false AND budgetUsdMonthly >= 50), and remote-peer (everything else — small-budget non-local pods route through a federation peer node). Use when a /loop driver, @metaharness/router policy hook, or operator CLI needs the structural routing pick BEFORE the cost-optimal KRR step — surfacing this as an MCP tool keeps the rule auditable from the pod template alone and lets non-TS callers reach it. Re-implementing the rule in the caller is wrong because it forks the §3.4 source-of-truth and skips the {success,valid,error,path} envelope shape callers already rely on from business_pod_validate. Pair with business_pod_validate when pre-flighting a template, since this tool also runs full schema validation and degrades to the same error shape on malformed input. Threshold lives in CLOUD_BUDGET_THRESHOLD_USD in domain-affinity-policy.ts — keep that constant and this description aligned.',
114
+ category: 'business-pods',
115
+ tags: ['business-pods', 'pod-template', 'routing', 'domain-affinity', 'adr-164'],
116
+ inputSchema: {
117
+ type: 'object',
118
+ properties: {
119
+ podTemplate: {
120
+ type: 'object',
121
+ description: 'In-memory pod template object. One of podTemplate or podTemplatePath is required.',
122
+ },
123
+ podTemplatePath: {
124
+ type: 'string',
125
+ description: 'Absolute or cwd-relative path to a pod template JSON file. One of podTemplate or podTemplatePath is required.',
126
+ },
127
+ },
128
+ },
129
+ handler: async (input) => {
130
+ const loaded = loadTemplate(input);
131
+ if (!loaded.ok) {
132
+ return {
133
+ success: false,
134
+ valid: false,
135
+ error: loaded.error,
136
+ path: loaded.path,
137
+ };
138
+ }
139
+ let template;
140
+ try {
141
+ template = validatePodTemplate(loaded.raw);
142
+ }
143
+ catch (err) {
144
+ if (err instanceof PodTemplateValidationError) {
145
+ return {
146
+ success: false,
147
+ valid: false,
148
+ error: err.message,
149
+ path: err.path,
150
+ };
151
+ }
152
+ throw err;
153
+ }
154
+ const decision = selectAgentBackend(template);
155
+ return {
156
+ success: true,
157
+ valid: true,
158
+ backend: decision.backend,
159
+ reason: decision.reason,
160
+ pod: {
161
+ name: template.name,
162
+ preferLocalExecution: template.preferLocalExecution,
163
+ budgetUsdMonthly: template.budgetUsdMonthly,
164
+ },
165
+ };
166
+ },
167
+ },
168
+ ];
169
+ //# sourceMappingURL=business-pod-tools.js.map
@@ -0,0 +1,55 @@
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
+ import type { MCPTool } from './types.js';
23
+ export declare class HttpFetchValidationError extends Error {
24
+ readonly code: string;
25
+ constructor(message: string, code: string);
26
+ }
27
+ /**
28
+ * Decide whether the URL is permitted under the default secure-by-default
29
+ * allowlist. Block file://, ftp://, RFC-1918 private addresses, loopback,
30
+ * link-local — unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 is set.
31
+ */
32
+ export declare function validateUrl(rawUrl: string): URL;
33
+ export declare function validateHeaders(headers: Record<string, string>): Record<string, string>;
34
+ export interface HttpFetchResult {
35
+ success: boolean;
36
+ status: number;
37
+ statusText: string;
38
+ headers: Record<string, string>;
39
+ body: string;
40
+ bodyTruncated: boolean;
41
+ bytesRead: number;
42
+ durationMs: number;
43
+ url: string;
44
+ method: string;
45
+ error?: string;
46
+ errorCode?: string;
47
+ }
48
+ /**
49
+ * Pure execution path so tests can call it without going through the MCP
50
+ * dispatcher. Returns a result object (does not throw on validation failure
51
+ * — it returns success: false with an errorCode).
52
+ */
53
+ export declare function httpFetchExecute(input: Record<string, unknown>): Promise<HttpFetchResult>;
54
+ export declare const httpFetchTools: MCPTool[];
55
+ //# sourceMappingURL=http-fetch-tools.d.ts.map