claude-flow 3.54.0 → 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/.proven-config-version +1 -0
- package/.claude/helpers/hook-handler.cjs +65 -3
- package/.claude/proven-config.json +42 -0
- 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 +5 -5
- package/v3/@claude-flow/cli/bin/cli.js +1 -1
- package/v3/@claude-flow/cli/bin/mcp-server.js +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/blocking-stdio.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/blocking-stdio.js +23 -0
- package/v3/@claude-flow/cli/dist/src/commands/autopilot.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +21 -1
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +7 -0
- package/v3/@claude-flow/cli/dist/src/commands/mcp.js +35 -1
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +10 -2
- package/v3/@claude-flow/cli/dist/src/commands/session.js +11 -37
- package/v3/@claude-flow/cli/dist/src/index.js +3 -0
- package/v3/@claude-flow/cli/dist/src/init/executor.js +1 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +65 -3
- package/v3/@claude-flow/cli/dist/src/mcp-server.d.ts +33 -0
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +96 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +13 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hive-mind-tools.d.ts +2 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hive-mind-tools.js +214 -40
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +4 -1
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +3 -2
- package/v3/@claude-flow/cli/dist/src/memory/lucene-bm25.js +11 -19
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +14 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +5 -4
- package/v3/@claude-flow/cli/dist/src/memory/shared-sqlite.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/memory/shared-sqlite.js +45 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.js +29 -1
- package/v3/@claude-flow/cli/dist/src/parser.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/parser.js +64 -13
- package/v3/@claude-flow/cli/dist/src/production/monitoring.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/production/monitoring.js +8 -3
- package/v3/@claude-flow/cli/dist/src/production/rate-limiter.js +8 -5
- package/v3/@claude-flow/cli/dist/src/production/retry.js +3 -1
- package/v3/@claude-flow/cli/dist/src/services/bounded-worker-pool.js +3 -0
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +24 -7
- package/v3/@claude-flow/cli/dist/src/services/git-workspace-identity.js +20 -3
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +42 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +163 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +11 -5
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +70 -15
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +12 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-queue.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-queue.js +20 -3
- package/v3/@claude-flow/cli/package.json +7 -7
- package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.d.ts +2 -0
- package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.js +32 -0
- package/v3/@claude-flow/shared/dist/events/event-store.js +11 -4
- package/v3/@claude-flow/shared/dist/events/event-store.test.js +20 -0
- package/v3/@claude-flow/shared/dist/mcp/connection-pool.js +5 -2
- package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.d.ts +2 -0
- package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.js +33 -0
- package/v3/@claude-flow/shared/dist/resilience/bulkhead.js +2 -1
- package/v3/@claude-flow/shared/dist/resilience/retry-timeout-cleanup.test.d.ts +2 -0
- package/v3/@claude-flow/shared/dist/resilience/retry-timeout-cleanup.test.js +38 -0
- package/v3/@claude-flow/shared/dist/resilience/retry.js +9 -2
- package/v3/@claude-flow/shared/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Second, hash-chained copy of the policy ledger anchor (#3602, ADR-475).
|
|
3
|
+
*
|
|
4
|
+
* The primary anchor (`ledgerLength` / `ledgerHead`) lives inside state.json,
|
|
5
|
+
* the same file it protects, so deleting it together with receipts used to
|
|
6
|
+
* make `verify` bless the truncated chain. This module keeps an append-only
|
|
7
|
+
* chain of anchor entries in a SEPARATE file plus a mirror of the newest entry
|
|
8
|
+
* outside the project directory (~/.config/ruflo/policy-trust/<project>/).
|
|
9
|
+
*
|
|
10
|
+
* Threat model: this detects any tampering confined to state.json, and any
|
|
11
|
+
* tampering confined to the project directory. It does NOT stop an attacker
|
|
12
|
+
* who can rewrite both the project directory and the user's home directory.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { userInfo } from 'node:os';
|
|
18
|
+
const LOG_FILE = 'ledger-anchors.json';
|
|
19
|
+
const MIRROR_FILE = 'ledger-anchor-head.json';
|
|
20
|
+
export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy', LOG_FILE);
|
|
21
|
+
function mirrorPath(projectRoot) {
|
|
22
|
+
const id = createHash('sha256').update(realpathSync(projectRoot)).digest('hex');
|
|
23
|
+
return join(userInfo().homedir, '.config', 'ruflo', 'policy-trust', id, MIRROR_FILE);
|
|
24
|
+
}
|
|
25
|
+
function entryHash(entry) {
|
|
26
|
+
return createHash('sha256').update(JSON.stringify([
|
|
27
|
+
entry.seq, entry.length, entry.headHash, entry.prevAnchorHash, entry.ts, entry.event, entry.by ?? null,
|
|
28
|
+
])).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
function validEntry(value, seq, prev) {
|
|
31
|
+
const e = value;
|
|
32
|
+
return !!e && typeof e === 'object'
|
|
33
|
+
&& e.seq === seq
|
|
34
|
+
&& Number.isInteger(e.length) && e.length >= 0
|
|
35
|
+
&& (e.headHash === null || typeof e.headHash === 'string')
|
|
36
|
+
&& e.prevAnchorHash === prev
|
|
37
|
+
&& typeof e.ts === 'number'
|
|
38
|
+
&& ['append', 'migrated-from-state', 'establish-anchor'].includes(e.event)
|
|
39
|
+
&& typeof e.hash === 'string'
|
|
40
|
+
&& e.hash === entryHash({ ...e, hash: undefined });
|
|
41
|
+
}
|
|
42
|
+
function readLog(projectRoot) {
|
|
43
|
+
const file = anchorLogPath(projectRoot);
|
|
44
|
+
if (!existsSync(file))
|
|
45
|
+
return [];
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
48
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.entries))
|
|
49
|
+
return 'corrupt';
|
|
50
|
+
let prev = null;
|
|
51
|
+
for (let i = 0; i < parsed.entries.length; i++) {
|
|
52
|
+
if (!validEntry(parsed.entries[i], i, prev))
|
|
53
|
+
return 'corrupt';
|
|
54
|
+
prev = parsed.entries[i].hash;
|
|
55
|
+
}
|
|
56
|
+
return parsed.entries;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return 'corrupt';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function readMirror(projectRoot) {
|
|
63
|
+
const file = mirrorPath(projectRoot);
|
|
64
|
+
if (!existsSync(file))
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
68
|
+
return typeof parsed?.hash === 'string' && parsed.hash === entryHash({ ...parsed, hash: undefined })
|
|
69
|
+
? parsed
|
|
70
|
+
: 'corrupt';
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return 'corrupt';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* How the stored anchors relate to `state`; read-only. `rebuild` is the
|
|
78
|
+
* explicit-establish path: an unreadable log or mirror is treated as absent, but
|
|
79
|
+
* whatever readable anchor remains must still agree with the chain, so it can
|
|
80
|
+
* never bless a truncation the anchors still remember.
|
|
81
|
+
*/
|
|
82
|
+
export function assessAnchors(projectRoot, state, rebuild = false) {
|
|
83
|
+
let entries = readLog(projectRoot);
|
|
84
|
+
if (entries === 'corrupt') {
|
|
85
|
+
if (!rebuild)
|
|
86
|
+
return { kind: 'fail', error: 'policy-anchor-log-corrupt' };
|
|
87
|
+
entries = [];
|
|
88
|
+
}
|
|
89
|
+
let mirror = readMirror(projectRoot);
|
|
90
|
+
if (mirror === 'corrupt') {
|
|
91
|
+
if (!rebuild)
|
|
92
|
+
return { kind: 'fail', error: 'policy-anchor-mirror-corrupt' };
|
|
93
|
+
mirror = null;
|
|
94
|
+
}
|
|
95
|
+
if (entries.length === 0) {
|
|
96
|
+
if (!mirror)
|
|
97
|
+
return { kind: 'absent' };
|
|
98
|
+
if (!rebuild)
|
|
99
|
+
return { kind: 'fail', error: 'policy-anchor-log-missing' };
|
|
100
|
+
if (state.receipts.length < mirror.length)
|
|
101
|
+
return { kind: 'fail', error: 'policy-ledger-truncated' };
|
|
102
|
+
if (mirror.length > 0 && state.receipts[mirror.length - 1].hash !== mirror.headHash) {
|
|
103
|
+
return { kind: 'fail', error: 'policy-ledger-anchor-mismatch' };
|
|
104
|
+
}
|
|
105
|
+
return { kind: 'absent' };
|
|
106
|
+
}
|
|
107
|
+
if (mirror && !entries.some((entry) => entry.hash === mirror.hash)) {
|
|
108
|
+
return { kind: 'fail', error: 'policy-anchor-log-rolled-back' };
|
|
109
|
+
}
|
|
110
|
+
const last = entries[entries.length - 1];
|
|
111
|
+
for (const anchor of mirror ? [last, mirror] : [last]) {
|
|
112
|
+
if (state.receipts.length < anchor.length)
|
|
113
|
+
return { kind: 'fail', error: 'policy-ledger-truncated' };
|
|
114
|
+
if (anchor.length > 0 && state.receipts[anchor.length - 1].hash !== anchor.headHash) {
|
|
115
|
+
return { kind: 'fail', error: 'policy-ledger-anchor-mismatch' };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { kind: 'ok', last, behind: state.receipts.length > last.length };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Append an entry for the current state if the newest entry does not already
|
|
122
|
+
* describe it. Must run under the policy lock. Throws if the log is corrupt:
|
|
123
|
+
* only an explicit establish may replace a corrupt log.
|
|
124
|
+
*/
|
|
125
|
+
export async function recordAnchor(projectRoot, state, write, event = 'append', by) {
|
|
126
|
+
if (state.receipts.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
const loaded = readLog(projectRoot);
|
|
129
|
+
const entries = loaded === 'corrupt' ? [] : loaded;
|
|
130
|
+
if (loaded === 'corrupt' && event !== 'establish-anchor')
|
|
131
|
+
throw new Error('policy-anchor-log-corrupt');
|
|
132
|
+
const last = entries[entries.length - 1];
|
|
133
|
+
const length = state.receipts.length;
|
|
134
|
+
const headHash = state.receipts[length - 1].hash;
|
|
135
|
+
let tip = last;
|
|
136
|
+
if (!last || last.length !== length || last.headHash !== headHash) {
|
|
137
|
+
const base = {
|
|
138
|
+
seq: entries.length,
|
|
139
|
+
length,
|
|
140
|
+
headHash,
|
|
141
|
+
prevAnchorHash: last?.hash ?? null,
|
|
142
|
+
ts: Date.now(),
|
|
143
|
+
event,
|
|
144
|
+
...(by ? { by } : {}),
|
|
145
|
+
};
|
|
146
|
+
tip = { ...base, hash: entryHash(base) };
|
|
147
|
+
await write(anchorLogPath(projectRoot), { version: 1, entries: [...entries, tip] });
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const mirror = mirrorPath(projectRoot);
|
|
151
|
+
mkdirSync(dirname(mirror), { recursive: true, mode: 0o700 });
|
|
152
|
+
await write(mirror, tip);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
// A read-only home must not stop the ledger; verify reports the weaker
|
|
156
|
+
// posture (no mirror) rather than failing closed on every decision.
|
|
157
|
+
process.stderr.write(`[policy] anchor mirror not written: ${error.message}\n`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function anchorPosture(projectRoot) {
|
|
161
|
+
return { log: existsSync(anchorLogPath(projectRoot)), mirror: existsSync(mirrorPath(projectRoot)) };
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=policy-ledger-anchor.js.map
|
|
@@ -22,13 +22,19 @@ export declare function issuePolicyApproval(approval: Omit<PolicyApproval, 'uses
|
|
|
22
22
|
issuedAt?: number;
|
|
23
23
|
}, projectRoot?: string, approvalIssuerVerifier?: (issuer: string) => boolean): Promise<PolicyApproval>;
|
|
24
24
|
export declare function revokePolicyApproval(id: string, projectRoot?: string): Promise<boolean>;
|
|
25
|
+
export type PolicyLedgerVerification = ReturnType<AgenticPolicyEngine['verifyLedger']> & {
|
|
26
|
+
/** How the second anchor was brought in line during this call. */
|
|
27
|
+
secondaryAnchor?: 'recorded-from-state' | 'restored-primary' | 'established-explicitly';
|
|
28
|
+
};
|
|
25
29
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `policy-ledger-verification-failed` and hides which check failed.
|
|
29
|
-
*
|
|
30
|
+
* Verification (#3568, #3602). It must not run inside `withPolicyTransaction`,
|
|
31
|
+
* whose own post-operation check throws a generic
|
|
32
|
+
* `policy-ledger-verification-failed` and hides which check failed. It writes
|
|
33
|
+
* only to bring the second anchor in line, or on an explicit `establishAnchor`.
|
|
30
34
|
*/
|
|
31
|
-
export declare function verifyPolicyLedger(projectRoot?: string
|
|
35
|
+
export declare function verifyPolicyLedger(projectRoot?: string, options?: {
|
|
36
|
+
establishAnchor?: boolean;
|
|
37
|
+
}): Promise<PolicyLedgerVerification>;
|
|
32
38
|
export declare function authorizeMcpTool(toolName: string, input: Record<string, unknown>, context?: Record<string, unknown>, attributes?: Readonly<{
|
|
33
39
|
actionType?: string;
|
|
34
40
|
network?: boolean;
|
|
@@ -5,6 +5,7 @@ import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync,
|
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { hostname, userInfo } from 'node:os';
|
|
7
7
|
import { syncPolicyProjection } from '../mods/policy-projection.js';
|
|
8
|
+
import { assessAnchors, recordAnchor } from './policy-ledger-anchor.js';
|
|
8
9
|
const POLICY_DIR = join('.claude-flow', 'policy');
|
|
9
10
|
const POLICY_FILE = 'state.json';
|
|
10
11
|
const LOCK_FILE = 'state.lock';
|
|
@@ -231,8 +232,11 @@ function verifyStateAnchor(projectRoot, state) {
|
|
|
231
232
|
throw new Error('policy-state-authentication-failed');
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
|
-
async function writePolicyState(projectRoot, statePath, state) {
|
|
235
|
+
async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by) {
|
|
235
236
|
await writePolicyStateFiles(projectRoot, statePath, state);
|
|
237
|
+
// #3602: the second anchor is written after the state, so a crash leaves the
|
|
238
|
+
// state AHEAD of the log (caught up on the next write), never behind it.
|
|
239
|
+
await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by);
|
|
236
240
|
// ADR-404: the ruflo mod reads Claude Code tool rules from a small
|
|
237
241
|
// projection, never from state.json. Written only after the state and its
|
|
238
242
|
// anchor are safely down; a failure here never fails the state write. A
|
|
@@ -350,20 +354,26 @@ export async function withPolicyTransaction(projectRoot, operation, options = {}
|
|
|
350
354
|
mkdirSync(target.dir, { recursive: true, mode: 0o700 });
|
|
351
355
|
const release = await acquireLock(target.lock);
|
|
352
356
|
try {
|
|
353
|
-
const
|
|
357
|
+
const loaded = loadPolicyState(projectRoot);
|
|
358
|
+
const engine = AgenticPolicyEngine.fromState(loaded, {
|
|
354
359
|
signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
|
|
355
360
|
keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
|
|
356
361
|
evidenceVerifier: verifyPolicyEvidence,
|
|
357
362
|
approvalIssuerVerifier: options.approvalIssuerVerifier,
|
|
358
363
|
});
|
|
364
|
+
// #3602: refuse to operate on a ledger whose anchors are missing or
|
|
365
|
+
// disagree; a restored primary anchor is persisted by the write below.
|
|
366
|
+
const gate = reconcileLedger(projectRoot, engine, {});
|
|
367
|
+
if (!gate.result.valid) {
|
|
368
|
+
throw new Error(gate.result.error === 'anchor-missing'
|
|
369
|
+
? `policy-ledger-anchor-missing: receipts exist but no ledger anchor was found. If this ledger predates anchors, run \`ruflo policy verify --establish-anchor\`; otherwise treat it as tampering.`
|
|
370
|
+
: gate.result.error ?? 'policy-ledger-verification-failed');
|
|
371
|
+
}
|
|
359
372
|
const result = await operation(engine);
|
|
360
|
-
// Verify before exporting: verification establishes the ledger anchor
|
|
361
|
-
// (#3568) on state written before the anchor existed, and that anchor
|
|
362
|
-
// must be part of what is persisted.
|
|
363
373
|
if (!engine.verifyLedger().valid)
|
|
364
374
|
throw new Error('policy-ledger-verification-failed');
|
|
365
375
|
const nextState = engine.exportState();
|
|
366
|
-
await writePolicyState(projectRoot, target.state, nextState);
|
|
376
|
+
await writePolicyState(projectRoot, target.state, nextState, gate.event);
|
|
367
377
|
return result;
|
|
368
378
|
}
|
|
369
379
|
finally {
|
|
@@ -416,12 +426,56 @@ export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
|
|
|
416
426
|
return withPolicyTransaction(projectRoot, (engine) => engine.revokeApproval(id));
|
|
417
427
|
}
|
|
418
428
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
429
|
+
* Decide whether the ledger may be trusted, using the chain, the primary anchor
|
|
430
|
+
* in state.json and the second anchor (#3602). It never creates an anchor over
|
|
431
|
+
* existing receipts on its own: only an authentic second anchor (restoring a
|
|
432
|
+
* deleted primary) or an explicit `establishAnchor` does.
|
|
433
|
+
*/
|
|
434
|
+
function reconcileLedger(projectRoot, engine, options) {
|
|
435
|
+
const state = engine.exportState();
|
|
436
|
+
const length = state.receipts.length;
|
|
437
|
+
const primary = typeof state.ledgerLength === 'number';
|
|
438
|
+
const base = engine.verifyLedger();
|
|
439
|
+
const fail = (error) => ({ result: { valid: false, length, error }, persist: false });
|
|
440
|
+
if (!base.valid && base.error !== 'anchor-missing')
|
|
441
|
+
return { result: base, persist: false };
|
|
442
|
+
const assessed = assessAnchors(projectRoot, state, options.establishAnchor === true);
|
|
443
|
+
if (assessed.kind === 'fail')
|
|
444
|
+
return fail(assessed.error);
|
|
445
|
+
if (primary) {
|
|
446
|
+
if (assessed.kind === 'absent' && length > 0) {
|
|
447
|
+
return options.establishAnchor
|
|
448
|
+
? { result: { ...base, secondaryAnchor: 'established-explicitly' }, persist: true, event: 'establish-anchor' }
|
|
449
|
+
: { result: { ...base, secondaryAnchor: 'recorded-from-state' }, persist: true, event: 'migrated-from-state' };
|
|
450
|
+
}
|
|
451
|
+
return { result: base, persist: assessed.kind === 'ok' && assessed.behind, event: 'append' };
|
|
452
|
+
}
|
|
453
|
+
// No primary anchor.
|
|
454
|
+
if (length === 0)
|
|
455
|
+
return { result: { valid: true, length, state: 'empty' }, persist: false };
|
|
456
|
+
if (assessed.kind === 'ok') {
|
|
457
|
+
// The second anchor is authentic and agrees with the chain: the deleted
|
|
458
|
+
// primary can be restored from it. Truncation was already rejected above.
|
|
459
|
+
engine.verifyLedger({ establishAnchor: true });
|
|
460
|
+
return { result: { valid: true, length, secondaryAnchor: 'restored-primary' }, persist: true, event: 'append' };
|
|
461
|
+
}
|
|
462
|
+
if (options.establishAnchor) {
|
|
463
|
+
const established = engine.verifyLedger({ establishAnchor: true });
|
|
464
|
+
return {
|
|
465
|
+
result: { ...established, secondaryAnchor: 'established-explicitly' },
|
|
466
|
+
persist: true,
|
|
467
|
+
event: 'establish-anchor',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return fail('anchor-missing');
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Verification (#3568, #3602). It must not run inside `withPolicyTransaction`,
|
|
474
|
+
* whose own post-operation check throws a generic
|
|
475
|
+
* `policy-ledger-verification-failed` and hides which check failed. It writes
|
|
476
|
+
* only to bring the second anchor in line, or on an explicit `establishAnchor`.
|
|
423
477
|
*/
|
|
424
|
-
export async function verifyPolicyLedger(projectRoot = process.cwd()) {
|
|
478
|
+
export async function verifyPolicyLedger(projectRoot = process.cwd(), options = {}) {
|
|
425
479
|
const target = paths(projectRoot);
|
|
426
480
|
mkdirSync(target.dir, { recursive: true, mode: 0o700 });
|
|
427
481
|
const release = await acquireLock(target.lock);
|
|
@@ -430,11 +484,12 @@ export async function verifyPolicyLedger(projectRoot = process.cwd()) {
|
|
|
430
484
|
signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
|
|
431
485
|
keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
|
|
432
486
|
});
|
|
433
|
-
const
|
|
434
|
-
if (
|
|
435
|
-
|
|
487
|
+
const outcome = reconcileLedger(projectRoot, engine, options);
|
|
488
|
+
if (outcome.persist) {
|
|
489
|
+
const by = outcome.event === 'establish-anchor' ? userInfo().username : undefined;
|
|
490
|
+
await writePolicyState(projectRoot, target.state, engine.exportState(), outcome.event, by);
|
|
436
491
|
}
|
|
437
|
-
return result;
|
|
492
|
+
return outcome.result;
|
|
438
493
|
}
|
|
439
494
|
finally {
|
|
440
495
|
release();
|
|
@@ -193,6 +193,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
193
193
|
?? fileConfig.aiWorkersEnabled
|
|
194
194
|
?? (process.env.RUFLO_DAEMON_AI_WORKERS === '1'),
|
|
195
195
|
workers: config?.workers ?? DEFAULT_WORKERS,
|
|
196
|
+
enabledWorkers: config?.enabledWorkers,
|
|
196
197
|
};
|
|
197
198
|
// #2935 — deferred from readDaemonConfigFromFile(): this.config (and
|
|
198
199
|
// therefore this.config.logDir) doesn't exist until the assignment
|
|
@@ -700,6 +701,17 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
700
701
|
// Ignore parse errors, start fresh
|
|
701
702
|
}
|
|
702
703
|
}
|
|
704
|
+
// #3547 (#1968 follow-up): an explicit `--workers` selection wins over
|
|
705
|
+
// both DEFAULT_WORKERS and whatever daemon-state.json just restored
|
|
706
|
+
// above — otherwise `daemon start --workers map,audit` always ran the
|
|
707
|
+
// full default set (or a stale saved selection) because nothing ever
|
|
708
|
+
// applied the requested list to `enabled`.
|
|
709
|
+
if (this.config.enabledWorkers) {
|
|
710
|
+
const selected = new Set(this.config.enabledWorkers);
|
|
711
|
+
for (const workerConfig of this.config.workers) {
|
|
712
|
+
workerConfig.enabled = selected.has(workerConfig.type);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
703
715
|
// Initialize any missing workers
|
|
704
716
|
for (const workerConfig of this.config.workers) {
|
|
705
717
|
if (!this.workers.has(workerConfig.type)) {
|
|
@@ -162,6 +162,7 @@ export class WorkerQueue extends EventEmitter {
|
|
|
162
162
|
workerId;
|
|
163
163
|
heartbeatTimer;
|
|
164
164
|
processingTasks = new Set();
|
|
165
|
+
retryTimers = new Set();
|
|
165
166
|
isShuttingDown = false;
|
|
166
167
|
maxConcurrent = 1;
|
|
167
168
|
initialized = false;
|
|
@@ -232,8 +233,9 @@ export class WorkerQueue extends EventEmitter {
|
|
|
232
233
|
// Check queues in priority order
|
|
233
234
|
for (const workerType of workerTypes) {
|
|
234
235
|
const queueName = this.getQueueName(workerType);
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
let taskId;
|
|
237
|
+
// Cancelled or otherwise stale entries do not exhaust this worker type.
|
|
238
|
+
while ((taskId = this.store.popFromQueue(queueName)) !== null) {
|
|
237
239
|
const task = this.store.getTask(taskId);
|
|
238
240
|
if (task && task.status === 'pending') {
|
|
239
241
|
task.status = 'processing';
|
|
@@ -257,6 +259,10 @@ export class WorkerQueue extends EventEmitter {
|
|
|
257
259
|
this.emit('warning', { message: `Task ${taskId} not found for completion` });
|
|
258
260
|
return;
|
|
259
261
|
}
|
|
262
|
+
// Late or duplicate outcomes must not rewrite a settled task.
|
|
263
|
+
if (['completed', 'failed', 'timeout', 'cancelled'].includes(task.status)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
260
266
|
task.status = 'completed';
|
|
261
267
|
task.completedAt = new Date();
|
|
262
268
|
task.result = result;
|
|
@@ -275,6 +281,10 @@ export class WorkerQueue extends EventEmitter {
|
|
|
275
281
|
this.emit('warning', { message: `Task ${taskId} not found for failure` });
|
|
276
282
|
return;
|
|
277
283
|
}
|
|
284
|
+
// Late or duplicate outcomes must not rewrite a settled task.
|
|
285
|
+
if (['completed', 'failed', 'timeout', 'cancelled'].includes(task.status)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
278
288
|
this.processingTasks.delete(taskId);
|
|
279
289
|
// Check if we should retry
|
|
280
290
|
if (retryable && task.retryCount < task.maxRetries) {
|
|
@@ -286,10 +296,13 @@ export class WorkerQueue extends EventEmitter {
|
|
|
286
296
|
this.store.setTask(taskId, task);
|
|
287
297
|
// Re-queue with delay (exponential backoff)
|
|
288
298
|
const delay = Math.min(30000, 1000 * Math.pow(2, task.retryCount));
|
|
289
|
-
|
|
299
|
+
// Tracked so shutdown() can release it
|
|
300
|
+
const timer = setTimeout(() => {
|
|
301
|
+
this.retryTimers.delete(timer);
|
|
290
302
|
const queueName = this.getQueueName(task.workerType);
|
|
291
303
|
this.store.pushToQueue(queueName, taskId, PRIORITY_SCORES[task.priority]);
|
|
292
304
|
}, delay);
|
|
305
|
+
this.retryTimers.add(timer);
|
|
293
306
|
this.emit('taskRetrying', { taskId, retryCount: task.retryCount, delay });
|
|
294
307
|
}
|
|
295
308
|
else {
|
|
@@ -469,6 +482,10 @@ export class WorkerQueue extends EventEmitter {
|
|
|
469
482
|
for (const taskId of this.processingTasks) {
|
|
470
483
|
await this.fail(taskId, 'Worker shutdown', false);
|
|
471
484
|
}
|
|
485
|
+
// Release scheduled retry timers (they would hold the process open)
|
|
486
|
+
for (const timer of this.retryTimers)
|
|
487
|
+
clearTimeout(timer);
|
|
488
|
+
this.retryTimers.clear();
|
|
472
489
|
// Stop store cleanup
|
|
473
490
|
this.store.stopCleanup();
|
|
474
491
|
await this.unregisterWorker();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.55.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -100,11 +100,11 @@
|
|
|
100
100
|
"dependencies": {
|
|
101
101
|
"@claude-flow/cli-core": "3.7.0",
|
|
102
102
|
"@claude-flow/codex": "3.0.3",
|
|
103
|
-
"@claude-flow/mcp": "3.
|
|
103
|
+
"@claude-flow/mcp": "3.1.0",
|
|
104
104
|
"@claude-flow/neural": "3.0.0",
|
|
105
|
-
"@claude-flow/plugin-agent-federation": "1.0.
|
|
106
|
-
"@claude-flow/security": "3.0.
|
|
107
|
-
"@claude-flow/shared": "3.0.
|
|
105
|
+
"@claude-flow/plugin-agent-federation": "1.0.1",
|
|
106
|
+
"@claude-flow/security": "3.0.2",
|
|
107
|
+
"@claude-flow/shared": "3.0.2",
|
|
108
108
|
"@iarna/toml": "^2.2.5",
|
|
109
109
|
"@noble/ed25519": "2.3.0",
|
|
110
110
|
"@ruvector/rabitq-wasm": "0.1.0",
|
|
@@ -125,11 +125,11 @@
|
|
|
125
125
|
"ws": "^8.21.0",
|
|
126
126
|
"yaml": "^2.8.0",
|
|
127
127
|
"zod": "^3.22.0",
|
|
128
|
-
"@claude-flow/memory": "3.0.
|
|
128
|
+
"@claude-flow/memory": "3.0.1"
|
|
129
129
|
},
|
|
130
130
|
"optionalDependencies": {
|
|
131
131
|
"@agntcy/slim-bindings": "2.0.0-alpha.5",
|
|
132
|
-
"@claude-flow/memory": "3.0.
|
|
132
|
+
"@claude-flow/memory": "3.0.1",
|
|
133
133
|
"@metaharness/darwin": "~0.10.2",
|
|
134
134
|
"@metaharness/flywheel": "~0.1.10",
|
|
135
135
|
"@metaharness/radio": "~0.1.0",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, it, expect } from 'vitest';
|
|
2
|
+
import { EventStore } from './event-store.js';
|
|
3
|
+
import { createAgentSpawnedEvent } from './domain-events.js';
|
|
4
|
+
describe('SQLite event query pagination', () => {
|
|
5
|
+
let store;
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
store = new EventStore({
|
|
8
|
+
databasePath: ':memory:',
|
|
9
|
+
autoPersistInterval: 0,
|
|
10
|
+
});
|
|
11
|
+
await store.initialize();
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
const event = createAgentSpawnedEvent(`agent-${i}`, 'coder', 'core', []);
|
|
14
|
+
event.timestamp = 100 + i;
|
|
15
|
+
await store.append(event);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await store.shutdown();
|
|
20
|
+
});
|
|
21
|
+
it('supports offsets without a caller-specified limit', async () => {
|
|
22
|
+
expect((await store.query({ offset: 1 })).map((event) => event.aggregateId)).toEqual(['agent-1', 'agent-2']);
|
|
23
|
+
});
|
|
24
|
+
it('returns no rows for an explicit zero limit', async () => {
|
|
25
|
+
expect(await store.query({ limit: 0 })).toEqual([]);
|
|
26
|
+
expect(await store.query({ limit: 0, offset: 1 })).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it('retains bounded page ordering', async () => {
|
|
29
|
+
expect((await store.query({ limit: 1, offset: 1 })).map((event) => event.aggregateId)).toEqual(['agent-1']);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=event-store-pagination.test.js.map
|
|
@@ -225,11 +225,15 @@ export class EventStore extends EventEmitter {
|
|
|
225
225
|
// Order by timestamp
|
|
226
226
|
sql += ' ORDER BY timestamp ASC';
|
|
227
227
|
// Pagination
|
|
228
|
-
if (filter.limit) {
|
|
228
|
+
if (filter.limit !== undefined) {
|
|
229
229
|
sql += ' LIMIT ?';
|
|
230
230
|
params.push(filter.limit);
|
|
231
231
|
}
|
|
232
|
-
if (filter.offset) {
|
|
232
|
+
else if (filter.offset !== undefined) {
|
|
233
|
+
// SQLite requires LIMIT before OFFSET; -1 means no upper bound.
|
|
234
|
+
sql += ' LIMIT -1';
|
|
235
|
+
}
|
|
236
|
+
if (filter.offset !== undefined) {
|
|
233
237
|
sql += ' OFFSET ?';
|
|
234
238
|
params.push(filter.offset);
|
|
235
239
|
}
|
|
@@ -281,9 +285,12 @@ export class EventStore extends EventEmitter {
|
|
|
281
285
|
async getSnapshot(aggregateId) {
|
|
282
286
|
this.ensureInitialized();
|
|
283
287
|
const stmt = this.db.prepare('SELECT * FROM snapshots WHERE aggregate_id = ? ORDER BY version DESC LIMIT 1');
|
|
284
|
-
|
|
288
|
+
stmt.bind([aggregateId]);
|
|
289
|
+
// sql.js getAsObject() yields undefined-valued column keys when no row matched,
|
|
290
|
+
// so step first and treat absence explicitly.
|
|
291
|
+
const row = stmt.step() ? stmt.getAsObject() : null;
|
|
285
292
|
stmt.free();
|
|
286
|
-
if (!row
|
|
293
|
+
if (!row) {
|
|
287
294
|
return null;
|
|
288
295
|
}
|
|
289
296
|
return {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
8
|
import { EventStore } from './event-store.js';
|
|
9
|
+
import { StateReconstructor, TaskAggregate } from './state-reconstructor.js';
|
|
9
10
|
import { AgentStateProjection, TaskHistoryProjection, MemoryIndexProjection } from './projections.js';
|
|
10
11
|
import { createAgentSpawnedEvent, createAgentStartedEvent, createAgentTaskAssignedEvent, createAgentTaskCompletedEvent, createTaskCreatedEvent, createTaskStartedEvent, createTaskCompletedEvent, createMemoryStoredEvent, createMemoryRetrievedEvent, createMemoryDeletedEvent, } from './domain-events.js';
|
|
11
12
|
describe('EventStore', () => {
|
|
@@ -157,6 +158,25 @@ describe('EventStore', () => {
|
|
|
157
158
|
const retrieved = await eventStore.getSnapshot('agent-1');
|
|
158
159
|
expect(retrieved?.version).toBe(10);
|
|
159
160
|
});
|
|
161
|
+
it('should return null when no snapshot exists for the aggregate', async () => {
|
|
162
|
+
await expect(eventStore.getSnapshot('missing')).resolves.toBeNull();
|
|
163
|
+
});
|
|
164
|
+
it('should return null for an unknown aggregate when other snapshots exist', async () => {
|
|
165
|
+
await eventStore.saveSnapshot({
|
|
166
|
+
aggregateId: 'agent-1',
|
|
167
|
+
aggregateType: 'agent',
|
|
168
|
+
version: 1,
|
|
169
|
+
state: {},
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
});
|
|
172
|
+
await expect(eventStore.getSnapshot('other')).resolves.toBeNull();
|
|
173
|
+
});
|
|
174
|
+
it('should reconstruct from the event log when no snapshot exists', async () => {
|
|
175
|
+
await eventStore.append(createTaskCreatedEvent('task-1', 'impl', 'Write code', 'desc', 'normal', []));
|
|
176
|
+
const reconstructor = new StateReconstructor(eventStore);
|
|
177
|
+
const task = await reconstructor.reconstruct('task-1', (id) => new TaskAggregate(id));
|
|
178
|
+
expect(task.version).toBe(1);
|
|
179
|
+
});
|
|
160
180
|
});
|
|
161
181
|
describe('Statistics', () => {
|
|
162
182
|
it('should provide event store statistics', async () => {
|
|
@@ -119,9 +119,12 @@ export class ConnectionPool extends EventEmitter {
|
|
|
119
119
|
/**
|
|
120
120
|
* Create a new connection
|
|
121
121
|
*/
|
|
122
|
-
async createConnection() {
|
|
122
|
+
async createConnection(reserved = false) {
|
|
123
123
|
const id = `conn-${++this.connectionCounter}-${Date.now()}`;
|
|
124
124
|
const connection = new ManagedConnection(id, this.transportType);
|
|
125
|
+
// Reserve before exposing it in the map or yielding to another acquirer.
|
|
126
|
+
if (reserved)
|
|
127
|
+
connection.state = 'busy';
|
|
125
128
|
this.connections.set(id, connection);
|
|
126
129
|
this.stats.totalCreated++;
|
|
127
130
|
this.emit('pool:connection:created', { connectionId: id });
|
|
@@ -149,7 +152,7 @@ export class ConnectionPool extends EventEmitter {
|
|
|
149
152
|
}
|
|
150
153
|
// Create new connection if under limit
|
|
151
154
|
if (this.connections.size < this.config.maxConnections) {
|
|
152
|
-
const connection = await this.createConnection();
|
|
155
|
+
const connection = await this.createConnection(true);
|
|
153
156
|
connection.acquire();
|
|
154
157
|
this.stats.totalAcquired++;
|
|
155
158
|
this.recordAcquireTime(startTime);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Bulkhead } from './bulkhead.js';
|
|
3
|
+
describe('queued bulkhead failures', () => {
|
|
4
|
+
it('rejects the throwing task and preserves the previous result and remaining capacity', async () => {
|
|
5
|
+
const bulkhead = new Bulkhead({
|
|
6
|
+
name: 'test',
|
|
7
|
+
maxConcurrent: 1,
|
|
8
|
+
maxQueue: 2,
|
|
9
|
+
queueTimeout: 100,
|
|
10
|
+
});
|
|
11
|
+
let release;
|
|
12
|
+
const first = bulkhead.execute(() => new Promise((resolve) => {
|
|
13
|
+
release = resolve;
|
|
14
|
+
}));
|
|
15
|
+
const failure = new Error('synchronous callback failure');
|
|
16
|
+
const second = bulkhead.execute(() => {
|
|
17
|
+
throw failure;
|
|
18
|
+
});
|
|
19
|
+
const secondResult = second.catch((error) => error);
|
|
20
|
+
const third = bulkhead.execute(async () => 'third');
|
|
21
|
+
release('first');
|
|
22
|
+
await expect(first).resolves.toBe('first');
|
|
23
|
+
expect(await secondResult).toBe(failure);
|
|
24
|
+
await expect(third).resolves.toBe('third');
|
|
25
|
+
await Promise.resolve();
|
|
26
|
+
expect(bulkhead.getStats()).toMatchObject({
|
|
27
|
+
active: 0,
|
|
28
|
+
queued: 0,
|
|
29
|
+
completed: 2,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=bulkhead-sync-throw.test.js.map
|
|
@@ -158,7 +158,8 @@ export class Bulkhead extends EventEmitter {
|
|
|
158
158
|
// Execute the queued function
|
|
159
159
|
this.active++;
|
|
160
160
|
this.emit('acquire');
|
|
161
|
-
|
|
161
|
+
Promise.resolve()
|
|
162
|
+
.then(() => item.fn())
|
|
162
163
|
.then((result) => {
|
|
163
164
|
this.completed++;
|
|
164
165
|
item.resolve(result);
|