@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
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Tool definitions for collective intelligence and swarm coordination.
5
5
  */
6
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, unlinkSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
- import { randomBytes, timingSafeEqual } from 'node:crypto';
8
+ import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
9
9
  import { getProjectCwd } from './types.js';
10
10
  import { validateIdentifier, validateText } from './validate-input.js';
11
11
  // Storage paths
@@ -84,32 +84,98 @@ function tryResolveProposal(proposal, totalNodes) {
84
84
  }
85
85
  return null;
86
86
  }
87
+ const LOCAL_TRANSPORTS = new Set(['stdio', 'cli']);
88
+ const MIN_ENV_SECRET_LENGTH = 16;
89
+ const BOOTSTRAP_FILE = 'bootstrap.secret';
90
+ function classifyCaller(context) {
91
+ if (process.env.RUFLO_HIVE_REQUIRE_AUTH === '1')
92
+ return 'remote';
93
+ if (context === undefined || context === null)
94
+ return 'local';
95
+ const transport = context.transport;
96
+ return typeof transport === 'string' && LOCAL_TRANSPORTS.has(transport) ? 'local' : 'remote';
97
+ }
87
98
  /**
88
- * Verify a caller-supplied hiveToken against the one minted by hive-mind_init,
89
- * using a constant-time comparison (bearer-token capability check -- callers
90
- * that never joined, and callers that guess/omit the token, get identical
91
- * rejection). Returns an error string on failure, or null on success.
99
+ * Constant-time string equality. Both sides are hashed first so the compare is
100
+ * over fixed-length digests: no early exit and no length side channel.
92
101
  */
93
- function requireHiveToken(state, suppliedToken) {
94
- if (!state.hiveToken) {
95
- return 'Hive-mind has no capability token minted (re-run hive-mind_init)';
102
+ function constantTimeEqual(expected, supplied) {
103
+ const a = createHash('sha256').update(expected, 'utf-8').digest();
104
+ const b = createHash('sha256').update(supplied, 'utf-8').digest();
105
+ return timingSafeEqual(a, b);
106
+ }
107
+ function getBootstrapSecretPath() {
108
+ return join(getHiveDir(), BOOTSTRAP_FILE);
109
+ }
110
+ /**
111
+ * Operator secret: env `RUFLO_HIVE_BOOTSTRAP_SECRET` wins (>= 16 chars), else
112
+ * the 0600 file. Read-only: never creates anything.
113
+ */
114
+ function readBootstrapSecret() {
115
+ const fromEnv = process.env.RUFLO_HIVE_BOOTSTRAP_SECRET?.trim();
116
+ if (fromEnv && fromEnv.length >= MIN_ENV_SECRET_LENGTH)
117
+ return fromEnv;
118
+ try {
119
+ const path = getBootstrapSecretPath();
120
+ if (existsSync(path)) {
121
+ const fromFile = readFileSync(path, 'utf-8').trim();
122
+ if (fromFile)
123
+ return fromFile;
124
+ }
96
125
  }
97
- if (typeof suppliedToken !== 'string' || !suppliedToken) {
98
- return 'hiveToken is required';
126
+ catch {
127
+ // unreadable file == no secret configured
99
128
  }
100
- const expected = Buffer.from(state.hiveToken, 'utf-8');
101
- const actual = Buffer.from(suppliedToken, 'utf-8');
102
- if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
103
- return 'Invalid hiveToken';
129
+ return undefined;
130
+ }
131
+ /**
132
+ * Create the 0600 secret file if neither env nor file provides one. Only local
133
+ * callers ever reach this (hive-mind_init, local path). Returns nothing: the
134
+ * value is never handed to a caller.
135
+ */
136
+ function ensureBootstrapSecret() {
137
+ if (readBootstrapSecret())
138
+ return;
139
+ ensureHiveDir();
140
+ try {
141
+ writeFileSync(getBootstrapSecretPath(), randomBytes(32).toString('hex'), {
142
+ encoding: 'utf-8',
143
+ mode: 0o600,
144
+ flag: 'wx', // never clobber a secret another process just created
145
+ });
104
146
  }
105
- return null;
147
+ catch (err) {
148
+ if (err.code !== 'EEXIST')
149
+ throw err;
150
+ }
151
+ }
152
+ /**
153
+ * Authorize a caller for a gated hive-mind operation. Returns an error string
154
+ * on denial, null on success. Denial text never contains either credential.
155
+ */
156
+ function authorizeHive(state, input, context, opts = {}) {
157
+ if (classifyCaller(context) === 'local')
158
+ return null;
159
+ const secret = readBootstrapSecret();
160
+ const suppliedSecret = typeof input.bootstrapSecret === 'string' ? input.bootstrapSecret : '';
161
+ if (secret && suppliedSecret && constantTimeEqual(secret, suppliedSecret))
162
+ return null;
163
+ const allowToken = opts.allowToken !== false;
164
+ const suppliedToken = typeof input.hiveToken === 'string' ? input.hiveToken : '';
165
+ if (allowToken && state?.hiveToken && suppliedToken && constantTimeEqual(state.hiveToken, suppliedToken)) {
166
+ return null;
167
+ }
168
+ if (!suppliedSecret && !suppliedToken) {
169
+ return allowToken
170
+ ? 'hiveToken is required (remote callers must present the operator credential: hiveToken or bootstrapSecret; local stdio/CLI callers are exempt; see ADR-476)'
171
+ : 'bootstrapSecret is required (remote callers must present the operator secret; set RUFLO_HIVE_BOOTSTRAP_SECRET on the server; see ADR-476)';
172
+ }
173
+ return suppliedSecret ? 'Invalid bootstrapSecret' : 'Invalid hiveToken';
106
174
  }
107
175
  /**
108
176
  * Read the current hiveToken directly off disk, for same-machine callers
109
- * that already have filesystem access to hive state (the CLI's own
110
- * `hive-mind join/leave/consensus` subcommands) -- NOT exposed over any MCP
111
- * tool response (in particular, not hive-mind_status), since that's a
112
- * remote-reachable surface a capability token must not leak through.
177
+ * (the CLI's own `hive-mind join/leave/consensus` subcommands) -- NOT exposed
178
+ * over any MCP tool response.
113
179
  */
114
180
  export function getHiveTokenForCli() {
115
181
  return loadHiveState().hiveToken;
@@ -123,8 +189,14 @@ function getHivePath() {
123
189
  function ensureHiveDir() {
124
190
  const dir = getHiveDir();
125
191
  if (!existsSync(dir)) {
126
- mkdirSync(dir, { recursive: true });
192
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
193
+ }
194
+ // The directory holds the capability token and operator secret: owner-only.
195
+ // Best effort (no-op on Windows, may fail on a directory we do not own).
196
+ try {
197
+ chmodSync(dir, 0o700);
127
198
  }
199
+ catch { /* not ours / unsupported */ }
128
200
  }
129
201
  function loadHiveState() {
130
202
  try {
@@ -150,7 +222,26 @@ function loadHiveState() {
150
222
  function saveHiveState(state) {
151
223
  ensureHiveDir();
152
224
  state.updatedAt = new Date().toISOString();
153
- writeFileSync(getHivePath(), JSON.stringify(state, null, 2), 'utf-8');
225
+ // state.json embeds hiveToken: write it owner-only (0600) and atomically
226
+ // (temp file in the same directory, then rename) so a reader never sees a
227
+ // half-written file and the token is never briefly world-readable.
228
+ const path = getHivePath();
229
+ const tmp = `${path}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
230
+ try {
231
+ writeFileSync(tmp, JSON.stringify(state, null, 2), { encoding: 'utf-8', mode: 0o600, flag: 'wx' });
232
+ try {
233
+ chmodSync(tmp, 0o600);
234
+ }
235
+ catch { /* unsupported */ }
236
+ renameSync(tmp, path);
237
+ }
238
+ catch (err) {
239
+ try {
240
+ unlinkSync(tmp);
241
+ }
242
+ catch { /* already gone */ }
243
+ throw err;
244
+ }
154
245
  }
155
246
  // Import agent store helpers for spawn functionality
156
247
  import { existsSync as agentStoreExists, readFileSync as readAgentStore, writeFileSync as writeAgentStore, mkdirSync as mkdirAgentStore } from 'node:fs';
@@ -183,13 +274,25 @@ export const hiveMindTools = [
183
274
  role: { type: 'string', enum: ['worker', 'specialist', 'scout'], description: 'Worker role in hive', default: 'worker' },
184
275
  agentType: { type: 'string', description: 'Agent type for spawned workers', default: 'worker' },
185
276
  prefix: { type: 'string', description: 'Prefix for worker IDs', default: 'hive-worker' },
277
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
278
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
186
279
  },
187
280
  },
188
- handler: async (input) => {
281
+ handler: async (input, context) => {
189
282
  const state = loadHiveState();
190
283
  if (!state.initialized) {
191
284
  return { success: false, error: 'Hive-mind not initialized. Run hive-mind/init first.' };
192
285
  }
286
+ // Fail-closed: spawned agents are pushed straight into state.workers,
287
+ // which hive-mind_consensus's vote tally treats as legitimate voting
288
+ // roster members (`state.workers.includes(voterId)`). Without this
289
+ // gate, an unauthenticated caller could mint arbitrary "workers" here
290
+ // and then vote as them -- bypassing #3291's join/leave/vote Sybil
291
+ // fix entirely via this sibling tool. No token, no spawn.
292
+ const tokenError = authorizeHive(state, input, context);
293
+ if (tokenError) {
294
+ return { success: false, error: tokenError };
295
+ }
193
296
  if (input.agentType) {
194
297
  const v = validateIdentifier(input.agentType, 'agentType');
195
298
  if (!v.valid)
@@ -258,9 +361,22 @@ export const hiveMindTools = [
258
361
  description: 'Consensus strategy. Default: raft (anti-drift). Use byzantine for f<n/3 fault tolerance.',
259
362
  },
260
363
  queenId: { type: 'string', description: 'Initial queen agent ID' },
364
+ bootstrapSecret: { type: 'string', description: 'Operator secret (RUFLO_HIVE_BOOTSTRAP_SECRET or .claude-flow/hive-mind/bootstrap.secret). Only needed by remote/HTTP callers; local stdio/CLI callers omit it. See ADR-476.' },
261
365
  },
262
366
  },
263
- handler: async (input) => {
367
+ handler: async (input, context) => {
368
+ // ADR-476: init is the credential-issuance point, so it is gated too
369
+ // (first-time bootstrap and re-init alike). hiveToken is not accepted
370
+ // here: only the operator secret may (re)initialize a hive remotely.
371
+ const bootstrapError = authorizeHive(undefined, input, context, { allowToken: false });
372
+ if (bootstrapError) {
373
+ return { success: false, error: bootstrapError };
374
+ }
375
+ // Local first init materialises the 0600 operator secret so a later
376
+ // remote/HTTP deployment has something to be told out of band. The
377
+ // value is never returned.
378
+ if (classifyCaller(context) === 'local')
379
+ ensureBootstrapSecret();
264
380
  if (input.queenId) {
265
381
  const v = validateIdentifier(input.queenId, 'queenId');
266
382
  if (!v.valid)
@@ -295,7 +411,11 @@ export const hiveMindTools = [
295
411
  consensus: state.consensusStrategy,
296
412
  queenId,
297
413
  status: 'initialized',
298
- hiveToken: state.hiveToken,
414
+ // Deliberately NOT echoing state.hiveToken here (even though this
415
+ // call is itself now bootstrap-secret-gated): the CLI never reads
416
+ // it off this response (it uses getHiveTokenForCli()'s direct
417
+ // same-machine state.json read instead), so there's no reason to
418
+ // widen the token's exposure onto an MCP tool response at all.
299
419
  config: {
300
420
  topology: state.topology,
301
421
  consensus: state.consensusStrategy,
@@ -415,11 +535,12 @@ export const hiveMindTools = [
415
535
  properties: {
416
536
  agentId: { type: 'string', description: 'Agent ID to join' },
417
537
  role: { type: 'string', enum: ['worker', 'specialist', 'scout'], description: 'Agent role in hive' },
418
- hiveToken: { type: 'string', description: 'Capability token minted by hive-mind_init' },
538
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
539
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
419
540
  },
420
- required: ['agentId', 'hiveToken'],
541
+ required: ['agentId'],
421
542
  },
422
- handler: async (input) => {
543
+ handler: async (input, context) => {
423
544
  const state = loadHiveState();
424
545
  const agentId = input.agentId;
425
546
  {
@@ -433,7 +554,7 @@ export const hiveMindTools = [
433
554
  // Fail-closed: an unrecognized/missing token makes no membership
434
555
  // change at all -- state.workers is untouched, not just left
435
556
  // unsaved (the write below never happens on this path).
436
- const tokenError = requireHiveToken(state, input.hiveToken);
557
+ const tokenError = authorizeHive(state, input, context);
437
558
  if (tokenError) {
438
559
  return { success: false, agentId, error: tokenError };
439
560
  }
@@ -458,11 +579,12 @@ export const hiveMindTools = [
458
579
  type: 'object',
459
580
  properties: {
460
581
  agentId: { type: 'string', description: 'Agent ID to remove' },
461
- hiveToken: { type: 'string', description: 'Capability token minted by hive-mind_init' },
582
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
583
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
462
584
  },
463
- required: ['agentId', 'hiveToken'],
585
+ required: ['agentId'],
464
586
  },
465
- handler: async (input) => {
587
+ handler: async (input, context) => {
466
588
  const state = loadHiveState();
467
589
  const agentId = input.agentId;
468
590
  {
@@ -472,7 +594,7 @@ export const hiveMindTools = [
472
594
  }
473
595
  // Fail-closed: a denied caller makes no membership change -- the
474
596
  // splice/save below is unreachable on this path.
475
- const tokenError = requireHiveToken(state, input.hiveToken);
597
+ const tokenError = authorizeHive(state, input, context);
476
598
  if (tokenError) {
477
599
  return { success: false, agentId, error: tokenError };
478
600
  }
@@ -503,7 +625,8 @@ export const hiveMindTools = [
503
625
  value: { description: 'Proposal value (for propose)' },
504
626
  vote: { type: 'boolean', description: 'Vote (true=for, false=against)' },
505
627
  voterId: { type: 'string', description: 'Voter agent ID' },
506
- hiveToken: { type: 'string', description: 'Capability token minted by hive-mind_init (required to vote)' },
628
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
629
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
507
630
  strategy: { type: 'string', enum: ['bft', 'raft', 'quorum'], description: 'Consensus strategy (default: raft)' },
508
631
  quorumPreset: { type: 'string', enum: ['unanimous', 'majority', 'supermajority'], description: 'Quorum threshold preset (for quorum strategy, default: majority)' },
509
632
  term: { type: 'number', description: 'Term number (for raft strategy)' },
@@ -511,7 +634,7 @@ export const hiveMindTools = [
511
634
  },
512
635
  required: ['action'],
513
636
  },
514
- handler: async (input) => {
637
+ handler: async (input, context) => {
515
638
  if (input.proposalId) {
516
639
  const v = validateIdentifier(input.proposalId, 'proposalId');
517
640
  if (!v.valid)
@@ -532,6 +655,15 @@ export const hiveMindTools = [
532
655
  const strategy = input.strategy || 'raft';
533
656
  const totalNodes = state.workers.length || 1;
534
657
  if (action === 'propose') {
658
+ // Fail-closed: an unauthenticated caller could otherwise inject
659
+ // arbitrary consensus proposals (type/value of their choosing) for
660
+ // legitimate workers to vote on, or exhaust raft's one-pending-
661
+ // proposal-per-term slot as a denial-of-service. No token, no
662
+ // proposal recorded.
663
+ const proposeTokenError = authorizeHive(state, input, context);
664
+ if (proposeTokenError) {
665
+ return { action, error: proposeTokenError };
666
+ }
535
667
  const proposalId = `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
536
668
  const quorumPreset = input.quorumPreset || 'majority';
537
669
  const term = input.term || (state.queen?.term ?? 1);
@@ -590,7 +722,7 @@ export const hiveMindTools = [
590
722
  // Fail-closed: a denied caller records no vote at all -- the
591
723
  // votes[voterId] write below is unreachable on this path, and
592
724
  // nothing about the proposal (vote tallies, status) changes.
593
- const tokenError = requireHiveToken(state, input.hiveToken);
725
+ const tokenError = authorizeHive(state, input, context);
594
726
  if (tokenError) {
595
727
  return { action, error: tokenError, proposalId: proposal.proposalId };
596
728
  }
@@ -809,14 +941,23 @@ export const hiveMindTools = [
809
941
  message: { type: 'string', description: 'Message to broadcast' },
810
942
  priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'], description: 'Message priority' },
811
943
  fromId: { type: 'string', description: 'Sender agent ID' },
944
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
945
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
812
946
  },
813
947
  required: ['message'],
814
948
  },
815
- handler: async (input) => {
949
+ handler: async (input, context) => {
816
950
  const state = loadHiveState();
817
951
  if (!state.initialized) {
818
952
  return { success: false, error: 'Hive-mind not initialized' };
819
953
  }
954
+ // Fail-closed: no token, no message stored -- otherwise any caller
955
+ // could inject spoofed broadcasts (arbitrary fromId) into shared
956
+ // memory that every real worker reads.
957
+ const tokenError = authorizeHive(state, input, context);
958
+ if (tokenError) {
959
+ return { success: false, error: tokenError };
960
+ }
820
961
  {
821
962
  const v = validateText(input.message, 'message');
822
963
  if (!v.valid)
@@ -858,13 +999,22 @@ export const hiveMindTools = [
858
999
  properties: {
859
1000
  graceful: { type: 'boolean', description: 'Graceful shutdown (wait for pending tasks)', default: true },
860
1001
  force: { type: 'boolean', description: 'Force immediate shutdown', default: false },
1002
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
1003
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
861
1004
  },
862
1005
  },
863
- handler: async (input) => {
1006
+ handler: async (input, context) => {
864
1007
  const state = loadHiveState();
865
1008
  if (!state.initialized) {
866
1009
  return { success: false, error: 'Hive-mind not initialized or already shut down' };
867
1010
  }
1011
+ // Fail-closed: no token, no shutdown -- otherwise any caller could
1012
+ // terminate a running hive (wiping workers/pending consensus/shared
1013
+ // memory) as a denial-of-service with zero proof of membership.
1014
+ const tokenError = authorizeHive(state, input, context);
1015
+ if (tokenError) {
1016
+ return { success: false, error: tokenError };
1017
+ }
868
1018
  const graceful = input.graceful !== false;
869
1019
  const force = input.force === true;
870
1020
  const workerCount = state.workers.length;
@@ -917,10 +1067,12 @@ export const hiveMindTools = [
917
1067
  action: { type: 'string', enum: ['get', 'set', 'delete', 'list'], description: 'Memory action' },
918
1068
  key: { type: 'string', description: 'Memory key' },
919
1069
  value: { description: 'Value to store (for set)' },
1070
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
1071
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
920
1072
  },
921
1073
  required: ['action'],
922
1074
  },
923
- handler: async (input) => {
1075
+ handler: async (input, context) => {
924
1076
  if (input.key) {
925
1077
  const v = validateIdentifier(input.key, 'key');
926
1078
  if (!v.valid)
@@ -942,6 +1094,14 @@ export const hiveMindTools = [
942
1094
  if (action === 'set') {
943
1095
  if (!key)
944
1096
  return { action, error: 'Key required' };
1097
+ // Fail-closed: no token, no write -- otherwise any caller could
1098
+ // tamper with or inject entries into shared memory every worker
1099
+ // and the queen read, the same attack class hive-mind_broadcast
1100
+ // was gated for.
1101
+ const tokenError = authorizeHive(state, input, context);
1102
+ if (tokenError) {
1103
+ return { action, key, error: tokenError };
1104
+ }
945
1105
  state.sharedMemory[key] = input.value;
946
1106
  saveHiveState(state);
947
1107
  // Also store in AgentDB for searchable hive memory
@@ -964,6 +1124,11 @@ export const hiveMindTools = [
964
1124
  if (action === 'delete') {
965
1125
  if (!key)
966
1126
  return { action, error: 'Key required' };
1127
+ // Fail-closed, same reasoning as 'set' above.
1128
+ const tokenError = authorizeHive(state, input, context);
1129
+ if (tokenError) {
1130
+ return { action, key, error: tokenError };
1131
+ }
967
1132
  const existed = key in state.sharedMemory;
968
1133
  delete state.sharedMemory[key];
969
1134
  saveHiveState(state);
@@ -996,13 +1161,22 @@ export const hiveMindTools = [
996
1161
  type: 'object',
997
1162
  properties: {
998
1163
  qualityThreshold: { type: 'number', description: 'Quality threshold for pattern retention (advisory — not enforced yet)' },
1164
+ hiveToken: { type: 'string', description: 'Remote/HTTP callers only: hive capability token (read from the server, never returned by tools). Local stdio/CLI callers omit it. See ADR-476.' },
1165
+ bootstrapSecret: { type: 'string', description: 'Remote/HTTP callers only: operator secret (alternative to hiveToken). See ADR-476.' },
999
1166
  },
1000
1167
  },
1001
- handler: async () => {
1168
+ handler: async (input, context) => {
1002
1169
  const t0 = Date.now();
1003
1170
  const state = loadHiveState();
1004
1171
  if (!state.initialized)
1005
1172
  return { optimized: false, error: 'Hive-mind not initialized', before: { patterns: 0, memory: '0' }, after: { patterns: 0, memory: '0' }, removed: 0, consolidated: 0, timeMs: 0 };
1173
+ // Fail-closed: no token, no prune -- this mutates state.sharedMemory
1174
+ // via the same saveHiveState() path set/delete/broadcast were gated
1175
+ // for (found in round 2 review, #3339).
1176
+ const tokenError = authorizeHive(state, input, context);
1177
+ if (tokenError) {
1178
+ return { optimized: false, error: tokenError, before: { patterns: 0, memory: '0' }, after: { patterns: 0, memory: '0' }, removed: 0, consolidated: 0, timeMs: 0 };
1179
+ }
1006
1180
  const beforeKeys = Object.keys(state.sharedMemory);
1007
1181
  const before = beforeKeys.length;
1008
1182
  for (const k of beforeKeys) {