@claude-flow/cli 3.56.9 → 3.57.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-version +1 -1
- package/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/helpers/intelligence.cjs +51 -9
- package/.claude/helpers/router.cjs +0 -0
- package/.claude/skills/verification-quality/SKILL.md +14 -11
- package/README.md +1 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/autopilot-state.d.ts +2 -0
- package/dist/src/autopilot-state.js +36 -8
- package/dist/src/command-resolution.d.ts +15 -0
- package/dist/src/command-resolution.js +25 -0
- package/dist/src/commands/agent.js +17 -17
- package/dist/src/commands/claims.js +6 -8
- package/dist/src/commands/daemon.js +94 -33
- package/dist/src/commands/doctor.js +95 -18
- package/dist/src/commands/memory.js +14 -2
- package/dist/src/commands/policy.js +6 -1
- package/dist/src/commands/security-code-patterns.d.ts +2 -2
- package/dist/src/commands/security-code-patterns.js +61 -3
- package/dist/src/commands/security.d.ts +24 -0
- package/dist/src/commands/security.js +81 -35
- package/dist/src/commands/tui.d.ts +9 -0
- package/dist/src/commands/tui.js +29 -3
- package/dist/src/config/proven-config-refresh.d.ts +9 -0
- package/dist/src/config/proven-config-refresh.js +24 -3
- package/dist/src/funnel/local-signals.js +2 -1
- package/dist/src/index.js +11 -47
- package/dist/src/init/settings-generator.js +4 -58
- package/dist/src/init/types.js +1 -1
- package/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
- package/dist/src/mcp-tools/agent-execute-core.js +15 -0
- package/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
- package/dist/src/mcp-tools/agent-health-metrics.js +33 -0
- package/dist/src/mcp-tools/agent-tools.js +45 -22
- package/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
- package/dist/src/mcp-tools/agentbbs-federation.js +7 -2
- package/dist/src/mcp-tools/agentdb-tools.js +25 -2
- package/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
- package/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
- package/dist/src/mcp-tools/analyze-tools.js +10 -11
- package/dist/src/mcp-tools/autopilot-tools.js +2 -2
- package/dist/src/mcp-tools/browser-intent-tools.js +6 -2
- package/dist/src/mcp-tools/embeddings-tools.js +29 -7
- package/dist/src/mcp-tools/hooks-tools.js +4 -0
- package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
- package/dist/src/mcp-tools/memory-tools.js +100 -25
- package/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/dist/src/mcp-tools/neural-tools.js +48 -54
- package/dist/src/mcp-tools/policy-tools.js +5 -3
- package/dist/src/mcp-tools/ruvllm-tools.js +6 -0
- package/dist/src/mcp-tools/security-tools.js +55 -17
- package/dist/src/mcp-tools/workflow-tools.js +66 -17
- package/dist/src/memory/intelligence.d.ts +5 -1
- package/dist/src/memory/intelligence.js +158 -35
- package/dist/src/memory/memory-bridge.js +10 -3
- package/dist/src/memory/memory-initializer.js +2 -1
- package/dist/src/memory/search-defaults.d.ts +54 -0
- package/dist/src/memory/search-defaults.js +37 -0
- package/dist/src/memory/sona-optimizer.d.ts +28 -0
- package/dist/src/memory/sona-optimizer.js +59 -2
- package/dist/src/memory/stats-file-lock.d.ts +7 -0
- package/dist/src/memory/stats-file-lock.js +53 -0
- package/dist/src/mods/command-registry/schema.d.ts +24 -24
- package/dist/src/ruvector/agent-wasm.d.ts +10 -0
- package/dist/src/ruvector/agent-wasm.js +27 -1
- package/dist/src/ruvector/diff-classifier.d.ts +2 -4
- package/dist/src/ruvector/diff-classifier.js +202 -69
- package/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
- package/dist/src/ruvector/ruvllm-wasm.js +12 -1
- package/dist/src/services/distill-oracle.d.ts +1 -1
- package/dist/src/services/distill-oracle.js +2 -2
- package/dist/src/services/policy-anchor-log.d.ts +13 -0
- package/dist/src/services/policy-anchor-log.js +242 -0
- package/dist/src/services/policy-archive-scan.d.ts +7 -0
- package/dist/src/services/policy-archive-scan.js +50 -0
- package/dist/src/services/policy-ledger-anchor.d.ts +14 -8
- package/dist/src/services/policy-ledger-anchor.js +356 -40
- package/dist/src/services/policy-ledger-audit.d.ts +22 -0
- package/dist/src/services/policy-ledger-audit.js +260 -0
- package/dist/src/services/policy-ledger-record.d.ts +3 -0
- package/dist/src/services/policy-ledger-record.js +22 -0
- package/dist/src/services/policy-runtime.d.ts +2 -1
- package/dist/src/services/policy-runtime.js +267 -40
- package/dist/src/services/worker-daemon.d.ts +11 -0
- package/dist/src/services/worker-daemon.js +37 -0
- package/dist/src/transfer/store/discovery.js +16 -17
- package/dist/src/transfer/store/types.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -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 +99 -24
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
- package/package.json +5 -5
- package/plugins/ruflo-console/dist/tui.mjs +162 -161
- package/plugins/ruflo-metaharness/hooks/screen.ts +7 -1
- package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
|
@@ -1,16 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
// Narrow subpath imports (not the '@claude-flow/security' barrel): this module
|
|
2
|
+
// is loaded eagerly by the MCP server, and the barrel also evaluates the OAuth
|
|
3
|
+
// callback server (an ESM `node:http` import that materialises http's lazy
|
|
4
|
+
// undici getters), bcryptjs, keychain and credential helpers that the policy
|
|
5
|
+
// runtime never uses.
|
|
6
|
+
import { AgenticPolicyEngine, POLICY_STATE_VERSION, createLegacyCompatibleState, } from '@claude-flow/security/policy/index';
|
|
7
|
+
import { isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security/mcp-caller-identity';
|
|
2
8
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
9
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync,
|
|
10
|
+
import { closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, ftruncateSync, writeSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
5
11
|
import { dirname, join, resolve } from 'node:path';
|
|
6
12
|
import { hostname, userInfo } from 'node:os';
|
|
7
13
|
import { syncPolicyProjection } from '../mods/policy-projection.js';
|
|
14
|
+
import { ATOMIC_JSON_WRITER } from './policy-anchor-log.js';
|
|
15
|
+
import { captureArchiveSnapshot, verifyFullPolicyLedger } from './policy-ledger-audit.js';
|
|
8
16
|
import { assessAnchors, policyTrustRoot, recordAnchor } from './policy-ledger-anchor.js';
|
|
9
17
|
const POLICY_DIR = join('.claude-flow', 'policy');
|
|
10
18
|
const POLICY_FILE = 'state.json';
|
|
11
19
|
const LOCK_FILE = 'state.lock';
|
|
12
|
-
const
|
|
20
|
+
const RECEIPT_ARCHIVE_FILE = 'receipts.ledger.jsonl';
|
|
13
21
|
const LOCK_WAIT_MS = 5_000;
|
|
22
|
+
// #3164: without a bound, `state.receipts` grows forever and every
|
|
23
|
+
// transaction's verify/clone/serialize cost is O(all receipts ever issued) —
|
|
24
|
+
// eventually exceeding LOCK_WAIT_MS and timing out every MCP tool call.
|
|
25
|
+
// Pruned receipts are archived to RECEIPT_ARCHIVE_FILE, never discarded.
|
|
26
|
+
const DEFAULT_LEDGER_RETENTION = 2_000;
|
|
27
|
+
function ledgerRetention() {
|
|
28
|
+
const raw = process.env.CLAUDE_FLOW_POLICY_LEDGER_RETENTION;
|
|
29
|
+
if (!raw)
|
|
30
|
+
return DEFAULT_LEDGER_RETENTION;
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : DEFAULT_LEDGER_RETENTION;
|
|
33
|
+
}
|
|
14
34
|
// A lock is ~170 bytes plus the hostname (at most 255 bytes).
|
|
15
35
|
const LOCK_MAX_BYTES = 1_024;
|
|
16
36
|
// Linux PID namespace of this process, e.g. `pid:[4026531836]`; undefined on
|
|
@@ -39,7 +59,7 @@ const BOOT_ID = (() => {
|
|
|
39
59
|
function paths(projectRoot) {
|
|
40
60
|
const root = resolve(projectRoot);
|
|
41
61
|
const dir = join(root, POLICY_DIR);
|
|
42
|
-
return { dir, state: join(dir, POLICY_FILE), lock: join(dir, LOCK_FILE) };
|
|
62
|
+
return { dir, state: join(dir, POLICY_FILE), lock: join(dir, LOCK_FILE), archive: join(dir, RECEIPT_ARCHIVE_FILE) };
|
|
43
63
|
}
|
|
44
64
|
function sleep(ms) {
|
|
45
65
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
@@ -62,7 +82,7 @@ function isPidAlive(pid) {
|
|
|
62
82
|
/**
|
|
63
83
|
* Reads a lock only if it is a small regular file: never follows a symlink,
|
|
64
84
|
* never blocks on a FIFO or device, never reads more than LOCK_MAX_BYTES.
|
|
65
|
-
* Anything else returns undefined
|
|
85
|
+
* Anything else returns undefined; unknown owners are preserved.
|
|
66
86
|
*/
|
|
67
87
|
function readLockFile(lockPath) {
|
|
68
88
|
if (!lstatSync(lockPath).isFile())
|
|
@@ -88,8 +108,8 @@ function readLockFile(lockPath) {
|
|
|
88
108
|
* boot and in this PID namespace: anywhere else (a bwrap --unshare-pid
|
|
89
109
|
* sandbox, a container, another machine on a shared filesystem) a live owner
|
|
90
110
|
* also answers ESRCH. Locks without that identity (older ruflo, a crash
|
|
91
|
-
* between create and write, anything unparsable) return false and
|
|
92
|
-
*
|
|
111
|
+
* between create and write, anything unparsable) return false and require
|
|
112
|
+
* explicit operator inspection rather than age-only takeover.
|
|
93
113
|
*/
|
|
94
114
|
function lockOwnerIsDead(lockPath) {
|
|
95
115
|
try {
|
|
@@ -121,6 +141,36 @@ function lockOwnerIsDead(lockPath) {
|
|
|
121
141
|
return false;
|
|
122
142
|
}
|
|
123
143
|
}
|
|
144
|
+
/** Serialize dead-owner reclamation across processes. A normal acquirer can
|
|
145
|
+
* create state.lock after deletion, but the next reclaimer must re-probe that
|
|
146
|
+
* new owner under this guard. Guard orphans fail closed, requiring explicit
|
|
147
|
+
* operator recovery after validating their exact recorded owner and nonce.
|
|
148
|
+
* Unknown/foreign owners are never stolen merely because their mtime aged. */
|
|
149
|
+
function reclaimDeadLock(lockPath) {
|
|
150
|
+
const guard = `${lockPath}.reclaim`;
|
|
151
|
+
let fd;
|
|
152
|
+
try {
|
|
153
|
+
fd = openSync(guard, 'wx', 0o600);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const ownership = JSON.stringify({ pid: process.pid, nonce: randomUUID(), host: hostname(), platform: process.platform, pidns: PID_NAMESPACE, boot: BOOT_ID });
|
|
159
|
+
try {
|
|
160
|
+
writeFileSync(fd, ownership);
|
|
161
|
+
if (lockOwnerIsDead(lockPath))
|
|
162
|
+
unlinkSync(lockPath);
|
|
163
|
+
}
|
|
164
|
+
catch { /* lock changed, unknown identity, or an inaccessible path */ }
|
|
165
|
+
finally {
|
|
166
|
+
closeSync(fd);
|
|
167
|
+
try {
|
|
168
|
+
if (readLockFile(guard) === ownership)
|
|
169
|
+
unlinkSync(guard);
|
|
170
|
+
}
|
|
171
|
+
catch { /* preserve an unknown guard */ }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
124
174
|
/**
|
|
125
175
|
* Exported for ADR-406 mission storage, which takes the same per-file lock
|
|
126
176
|
* (pid, pid namespace and boot id identify a dead owner) rather than a copy.
|
|
@@ -130,33 +180,37 @@ export async function acquireLock(lockPath) {
|
|
|
130
180
|
while (Date.now() - started < LOCK_WAIT_MS) {
|
|
131
181
|
try {
|
|
132
182
|
const fd = openSync(lockPath, 'wx', 0o600);
|
|
133
|
-
|
|
183
|
+
const ownership = JSON.stringify({
|
|
134
184
|
pid: process.pid,
|
|
135
185
|
acquiredAt: Date.now(),
|
|
136
186
|
host: hostname(),
|
|
137
187
|
platform: process.platform,
|
|
138
188
|
pidns: PID_NAMESPACE,
|
|
139
189
|
boot: BOOT_ID,
|
|
140
|
-
|
|
141
|
-
|
|
190
|
+
nonce: randomUUID(),
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(fd, ownership);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
closeSync(fd);
|
|
197
|
+
}
|
|
142
198
|
return () => {
|
|
143
199
|
try {
|
|
144
|
-
|
|
200
|
+
if (readLockFile(lockPath) === ownership)
|
|
201
|
+
unlinkSync(lockPath);
|
|
145
202
|
}
|
|
146
203
|
catch { /* already released */ }
|
|
147
204
|
};
|
|
148
205
|
}
|
|
149
206
|
catch {
|
|
150
|
-
|
|
151
|
-
if (lockOwnerIsDead(lockPath)
|
|
152
|
-
|| Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
|
|
153
|
-
unlinkSync(lockPath);
|
|
154
|
-
}
|
|
155
|
-
catch { /* another process changed the lock */ }
|
|
207
|
+
reclaimDeadLock(lockPath);
|
|
156
208
|
await sleep(10);
|
|
157
209
|
}
|
|
158
210
|
}
|
|
159
|
-
throw new Error(
|
|
211
|
+
throw new Error(lockOwnerIsDead(lockPath)
|
|
212
|
+
? 'policy-state-lock-timeout: dead owner; inspect exact owner of state.lock.reclaim before explicit recovery'
|
|
213
|
+
: 'policy-state-lock-timeout');
|
|
160
214
|
}
|
|
161
215
|
// #3398: Windows rename over a file another process holds open fails with
|
|
162
216
|
// EPERM/EBUSY/EACCES. Retry (~1.3s total, inside LOCK_WAIT_MS), never leave the
|
|
@@ -164,17 +218,129 @@ export async function acquireLock(lockPath) {
|
|
|
164
218
|
// and consumed approval uses, so dropping it would let an approval be reused.
|
|
165
219
|
const RENAME_RETRY_DELAYS_MS = [25, 50, 100, 150, 250, 300, 400];
|
|
166
220
|
const TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
221
|
+
function mkdirDurably(dir) {
|
|
222
|
+
if (existsSync(dir))
|
|
223
|
+
return;
|
|
224
|
+
const parent = dirname(dir);
|
|
225
|
+
if (parent !== dir)
|
|
226
|
+
mkdirDurably(parent);
|
|
227
|
+
try {
|
|
228
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error.code !== 'EEXIST')
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
syncDirectory(parent);
|
|
235
|
+
}
|
|
236
|
+
function syncDirectory(dir) {
|
|
237
|
+
// Windows does not expose directory fsync through Node. File sync and
|
|
238
|
+
// atomic rename still apply there; POSIX failures must reach the caller.
|
|
239
|
+
if (process.platform === 'win32')
|
|
240
|
+
return;
|
|
241
|
+
const fd = openSync(dir, fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0));
|
|
242
|
+
try {
|
|
243
|
+
fsyncSync(fd);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
closeSync(fd);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Recover only an incomplete, unacknowledged final append. All inspection,
|
|
250
|
+
* truncation, append and sync use the same nofollow regular-file descriptor. */
|
|
251
|
+
function appendArchiveDurably(file, receipts, verifier) {
|
|
252
|
+
const fd = openSync(file, fsConstants.O_RDWR | fsConstants.O_CREAT
|
|
253
|
+
| (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0), 0o600);
|
|
254
|
+
try {
|
|
255
|
+
const stat = fstatSync(fd);
|
|
256
|
+
if (!stat.isFile())
|
|
257
|
+
throw new Error('policy-ledger-archive-not-regular');
|
|
258
|
+
const limit = 64 * 1024;
|
|
259
|
+
const start = Math.max(0, stat.size - limit);
|
|
260
|
+
const buffer = Buffer.alloc(stat.size - start);
|
|
261
|
+
if (readSync(fd, buffer, 0, buffer.length, start) !== buffer.length)
|
|
262
|
+
throw new Error('policy-ledger-archive-short-read');
|
|
263
|
+
let end = stat.size;
|
|
264
|
+
const lastNewline = buffer.lastIndexOf(10);
|
|
265
|
+
if (buffer.length && buffer[buffer.length - 1] !== 10) {
|
|
266
|
+
if (lastNewline < 0 && start > 0)
|
|
267
|
+
throw new Error('policy-ledger-archive-tail-too-large');
|
|
268
|
+
const fragmentBytes = buffer.subarray(lastNewline + 1);
|
|
269
|
+
const fragment = fragmentBytes.toString('utf8');
|
|
270
|
+
try {
|
|
271
|
+
JSON.parse(fragment);
|
|
272
|
+
throw new Error('policy-ledger-archive-unterminated-record');
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (!(error instanceof SyntaxError))
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
// A parse failure alone grants no repair authority: the bytes must
|
|
279
|
+
// be an exact prefix of a receipt still available in the verified hot
|
|
280
|
+
// state and being retried by this prune.
|
|
281
|
+
if (!fragmentBytes.length || !receipts.some(receipt => {
|
|
282
|
+
const expected = Buffer.from(JSON.stringify(receipt));
|
|
283
|
+
return fragmentBytes.length < expected.length && expected.subarray(0, fragmentBytes.length).equals(fragmentBytes);
|
|
284
|
+
})) {
|
|
285
|
+
throw new Error('policy-ledger-archive-unrecoverable-tail');
|
|
286
|
+
}
|
|
287
|
+
end = start + lastNewline + 1;
|
|
288
|
+
}
|
|
289
|
+
const complete = buffer.subarray(0, end - start).toString('utf8');
|
|
290
|
+
const usable = start === 0 ? complete : complete.slice(complete.indexOf('\n') + 1);
|
|
291
|
+
for (const line of usable.split('\n')) {
|
|
292
|
+
if (!line.trim())
|
|
293
|
+
continue;
|
|
294
|
+
let record;
|
|
295
|
+
try {
|
|
296
|
+
record = JSON.parse(line);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
throw new Error('policy-ledger-archive-parse-error');
|
|
300
|
+
}
|
|
301
|
+
if (verifier.verifyReceipt(record))
|
|
302
|
+
throw new Error('policy-ledger-archive-conflict');
|
|
303
|
+
}
|
|
304
|
+
if (end < stat.size) {
|
|
305
|
+
ftruncateSync(fd, end);
|
|
306
|
+
fsyncSync(fd);
|
|
307
|
+
}
|
|
308
|
+
const bytes = Buffer.from(`${receipts.map(receipt => JSON.stringify(receipt)).join('\n')}\n`);
|
|
309
|
+
let written = 0;
|
|
310
|
+
while (written < bytes.length) {
|
|
311
|
+
const count = writeSync(fd, bytes, written, bytes.length - written, end + written);
|
|
312
|
+
if (!count)
|
|
313
|
+
throw new Error('policy-ledger-archive-short-write');
|
|
314
|
+
written += count;
|
|
315
|
+
}
|
|
316
|
+
fsyncSync(fd);
|
|
317
|
+
syncDirectory(dirname(file));
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
closeSync(fd);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
167
323
|
async function writeJsonAtomic(file, value) {
|
|
168
|
-
|
|
324
|
+
mkdirDurably(dirname(file));
|
|
169
325
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
170
326
|
let renamed = false;
|
|
171
327
|
try {
|
|
172
|
-
|
|
328
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
329
|
+
try {
|
|
330
|
+
if (value && typeof value[ATOMIC_JSON_WRITER] === 'function')
|
|
331
|
+
value[ATOMIC_JSON_WRITER](fd);
|
|
332
|
+
else
|
|
333
|
+
writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
334
|
+
fsyncSync(fd);
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
closeSync(fd);
|
|
338
|
+
}
|
|
173
339
|
for (let attempt = 0;; attempt++) {
|
|
174
340
|
try {
|
|
175
341
|
renameSync(temporary, file);
|
|
176
342
|
renamed = true;
|
|
177
|
-
|
|
343
|
+
break;
|
|
178
344
|
}
|
|
179
345
|
catch (error) {
|
|
180
346
|
const code = error?.code ?? '';
|
|
@@ -183,6 +349,9 @@ async function writeJsonAtomic(file, value) {
|
|
|
183
349
|
await sleep(RENAME_RETRY_DELAYS_MS[attempt]);
|
|
184
350
|
}
|
|
185
351
|
}
|
|
352
|
+
// A post-rename sync failure is a publication failure, not a rename
|
|
353
|
+
// failure. Never retry with a temporary file already consumed by rename.
|
|
354
|
+
syncDirectory(dirname(file));
|
|
186
355
|
}
|
|
187
356
|
finally {
|
|
188
357
|
if (!renamed) {
|
|
@@ -204,8 +373,16 @@ function trustKey(projectRoot, create) {
|
|
|
204
373
|
if (!existsSync(key)) {
|
|
205
374
|
if (!create)
|
|
206
375
|
return undefined;
|
|
207
|
-
|
|
208
|
-
|
|
376
|
+
mkdirDurably(dirname(key));
|
|
377
|
+
const fd = openSync(key, 'wx', 0o600);
|
|
378
|
+
try {
|
|
379
|
+
writeFileSync(fd, randomBytes(32));
|
|
380
|
+
fsyncSync(fd);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
closeSync(fd);
|
|
384
|
+
}
|
|
385
|
+
syncDirectory(dirname(key));
|
|
209
386
|
}
|
|
210
387
|
const material = readFileSync(key);
|
|
211
388
|
if (material.length !== 32)
|
|
@@ -232,11 +409,11 @@ function verifyStateAnchor(projectRoot, state) {
|
|
|
232
409
|
throw new Error('policy-state-authentication-failed');
|
|
233
410
|
}
|
|
234
411
|
}
|
|
235
|
-
async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by) {
|
|
412
|
+
async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by, assessedTip) {
|
|
236
413
|
await writePolicyStateFiles(projectRoot, statePath, state);
|
|
237
414
|
// #3602: the second anchor is written after the state, so a crash leaves the
|
|
238
415
|
// state AHEAD of the log (caught up on the next write), never behind it.
|
|
239
|
-
await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by);
|
|
416
|
+
await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by, writeJsonAtomic, assessedTip);
|
|
240
417
|
// ADR-404: the ruflo mod reads Claude Code tool rules from a small
|
|
241
418
|
// projection, never from state.json. Written only after the state and its
|
|
242
419
|
// anchor are safely down; a failure here never fails the state write. A
|
|
@@ -310,7 +487,7 @@ export function loadPolicyState(projectRoot = process.cwd()) {
|
|
|
310
487
|
return createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
|
|
311
488
|
}
|
|
312
489
|
const parsed = JSON.parse(readFileSync(target.state, 'utf8'));
|
|
313
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.rules) || !Array.isArray(parsed.receipts)) {
|
|
490
|
+
if ((parsed.version !== 1 && parsed.version !== POLICY_STATE_VERSION) || !Array.isArray(parsed.rules) || !Array.isArray(parsed.receipts)) {
|
|
314
491
|
throw new Error(`unsupported-policy-state-version:${String(parsed.version)}`);
|
|
315
492
|
}
|
|
316
493
|
verifyStateAnchor(projectRoot, parsed);
|
|
@@ -331,7 +508,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
|
|
|
331
508
|
if (!existsSync(join(resolve(projectRoot), '.claude-flow'))
|
|
332
509
|
&& !existsSync(join(resolve(projectRoot), '.swarm')))
|
|
333
510
|
return { migrated: false };
|
|
334
|
-
|
|
511
|
+
mkdirDurably(target.dir);
|
|
335
512
|
const release = await acquireLock(target.lock);
|
|
336
513
|
try {
|
|
337
514
|
if (!existsSync(target.state)) {
|
|
@@ -351,7 +528,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
|
|
|
351
528
|
}
|
|
352
529
|
export async function withPolicyTransaction(projectRoot, operation, options = {}) {
|
|
353
530
|
const target = paths(projectRoot);
|
|
354
|
-
|
|
531
|
+
mkdirDurably(target.dir);
|
|
355
532
|
const release = await acquireLock(target.lock);
|
|
356
533
|
try {
|
|
357
534
|
const loaded = loadPolicyState(projectRoot);
|
|
@@ -370,10 +547,30 @@ export async function withPolicyTransaction(projectRoot, operation, options = {}
|
|
|
370
547
|
: gate.result.error ?? 'policy-ledger-verification-failed');
|
|
371
548
|
}
|
|
372
549
|
const result = await operation(engine);
|
|
373
|
-
|
|
550
|
+
const verified = engine.verifyLedger();
|
|
551
|
+
if (!verified.valid)
|
|
374
552
|
throw new Error('policy-ledger-verification-failed');
|
|
553
|
+
// Only archive the prefix already committed in loaded state. Archiving
|
|
554
|
+
// newly generated receipts before their first state publication would
|
|
555
|
+
// leave conflicting same-sequence records when a failed operation is
|
|
556
|
+
// retried with a different timestamp/request. Retention=0 and large
|
|
557
|
+
// batches therefore retain this transaction's new receipts until a
|
|
558
|
+
// subsequent transaction can archive their exact committed bytes.
|
|
559
|
+
const newlyAppended = verified.length - ((loaded.retainedFrom ?? 0) + loaded.receipts.length);
|
|
560
|
+
const pruned = engine.pruneReceipts(Math.max(ledgerRetention(), newlyAppended));
|
|
561
|
+
// #3164: archive BEFORE writing the shrunk state. If the process crashes
|
|
562
|
+
// between the two writes, the archive append already landed but
|
|
563
|
+
// `retainedFrom` on disk still reflects the pre-prune length, so the next
|
|
564
|
+
// prune recomputes and re-appends the SAME receipts — a harmless,
|
|
565
|
+
// detectable duplicate (same sequence, identical content), deduped by
|
|
566
|
+
// `verifyPolicyLedger({ full: true })`. The reverse order (state first)
|
|
567
|
+
// would instead risk a permanent gap if the process died in between,
|
|
568
|
+
// which is strictly worse for an audit ledger — never reorder this.
|
|
569
|
+
if (pruned.length > 0) {
|
|
570
|
+
appendArchiveDurably(target.archive, pruned, engine);
|
|
571
|
+
}
|
|
375
572
|
const nextState = engine.exportState();
|
|
376
|
-
await writePolicyState(projectRoot, target.state, nextState, gate.event);
|
|
573
|
+
await writePolicyState(projectRoot, target.state, nextState, gate.event, undefined, gate.anchorTip);
|
|
377
574
|
return result;
|
|
378
575
|
}
|
|
379
576
|
finally {
|
|
@@ -433,7 +630,9 @@ export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
|
|
|
433
630
|
*/
|
|
434
631
|
function reconcileLedger(projectRoot, engine, options) {
|
|
435
632
|
const state = engine.exportState();
|
|
436
|
-
|
|
633
|
+
// #3164: the historical length includes anything pruned out of the hot
|
|
634
|
+
// array — `length` here must keep meaning "total receipts ever issued".
|
|
635
|
+
const length = (state.retainedFrom ?? 0) + state.receipts.length;
|
|
437
636
|
const primary = typeof state.ledgerLength === 'number';
|
|
438
637
|
const base = engine.verifyLedger();
|
|
439
638
|
const fail = (error) => ({ result: { valid: false, length, error }, persist: false });
|
|
@@ -448,7 +647,7 @@ function reconcileLedger(projectRoot, engine, options) {
|
|
|
448
647
|
? { result: { ...base, secondaryAnchor: 'established-explicitly' }, persist: true, event: 'establish-anchor' }
|
|
449
648
|
: { result: { ...base, secondaryAnchor: 'recorded-from-state' }, persist: true, event: 'migrated-from-state' };
|
|
450
649
|
}
|
|
451
|
-
return { result: base, persist: assessed.kind === 'ok' && assessed.behind, event: 'append' };
|
|
650
|
+
return { result: base, persist: assessed.kind === 'ok' && assessed.behind, event: 'append', anchorTip: assessed.kind === 'ok' ? assessed.last : undefined };
|
|
452
651
|
}
|
|
453
652
|
// No primary anchor.
|
|
454
653
|
if (length === 0)
|
|
@@ -457,7 +656,7 @@ function reconcileLedger(projectRoot, engine, options) {
|
|
|
457
656
|
// The second anchor is authentic and agrees with the chain: the deleted
|
|
458
657
|
// primary can be restored from it. Truncation was already rejected above.
|
|
459
658
|
engine.verifyLedger({ establishAnchor: true });
|
|
460
|
-
return { result: { valid: true, length, secondaryAnchor: 'restored-primary' }, persist: true, event: 'append' };
|
|
659
|
+
return { result: { valid: true, length, secondaryAnchor: 'restored-primary' }, persist: true, event: 'append', anchorTip: assessed.last };
|
|
461
660
|
}
|
|
462
661
|
if (options.establishAnchor) {
|
|
463
662
|
const established = engine.verifyLedger({ establishAnchor: true });
|
|
@@ -477,23 +676,51 @@ function reconcileLedger(projectRoot, engine, options) {
|
|
|
477
676
|
*/
|
|
478
677
|
export async function verifyPolicyLedger(projectRoot = process.cwd(), options = {}) {
|
|
479
678
|
const target = paths(projectRoot);
|
|
480
|
-
|
|
679
|
+
mkdirDurably(target.dir);
|
|
680
|
+
const engineOptions = {
|
|
681
|
+
signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
|
|
682
|
+
keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
|
|
683
|
+
};
|
|
684
|
+
let outcomeResult;
|
|
685
|
+
let snapshotState;
|
|
686
|
+
let archiveSnapshot;
|
|
687
|
+
// Capture the verified hot state and a checked regular archive descriptor
|
|
688
|
+
// plus immutable byte boundary under the writer lock. Historical read,
|
|
689
|
+
// authentication, deduplication and replay all happen after release, using
|
|
690
|
+
// bounded chunks and private disposable disk scratch instead of O(history)
|
|
691
|
+
// strings, receipt arrays or maps. The descriptor pins path replacements.
|
|
481
692
|
const release = await acquireLock(target.lock);
|
|
482
693
|
try {
|
|
483
|
-
const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot),
|
|
484
|
-
signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
|
|
485
|
-
keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
|
|
486
|
-
});
|
|
694
|
+
const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), engineOptions);
|
|
487
695
|
const outcome = reconcileLedger(projectRoot, engine, options);
|
|
488
696
|
if (outcome.persist) {
|
|
489
697
|
const by = outcome.event === 'establish-anchor' ? userInfo().username : undefined;
|
|
490
|
-
await writePolicyState(projectRoot, target.state, engine.exportState(), outcome.event, by);
|
|
698
|
+
await writePolicyState(projectRoot, target.state, engine.exportState(), outcome.event, by, outcome.anchorTip);
|
|
699
|
+
}
|
|
700
|
+
outcomeResult = outcome.result;
|
|
701
|
+
snapshotState = engine.exportState();
|
|
702
|
+
if (options.full && outcomeResult.valid) {
|
|
703
|
+
try {
|
|
704
|
+
archiveSnapshot = captureArchiveSnapshot(target.archive);
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
outcomeResult = { valid: false, length: outcomeResult.length, error: error.message };
|
|
708
|
+
}
|
|
491
709
|
}
|
|
492
|
-
return outcome.result;
|
|
493
710
|
}
|
|
494
711
|
finally {
|
|
495
712
|
release();
|
|
496
713
|
}
|
|
714
|
+
if (!options.full || !outcomeResult.valid)
|
|
715
|
+
return outcomeResult;
|
|
716
|
+
try {
|
|
717
|
+
const full = await verifyFullPolicyLedger(archiveSnapshot, snapshotState, engineOptions);
|
|
718
|
+
return { ...full, secondaryAnchor: outcomeResult.secondaryAnchor };
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
if (archiveSnapshot !== undefined)
|
|
722
|
+
closeSync(archiveSnapshot.fd);
|
|
723
|
+
}
|
|
497
724
|
}
|
|
498
725
|
/**
|
|
499
726
|
* Resolve the caller identity for `authorizeMcpTool`.
|
|
@@ -316,6 +316,17 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
316
316
|
* Execute a worker with timeout protection
|
|
317
317
|
*/
|
|
318
318
|
private executeWorker;
|
|
319
|
+
/**
|
|
320
|
+
* Feed a finished worker job into the self-learning pipeline as a one-step
|
|
321
|
+
* trajectory (SONA + ReasoningBank + @ruvector/ruvllm forwarding, persisted
|
|
322
|
+
* to `.claude-flow/neural/stats.json`). Before this, the daemon could run
|
|
323
|
+
* hundreds of jobs at "100% success" while `trajectoriesRecorded` stayed 0:
|
|
324
|
+
* no daemon code path ever called the intelligence module.
|
|
325
|
+
*
|
|
326
|
+
* Best-effort and bounded: never throws, never changes the WorkerResult.
|
|
327
|
+
* Opt-out: RUFLO_DAEMON_NO_LEARN=1.
|
|
328
|
+
*/
|
|
329
|
+
private recordWorkerTrajectory;
|
|
319
330
|
/**
|
|
320
331
|
* Run a function with timeout (P1 fix)
|
|
321
332
|
* @param fn - The async function to execute
|
|
@@ -75,6 +75,9 @@ const CONSOLIDATE_DEDUP_DISTANCE = 0.2;
|
|
|
75
75
|
// `--no-distill` flag on `daemon start` (see commands/daemon.ts), which sets
|
|
76
76
|
// this env var on the forked/foreground daemon process.
|
|
77
77
|
const NO_DISTILL_ENV = 'RUFLO_DAEMON_NO_DISTILL';
|
|
78
|
+
// Opt-out for recording each finished worker job as a learning trajectory
|
|
79
|
+
// (see WorkerDaemon.recordWorkerTrajectory).
|
|
80
|
+
const NO_LEARN_ENV = 'RUFLO_DAEMON_NO_LEARN';
|
|
78
81
|
// #2356 — Self-terminating lifecycle defaults. A background daemon with no
|
|
79
82
|
// upper bound on its lifetime runs until the box reboots; in the field this
|
|
80
83
|
// leaked tens of thousands of headless `claude --print` sweeps over many days
|
|
@@ -1234,6 +1237,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1234
1237
|
this.emit('worker:complete', result);
|
|
1235
1238
|
this.log('info', `Worker ${workerConfig.type} completed in ${durationMs}ms`);
|
|
1236
1239
|
this.saveState();
|
|
1240
|
+
await this.recordWorkerTrajectory(result);
|
|
1237
1241
|
return result;
|
|
1238
1242
|
}
|
|
1239
1243
|
catch (error) {
|
|
@@ -1253,6 +1257,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1253
1257
|
this.emit('worker:error', result);
|
|
1254
1258
|
this.log('error', `Worker ${workerConfig.type} failed: ${result.error}`);
|
|
1255
1259
|
this.saveState();
|
|
1260
|
+
await this.recordWorkerTrajectory(result);
|
|
1256
1261
|
return result;
|
|
1257
1262
|
}
|
|
1258
1263
|
finally {
|
|
@@ -1261,6 +1266,38 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1261
1266
|
this.processPendingWorkers();
|
|
1262
1267
|
}
|
|
1263
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Feed a finished worker job into the self-learning pipeline as a one-step
|
|
1271
|
+
* trajectory (SONA + ReasoningBank + @ruvector/ruvllm forwarding, persisted
|
|
1272
|
+
* to `.claude-flow/neural/stats.json`). Before this, the daemon could run
|
|
1273
|
+
* hundreds of jobs at "100% success" while `trajectoriesRecorded` stayed 0:
|
|
1274
|
+
* no daemon code path ever called the intelligence module.
|
|
1275
|
+
*
|
|
1276
|
+
* Best-effort and bounded: never throws, never changes the WorkerResult.
|
|
1277
|
+
* Opt-out: RUFLO_DAEMON_NO_LEARN=1.
|
|
1278
|
+
*/
|
|
1279
|
+
async recordWorkerTrajectory(result) {
|
|
1280
|
+
if (process.env[NO_LEARN_ENV] === '1')
|
|
1281
|
+
return;
|
|
1282
|
+
try {
|
|
1283
|
+
const intel = await import('../memory/intelligence.js');
|
|
1284
|
+
await intel.recordTrajectory([{
|
|
1285
|
+
type: 'result',
|
|
1286
|
+
// A worker error is free text (it can quote command output): keep only its first line, cut, so it cannot fill the learning store.
|
|
1287
|
+
content: `daemon worker ${result.type} ${result.success ? 'succeeded' : `failed: ${String(result.error ?? 'unknown error').split('\n')[0].slice(0, 200)}`}`,
|
|
1288
|
+
metadata: {
|
|
1289
|
+
source: 'worker-daemon',
|
|
1290
|
+
worker: result.type,
|
|
1291
|
+
success: result.success,
|
|
1292
|
+
durationMs: result.durationMs,
|
|
1293
|
+
},
|
|
1294
|
+
timestamp: Date.now(),
|
|
1295
|
+
}], result.success ? 'success' : 'failure');
|
|
1296
|
+
}
|
|
1297
|
+
catch (error) {
|
|
1298
|
+
this.log('warn', `Worker ${result.type}: trajectory recording skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1264
1301
|
/**
|
|
1265
1302
|
* Run a function with timeout (P1 fix)
|
|
1266
1303
|
* @param fn - The async function to execute
|
|
@@ -251,9 +251,12 @@ export class PatternDiscovery {
|
|
|
251
251
|
* Get built-in genesis registry (always available offline)
|
|
252
252
|
*/
|
|
253
253
|
getGenesisRegistry(cid) {
|
|
254
|
+
// #4157: this is an offline placeholder, so it claims nothing it cannot back: no
|
|
255
|
+
// verification, no signature, no popularity numbers, and fixed (not "now") dates.
|
|
254
256
|
return {
|
|
257
|
+
source: 'builtin-fallback',
|
|
255
258
|
version: '1.0.0',
|
|
256
|
-
updatedAt:
|
|
259
|
+
updatedAt: '2026-01-08T18:42:31.126Z',
|
|
257
260
|
ipnsName: 'k51qzi5uqu5dj0w8q1xvqn8ql2g4p7x8qpk9vz3xm1y2n3o4p5q6r7s8t9u0v',
|
|
258
261
|
previousCid: undefined,
|
|
259
262
|
patterns: [
|
|
@@ -269,25 +272,24 @@ export class PatternDiscovery {
|
|
|
269
272
|
author: {
|
|
270
273
|
id: 'claude-flow-team',
|
|
271
274
|
displayName: 'Claude Flow Team',
|
|
272
|
-
verified:
|
|
275
|
+
verified: false,
|
|
273
276
|
patterns: 1,
|
|
274
|
-
totalDownloads:
|
|
277
|
+
totalDownloads: 0,
|
|
275
278
|
},
|
|
276
279
|
license: 'MIT',
|
|
277
280
|
categories: ['routing', 'coordination'],
|
|
278
281
|
tags: ['genesis', 'foundational', 'routing', 'swarm', 'coordination', 'multi-agent', 'hello-world'],
|
|
279
282
|
language: 'typescript',
|
|
280
283
|
framework: 'claude-flow',
|
|
281
|
-
downloads:
|
|
282
|
-
rating:
|
|
283
|
-
ratingCount:
|
|
284
|
-
lastUpdated:
|
|
284
|
+
downloads: 0,
|
|
285
|
+
rating: 0,
|
|
286
|
+
ratingCount: 0,
|
|
287
|
+
lastUpdated: '2026-01-08T18:42:31.126Z',
|
|
285
288
|
createdAt: '2026-01-08T18:42:31.126Z',
|
|
286
289
|
minClaudeFlowVersion: '3.0.0',
|
|
287
|
-
verified:
|
|
288
|
-
trustLevel: '
|
|
289
|
-
|
|
290
|
-
publicKey: 'ed25519:claude-flow-team-key',
|
|
290
|
+
verified: false,
|
|
291
|
+
trustLevel: 'unverified',
|
|
292
|
+
source: 'builtin-fallback',
|
|
291
293
|
},
|
|
292
294
|
],
|
|
293
295
|
categories: [
|
|
@@ -301,20 +303,17 @@ export class PatternDiscovery {
|
|
|
301
303
|
{
|
|
302
304
|
id: 'claude-flow-team',
|
|
303
305
|
displayName: 'Claude Flow Team',
|
|
304
|
-
|
|
305
|
-
verified: true,
|
|
306
|
+
verified: false,
|
|
306
307
|
patterns: 1,
|
|
307
|
-
totalDownloads:
|
|
308
|
+
totalDownloads: 0,
|
|
308
309
|
},
|
|
309
310
|
],
|
|
310
311
|
totalPatterns: 1,
|
|
311
|
-
totalDownloads:
|
|
312
|
+
totalDownloads: 0,
|
|
312
313
|
totalAuthors: 1,
|
|
313
314
|
featured: ['seraphine-genesis-v1'],
|
|
314
315
|
trending: ['seraphine-genesis-v1'],
|
|
315
316
|
newest: ['seraphine-genesis-v1'],
|
|
316
|
-
registrySignature: crypto.randomBytes(32).toString('hex'),
|
|
317
|
-
registryPublicKey: 'ed25519:claude-flow-registry-key',
|
|
318
317
|
};
|
|
319
318
|
}
|
|
320
319
|
/**
|
|
@@ -32,6 +32,8 @@ export interface PatternEntry {
|
|
|
32
32
|
trustLevel: TrustLevel;
|
|
33
33
|
signature?: string;
|
|
34
34
|
publicKey?: string;
|
|
35
|
+
/** Where the entry came from. 'builtin-fallback' = the offline placeholder, not live registry data (#4157). */
|
|
36
|
+
source?: 'registry' | 'builtin-fallback';
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Pattern author info
|
|
@@ -60,6 +62,8 @@ export interface PatternCategory {
|
|
|
60
62
|
* Stored on IPFS with IPNS pointer for updates
|
|
61
63
|
*/
|
|
62
64
|
export interface PatternRegistry {
|
|
65
|
+
/** 'builtin-fallback' when discovery could not reach a registry and used the offline placeholder (#4157). */
|
|
66
|
+
source?: 'registry' | 'builtin-fallback';
|
|
63
67
|
version: string;
|
|
64
68
|
updatedAt: string;
|
|
65
69
|
ipnsName: string;
|
|
File without changes
|
|
File without changes
|