@claude-flow/cli 3.54.1 → 3.55.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.
Files changed (60) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/bin/cli.js +1 -1
  3. package/bin/mcp-server.js +1 -1
  4. package/catalog-manifest.json +2 -2
  5. package/dist/src/commands/autopilot.js +2 -2
  6. package/dist/src/commands/daemon.js +21 -1
  7. package/dist/src/commands/hive-mind.js +7 -0
  8. package/dist/src/commands/mcp.js +35 -1
  9. package/dist/src/commands/policy.js +10 -2
  10. package/dist/src/mcp-server.d.ts +33 -0
  11. package/dist/src/mcp-server.js +96 -4
  12. package/dist/src/mcp-tools/hive-mind-tools.d.ts +2 -4
  13. package/dist/src/mcp-tools/hive-mind-tools.js +214 -40
  14. package/dist/src/missions/schemas.d.ts +125 -125
  15. package/dist/src/mods/command-registry/schema.d.ts +199 -199
  16. package/dist/src/parser.d.ts +3 -0
  17. package/dist/src/parser.js +64 -13
  18. package/dist/src/production/retry.js +1 -1
  19. package/dist/src/services/bounded-worker-pool.js +3 -0
  20. package/dist/src/services/config-file-manager.d.ts +3 -0
  21. package/dist/src/services/config-file-manager.js +24 -7
  22. package/dist/src/services/policy-ledger-anchor.d.ts +42 -0
  23. package/dist/src/services/policy-ledger-anchor.js +163 -0
  24. package/dist/src/services/policy-runtime.d.ts +11 -5
  25. package/dist/src/services/policy-runtime.js +70 -15
  26. package/dist/src/services/worker-daemon.d.ts +1 -0
  27. package/dist/src/services/worker-daemon.js +12 -0
  28. package/dist/src/services/worker-queue.js +11 -2
  29. package/node_modules/@claude-flow/mcp/dist/connection-pool.d.ts.map +1 -1
  30. package/node_modules/@claude-flow/mcp/dist/connection-pool.js +5 -2
  31. package/node_modules/@claude-flow/mcp/dist/connection-pool.js.map +1 -1
  32. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts +7 -1
  33. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
  34. package/node_modules/@claude-flow/mcp/dist/transport/http.js +60 -47
  35. package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
  36. package/node_modules/@claude-flow/mcp/package.json +1 -1
  37. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
  38. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
  39. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  40. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +3 -0
  41. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts.map +1 -1
  42. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js +4 -1
  43. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js.map +1 -1
  44. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
  45. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +1 -0
  46. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
  47. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
  48. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +11 -1
  49. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  50. package/node_modules/@claude-flow/security/dist/policy/engine.js +19 -3
  51. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  52. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +4 -2
  53. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  54. package/node_modules/@claude-flow/security/package.json +1 -1
  55. package/package.json +7 -7
  56. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  57. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +80 -7
  58. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
  59. package/plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs +141 -0
  60. package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +5 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.54.1",
3
+ "version": "3.55.0",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "c8d218526a3c34034c2a69de23f507f0eb0f70d59c16426a8edc849e6ac22529",
6
6
  "hook-handler.cjs": "34d2aacfb59a73ff94b1467bb57ab9d9c4af89c0e470aadf3bd0d74a503a658d",
@@ -9,6 +9,6 @@
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "GORi4ozu4COfL048CD+GWNMiX4hdXVrn2/tvRXWcj9OioRiUknKlfhNwkL6PVtcNar+5xDn7J9BzGFBrPgblCA==",
12
+ "signature": "FT6oLMAU2RXu8iAdlOFTNGcjYZ52pkhrGFgn3/oKJdtVab0etCzO24Nr2ut1Hr9kn3tRAqb6D8MZwKfcoSqlCQ==",
13
13
  "algorithm": "ed25519"
14
14
  }
package/bin/cli.js CHANGED
@@ -301,7 +301,7 @@ if (isMCPMode) {
301
301
  }
302
302
 
303
303
  try {
304
- const result = await callMCPTool(toolName, toolParams, { sessionId });
304
+ const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio' });
305
305
  // Failed task outcomes are data; an explicit handler error is a tool failure.
306
306
  const isError = result !== null && typeof result === 'object'
307
307
  && typeof result.error === 'string' && result.error.trim().length > 0;
package/bin/mcp-server.js CHANGED
@@ -190,7 +190,7 @@ async function handleMessage(message) {
190
190
  }
191
191
 
192
192
  try {
193
- const result = await callMCPTool(toolName, toolParams, { sessionId });
193
+ const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio' });
194
194
  return {
195
195
  jsonrpc: '2.0',
196
196
  id: message.id,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-07T07:34:48.002Z",
5
- "gitSha": "b9ce891c",
4
+ "generatedAt": "2026-10-07T17:32:59.615Z",
5
+ "gitSha": "2ab3f99f",
6
6
  "catalog": {
7
7
  "agents": 176,
8
8
  "tools": 423,
@@ -181,8 +181,8 @@ const configCommand = {
181
181
  name: 'config',
182
182
  description: 'Configure max iterations, timeout, and task sources',
183
183
  options: [
184
- { name: 'max-iterations', type: 'string', description: 'Max re-engagement iterations (1-1000)' },
185
- { name: 'timeout', type: 'string', description: 'Timeout in minutes (1-1440)' },
184
+ { name: 'max-iterations', type: 'number', description: 'Max re-engagement iterations (1-1000)' },
185
+ { name: 'timeout', type: 'number', description: 'Timeout in minutes (1-1440)' },
186
186
  { name: 'task-sources', type: 'string', description: `Comma-separated task sources: ${[...VALID_TASK_SOURCES].join(', ')}` },
187
187
  ],
188
188
  action: async (ctx) => {
@@ -10,6 +10,11 @@ import { fileURLToPath } from 'url';
10
10
  import { dirname, join, resolve } from 'path';
11
11
  import { homedir } from 'os';
12
12
  import * as fs from 'fs';
13
+ // #1968 / #3547: shared validator for `--workers <list>`, used both when
14
+ // reading the flag into DaemonConfig (below) and when forwarding it to the
15
+ // forked background child's argv (startBackgroundDaemon). One pattern, one
16
+ // place, so the two paths can't drift out of sync on what they accept.
17
+ const WORKERS_RE = /^[a-z][a-z0-9_-]*(,[a-z][a-z0-9_-]*)*$/;
13
18
  // Start daemon subcommand
14
19
  const startCommand = {
15
20
  name: 'start',
@@ -119,6 +124,22 @@ const startCommand = {
119
124
  output.printWarning(`Ignoring invalid --ttl value: ${sanitize(rawTtl)}`);
120
125
  }
121
126
  }
127
+ // #3547 (#1968 follow-up): `--workers` was forwarded to the forked
128
+ // background child's argv (below, in startBackgroundDaemon) but never
129
+ // read into DaemonConfig here, so WorkerDaemon always fell back to
130
+ // DEFAULT_WORKERS (7 enabled) regardless of what was requested — in
131
+ // both foreground mode (this same action, run directly) and background
132
+ // mode (this same action, re-run in the forked child with --foreground
133
+ // and the forwarded --workers flag).
134
+ const rawWorkers = ctx.flags.workers;
135
+ if (typeof rawWorkers === 'string' && rawWorkers.length > 0) {
136
+ if (WORKERS_RE.test(rawWorkers)) {
137
+ config.enabledWorkers = rawWorkers.split(',');
138
+ }
139
+ else if (!quiet) {
140
+ output.printWarning(`Ignoring invalid --workers value: ${sanitize(rawWorkers)}`);
141
+ }
142
+ }
122
143
  // Check if background daemon already running (skip if we ARE the daemon process)
123
144
  //
124
145
  // #2407 — without an atomic lockfile, N concurrent `daemon start` calls
@@ -518,7 +539,6 @@ async function startBackgroundDaemon(projectRoot, quiet, forwarded = {}) {
518
539
  // through — argv goes straight to a forked process so reject anything
519
540
  // that doesn't look like a comma-separated worker-name list or one of
520
541
  // the allowed sandbox modes.
521
- const WORKERS_RE = /^[a-z][a-z0-9_-]*(,[a-z][a-z0-9_-]*)*$/;
522
542
  if (typeof workers === 'string' && workers.length > 0 && WORKERS_RE.test(workers)) {
523
543
  forkArgs.push('--workers', workers);
524
544
  }
@@ -1016,6 +1016,13 @@ const consensusCommand = {
1016
1016
  const action = ctx.flags.action || 'list';
1017
1017
  try {
1018
1018
  const result = await callMCPTool('hive-mind_consensus', { action, proposalId: ctx.flags.proposalId, type: ctx.flags.type, value: ctx.flags.value, vote: ctx.flags.vote === 'yes', voterId: ctx.flags.voterId, hiveToken: getHiveTokenForCli() });
1019
+ if (result.error) {
1020
+ if (ctx.flags.format === 'json')
1021
+ output.printJson(result);
1022
+ else
1023
+ output.printError(`Consensus error: ${String(result.error)}`);
1024
+ return { success: false, exitCode: 1, data: result };
1025
+ }
1019
1026
  if (ctx.flags.format === 'json') {
1020
1027
  output.printJson(result);
1021
1028
  return { success: true, data: result };
@@ -8,7 +8,7 @@
8
8
  import { output } from '../output.js';
9
9
  import { confirm } from '../prompt.js';
10
10
  import { installParentDeathWatchdog } from '../runtime/parent-death-watchdog.js';
11
- import { getServerManager, getMCPServerStatus, filterAdvertisedMcpTools, parseMcpToolSelection, } from '../mcp-server.js';
11
+ import { getServerManager, getMCPServerStatus, resolveMcpHttpAuthToken, isLoopbackHost, filterAdvertisedMcpTools, parseMcpToolSelection, } from '../mcp-server.js';
12
12
  import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js';
13
13
  // MCP tools categories
14
14
  const TOOL_CATEGORIES = [
@@ -71,6 +71,16 @@ const startCommand = {
71
71
  description: 'HTTP request timeout in milliseconds (default 30000; env RUFLO_MCP_REQUEST_TIMEOUT_MS)',
72
72
  type: 'number'
73
73
  },
74
+ {
75
+ name: 'auth-token-file',
76
+ description: 'Read the HTTP bearer token from this file (http transport). Preferred over --auth-token; env RUFLO_MCP_HTTP_TOKEN also works',
77
+ type: 'string'
78
+ },
79
+ {
80
+ name: 'auth-token',
81
+ description: 'HTTP bearer token (16+ printable ASCII chars). Visible in argv/ps - prefer RUFLO_MCP_HTTP_TOKEN or --auth-token-file',
82
+ type: 'string'
83
+ },
74
84
  {
75
85
  name: 'daemon',
76
86
  short: 'd',
@@ -90,6 +100,8 @@ const startCommand = {
90
100
  { command: 'claude-flow mcp start', description: 'Start with defaults (stdio)' },
91
101
  { command: 'claude-flow mcp start -p 8080 -t http', description: 'Start HTTP server' },
92
102
  { command: 'claude-flow mcp start -t http --request-timeout-ms 120000', description: 'Allow slower cold-start HTTP tools' },
103
+ { command: 'RUFLO_MCP_HTTP_TOKEN=<16+ chars> claude-flow mcp start -t http', description: 'HTTP server requiring "Authorization: Bearer <token>" on every request (except GET /health)' },
104
+ { command: 'RUFLO_MCP_HTTP_TOKEN=<16+ chars> claude-flow mcp start -t http --host 0.0.0.0', description: 'Non-loopback bind: allowed only with a token (or RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1)' },
93
105
  { command: 'claude-flow mcp start -d', description: 'Start as daemon' },
94
106
  { command: 'claude-flow mcp start -f', description: 'Force restart (kill existing)' }
95
107
  ],
@@ -109,6 +121,17 @@ const startCommand = {
109
121
  output.printError('Request timeout must be an integer from 1 to 3600000 milliseconds');
110
122
  return { success: false, exitCode: 1 };
111
123
  }
124
+ let authToken;
125
+ try {
126
+ authToken = resolveMcpHttpAuthToken({
127
+ token: (ctx.flags.authToken ?? ctx.flags['auth-token']),
128
+ tokenFile: (ctx.flags.authTokenFile ?? ctx.flags['auth-token-file']),
129
+ });
130
+ }
131
+ catch (error) {
132
+ output.printError(error.message);
133
+ return { success: false, exitCode: 1 };
134
+ }
112
135
  const daemon = ctx.flags.daemon ?? false;
113
136
  const force = ctx.flags.force ?? false;
114
137
  output.writeln();
@@ -166,6 +189,7 @@ const startCommand = {
166
189
  port,
167
190
  tools: !tools || tools === 'all' ? 'all' : tools.split(','),
168
191
  ...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
192
+ ...(authToken ? { authToken } : {}),
169
193
  daemonize: daemon,
170
194
  };
171
195
  try {
@@ -218,6 +242,15 @@ const startCommand = {
218
242
  if (transport === 'http') {
219
243
  output.writeln(output.dim(` Health: http://${host}:${port}/health`));
220
244
  output.writeln(output.dim(` RPC: http://${host}:${port}/rpc`));
245
+ if (authToken) {
246
+ output.writeln(output.dim(' Auth: bearer token required (Authorization: Bearer <token>); /health is public'));
247
+ }
248
+ else if (isLoopbackHost(host)) {
249
+ output.printWarning('No auth token set: any local process (and DNS-rebinding web pages) can call tools. Set RUFLO_MCP_HTTP_TOKEN to require a bearer token.');
250
+ }
251
+ else {
252
+ output.printWarning('Running UNAUTHENTICATED on a non-loopback host (RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP is set).');
253
+ }
221
254
  }
222
255
  else if (transport === 'websocket') {
223
256
  output.writeln(output.dim(` WebSocket: ws://${host}:${port}/ws`));
@@ -596,6 +629,7 @@ const execCommand = {
596
629
  const startTime = performance.now();
597
630
  const result = await callMCPTool(tool, params, {
598
631
  sessionId: `cli-${Date.now().toString(36)}`,
632
+ transport: 'cli',
599
633
  requestId: `exec-${Date.now()}`,
600
634
  });
601
635
  const duration = performance.now() - startTime;
@@ -1,3 +1,4 @@
1
+ import { anchorPosture } from '../services/policy-ledger-anchor.js';
1
2
  import { output } from '../output.js';
2
3
  import { autoMigratePolicyStateIfNeeded, evaluatePolicyRequest, loadPolicyState, revokePolicyApproval, setPolicyBudget, setPolicyMode, upsertPolicyRule, verifyPolicyLedger, } from '../services/policy-runtime.js';
3
4
  function argJson(value, label) {
@@ -19,12 +20,16 @@ function requireInteractiveAdministrator() {
19
20
  throw new Error('policy administration requires an interactive local terminal');
20
21
  }
21
22
  }
23
+ // #3602: say what verify can and cannot prove.
24
+ const LEDGER_TRUST_SCOPE = 'detects edits confined to state.json or to the project directory; '
25
+ + 'does not defend against an attacker who can rewrite both the project directory and ~/.config/ruflo (ADR-475)';
22
26
  export const policyCommand = {
23
27
  name: 'policy',
24
28
  description: 'Agentic policy engine — evaluate actions, manage rules/approvals, and verify the decision ledger (ADR-324)',
25
29
  options: [
26
30
  { name: 'mode', type: 'string', description: 'Policy mode: legacy | observe | enforce' },
27
31
  { name: 'project-root', type: 'string', description: 'Project root containing .claude-flow/policy' },
32
+ { name: 'establish-anchor', type: 'boolean', description: 'verify: explicitly anchor a ledger that has receipts but no anchor (interactive, logged with user and time)' },
28
33
  ],
29
34
  async action(context) {
30
35
  const args = context.args ?? [];
@@ -87,8 +92,11 @@ export const policyCommand = {
87
92
  return print({ receipts: state.receipts });
88
93
  }
89
94
  if (operation === 'verify') {
90
- const ledger = await verifyPolicyLedger(root);
91
- const result = print(ledger);
95
+ const establishAnchor = flags['establish-anchor'] === true || flags.establishAnchor === true;
96
+ if (establishAnchor)
97
+ requireInteractiveAdministrator();
98
+ const ledger = await verifyPolicyLedger(root, { establishAnchor });
99
+ const result = print({ ...ledger, anchors: anchorPosture(root), scope: LEDGER_TRUST_SCOPE });
92
100
  return ledger.valid ? result : { ...result, success: false, exitCode: 1 };
93
101
  }
94
102
  throw new Error(`unknown policy operation: ${operation}`);
@@ -30,6 +30,12 @@ export interface MCPServerOptions {
30
30
  daemonize?: boolean;
31
31
  timeout?: number;
32
32
  requestTimeoutMs?: number;
33
+ /**
34
+ * Bearer token every HTTP request must present (`Authorization: Bearer <token>`).
35
+ * http transport only. Prefer RUFLO_MCP_HTTP_TOKEN / a token file over this
36
+ * field's CLI flag so the secret stays out of argv. Never logged or emitted.
37
+ */
38
+ authToken?: string;
33
39
  }
34
40
  /**
35
41
  * MCP Server status
@@ -49,6 +55,33 @@ export interface MCPServerStatus {
49
55
  metrics?: Record<string, number>;
50
56
  };
51
57
  }
58
+ /** Exported for tests — see LOOPBACK_HOSTS. */
59
+ export declare function isLoopbackHost(host: string): boolean;
60
+ /**
61
+ * Explicit, documented opt-out of the non-loopback authorization gate in
62
+ * startHttpServer(). Exported for tests.
63
+ */
64
+ export declare function isUnauthenticatedHttpAllowed(env?: NodeJS.ProcessEnv): boolean;
65
+ /**
66
+ * The full startHttpServer() authorization gate, as a pure function:
67
+ * true means "refuse to start" (see the thrown error for why).
68
+ *
69
+ * A non-loopback bind is allowed when requests are authenticated (a bearer
70
+ * token is configured on the http transport) or the operator explicitly opts
71
+ * out with RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1. Loopback binds are never
72
+ * refused, with or without a token. Exported for tests.
73
+ */
74
+ export declare function shouldRefuseUnauthenticatedHttp(host: string, env?: NodeJS.ProcessEnv, authenticated?: boolean): boolean;
75
+ /**
76
+ * Resolve the HTTP bearer token. Precedence: explicit value (CLI flag), token
77
+ * file, then RUFLO_MCP_HTTP_TOKEN. An unset/empty env var means "no token";
78
+ * a token that is set but malformed throws, so a typo can never silently
79
+ * start the server unauthenticated. Exported for tests.
80
+ */
81
+ export declare function resolveMcpHttpAuthToken(input?: {
82
+ token?: string;
83
+ tokenFile?: string;
84
+ }, env?: NodeJS.ProcessEnv, readFile?: (p: string) => string): string | undefined;
52
85
  export declare function parseMcpToolSelection(value: string | undefined): string[] | 'all';
53
86
  /**
54
87
  * Apply the existing `--tools` contract to advertised schemas. A selector can
@@ -104,8 +104,72 @@ function processStartToken(pid) {
104
104
  }
105
105
  }
106
106
  /**
107
- * Default configuration
107
+ * Hosts treated as same-machine-only for MCP HTTP tool-call authorization
108
+ * purposes (see startHttpServer's non-loopback authorization gate below).
109
+ * Deliberately exact-string matching only (no DNS resolution) so anything
110
+ * not spelled exactly one of these three fails closed rather than open.
108
111
  */
112
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
113
+ /** Exported for tests — see LOOPBACK_HOSTS. */
114
+ export function isLoopbackHost(host) {
115
+ return LOOPBACK_HOSTS.has(host);
116
+ }
117
+ /**
118
+ * Explicit, documented opt-out of the non-loopback authorization gate in
119
+ * startHttpServer(). Exported for tests.
120
+ */
121
+ export function isUnauthenticatedHttpAllowed(env = process.env) {
122
+ const v = env.RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP;
123
+ return v === '1' || v === 'true';
124
+ }
125
+ /**
126
+ * The full startHttpServer() authorization gate, as a pure function:
127
+ * true means "refuse to start" (see the thrown error for why).
128
+ *
129
+ * A non-loopback bind is allowed when requests are authenticated (a bearer
130
+ * token is configured on the http transport) or the operator explicitly opts
131
+ * out with RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1. Loopback binds are never
132
+ * refused, with or without a token. Exported for tests.
133
+ */
134
+ export function shouldRefuseUnauthenticatedHttp(host, env = process.env, authenticated = false) {
135
+ return !isLoopbackHost(host) && !authenticated && !isUnauthenticatedHttpAllowed(env);
136
+ }
137
+ /** Printable ASCII without space (RFC 6750 token-ish), 16-512 chars. */
138
+ const AUTH_TOKEN_PATTERN = /^[\x21-\x7e]{16,512}$/;
139
+ /**
140
+ * Resolve the HTTP bearer token. Precedence: explicit value (CLI flag), token
141
+ * file, then RUFLO_MCP_HTTP_TOKEN. An unset/empty env var means "no token";
142
+ * a token that is set but malformed throws, so a typo can never silently
143
+ * start the server unauthenticated. Exported for tests.
144
+ */
145
+ export function resolveMcpHttpAuthToken(input = {}, env = process.env, readFile = (p) => fs.readFileSync(p, 'utf8')) {
146
+ let raw;
147
+ let source = '';
148
+ if (input.token !== undefined) {
149
+ raw = input.token;
150
+ source = '--auth-token';
151
+ }
152
+ else if (input.tokenFile !== undefined) {
153
+ source = '--auth-token-file';
154
+ try {
155
+ raw = readFile(input.tokenFile).replace(/\r?\n$/, '');
156
+ }
157
+ catch {
158
+ throw new Error('Could not read the MCP HTTP auth token file given to --auth-token-file');
159
+ }
160
+ }
161
+ else if (env.RUFLO_MCP_HTTP_TOKEN !== undefined && env.RUFLO_MCP_HTTP_TOKEN !== '') {
162
+ raw = env.RUFLO_MCP_HTTP_TOKEN;
163
+ source = 'RUFLO_MCP_HTTP_TOKEN';
164
+ }
165
+ if (raw === undefined)
166
+ return undefined;
167
+ if (!AUTH_TOKEN_PATTERN.test(raw)) {
168
+ // The value is deliberately not echoed.
169
+ throw new Error(`Invalid MCP HTTP auth token from ${source}: it must be 16-512 printable ASCII characters with no spaces`);
170
+ }
171
+ return raw;
172
+ }
109
173
  const DEFAULT_OPTIONS = {
110
174
  transport: 'stdio',
111
175
  host: 'localhost',
@@ -239,7 +303,7 @@ export class MCPServerManager extends EventEmitter {
239
303
  }
240
304
  const startTime = performance.now();
241
305
  this.startTime = new Date();
242
- this.emit('starting', { options: this.options });
306
+ this.emit('starting', { options: { ...this.options, authToken: this.options.authToken ? '[redacted]' : undefined } });
243
307
  try {
244
308
  if (this.options.transport === 'stdio') {
245
309
  // For stdio transport, spawn the server process
@@ -671,7 +735,7 @@ export class MCPServerManager extends EventEmitter {
671
735
  }
672
736
  }
673
737
  try {
674
- const result = await callMCPTool(toolName, toolParams, { sessionId });
738
+ const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio' });
675
739
  const isError = hasToolError(result);
676
740
  trackRequest(toolName, !isError);
677
741
  return {
@@ -725,6 +789,31 @@ export class MCPServerManager extends EventEmitter {
725
789
  * Start HTTP server in-process
726
790
  */
727
791
  async startHttpServer() {
792
+ // Authorization gate first, before any import or bind: a refused start must
793
+ // not depend on (or touch) anything else.
794
+ const authToken = this.options.authToken || undefined;
795
+ if (authToken && this.options.transport !== 'http') {
796
+ // The standalone websocket transport has no working token check, so a
797
+ // token there would be a false promise of protection.
798
+ throw new Error('An MCP HTTP auth token is only supported with --transport http');
799
+ }
800
+ if (shouldRefuseUnauthenticatedHttp(this.options.host, process.env, !!authToken)) {
801
+ // No ToolAuthorizer is wired below: @claude-flow/mcp's tool-call
802
+ // authorization is fully opt-in (requireToolAuthorization/toolAuthorizer),
803
+ // so without this check every registered tool (memory_*, hooks_*,
804
+ // agentdb_*, hive-mind_*, ...) would be callable by any client that can
805
+ // reach this host:port, unauthenticated — the same shape as
806
+ // CVE-2026-81735 (CVSS 10/10, UI-TARS-desktop mcp-http-server: optional
807
+ // auth middleware never wired by the integrating CLI + a non-loopback
808
+ // bind). Loopback stays unaffected; this only gates an explicit
809
+ // non-default `--host`. A configured bearer token (below) lifts the gate.
810
+ throw new Error(`Refusing to start the MCP HTTP server on non-loopback host "${this.options.host}": ` +
811
+ 'this server is unauthenticated unless a token is set, so every registered MCP tool ' +
812
+ 'would be reachable over the network. Set RUFLO_MCP_HTTP_TOKEN (or --auth-token-file) ' +
813
+ 'to require a bearer token, bind to a loopback host (127.0.0.1, ::1, or localhost), or set ' +
814
+ 'RUFLO_MCP_ALLOW_UNAUTHENTICATED_HTTP=1 to acknowledge the risk and proceed ' +
815
+ '(e.g. when a trusted reverse proxy or network boundary already enforces auth).');
816
+ }
728
817
  // Dynamically import the MCP server package
729
818
  // FIX for issue #942: Use proper package import instead of broken relative path
730
819
  const { createMCPServer } = await import('@claude-flow/mcp');
@@ -747,7 +836,7 @@ export class MCPServerManager extends EventEmitter {
747
836
  cacheTTL: tool.cacheTTL,
748
837
  handler: async (input, context) => {
749
838
  try {
750
- const result = await callMCPTool(tool.name, input || {}, { sessionId: context?.sessionId || fallbackSessionId });
839
+ const result = await callMCPTool(tool.name, input || {}, { sessionId: context?.sessionId || fallbackSessionId, transport: this.options.transport === 'websocket' ? 'websocket' : 'http' });
751
840
  trackRequest(tool.name, !hasToolError(result));
752
841
  return result;
753
842
  }
@@ -770,6 +859,9 @@ export class MCPServerManager extends EventEmitter {
770
859
  enableMetrics: true,
771
860
  enableCaching: true,
772
861
  requestTimeout: this.options.requestTimeoutMs,
862
+ auth: authToken
863
+ ? { enabled: true, method: 'token', tokens: [authToken] }
864
+ : undefined,
773
865
  }, logger);
774
866
  const registration = mcpServer.registerTools(cliTools);
775
867
  if (registration.failed.length > 0) {
@@ -6,10 +6,8 @@
6
6
  import { type MCPTool } from './types.js';
7
7
  /**
8
8
  * Read the current hiveToken directly off disk, for same-machine callers
9
- * that already have filesystem access to hive state (the CLI's own
10
- * `hive-mind join/leave/consensus` subcommands) -- NOT exposed over any MCP
11
- * tool response (in particular, not hive-mind_status), since that's a
12
- * remote-reachable surface a capability token must not leak through.
9
+ * (the CLI's own `hive-mind join/leave/consensus` subcommands) -- NOT exposed
10
+ * over any MCP tool response.
13
11
  */
14
12
  export declare function getHiveTokenForCli(): string | undefined;
15
13
  export declare const hiveMindTools: MCPTool[];