@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.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/bin/cli.js +1 -1
- package/bin/mcp-server.js +1 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/autopilot.js +2 -2
- package/dist/src/commands/daemon.js +21 -1
- package/dist/src/commands/hive-mind.js +7 -0
- package/dist/src/commands/mcp.js +35 -1
- package/dist/src/commands/policy.js +10 -2
- package/dist/src/mcp-server.d.ts +33 -0
- package/dist/src/mcp-server.js +96 -4
- package/dist/src/mcp-tools/hive-mind-tools.d.ts +2 -4
- package/dist/src/mcp-tools/hive-mind-tools.js +214 -40
- package/dist/src/missions/schemas.d.ts +125 -125
- package/dist/src/mods/command-registry/schema.d.ts +199 -199
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +64 -13
- package/dist/src/production/retry.js +1 -1
- package/dist/src/services/bounded-worker-pool.js +3 -0
- package/dist/src/services/config-file-manager.d.ts +3 -0
- package/dist/src/services/config-file-manager.js +24 -7
- package/dist/src/services/policy-ledger-anchor.d.ts +42 -0
- package/dist/src/services/policy-ledger-anchor.js +163 -0
- package/dist/src/services/policy-runtime.d.ts +11 -5
- package/dist/src/services/policy-runtime.js +70 -15
- package/dist/src/services/worker-daemon.d.ts +1 -0
- package/dist/src/services/worker-daemon.js +12 -0
- package/dist/src/services/worker-queue.js +11 -2
- package/node_modules/@claude-flow/mcp/dist/connection-pool.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js +5 -2
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts +7 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.js +60 -47
- package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +3 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js +4 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +11 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +19 -3
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +4 -2
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +7 -7
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +80 -7
- package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
- package/plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs +141 -0
- 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
|
-
*
|
|
89
|
-
*
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
126
|
+
catch {
|
|
127
|
+
// unreadable file == no secret configured
|
|
99
128
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
110
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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'
|
|
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 =
|
|
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: '
|
|
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'
|
|
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 =
|
|
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: '
|
|
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 =
|
|
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) {
|