@claude-flow/cli 3.56.10 → 3.57.1
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 +2 -2
- package/README.md +12 -3
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/daemon.js +8 -2
- package/dist/src/commands/doctor.js +95 -18
- package/dist/src/commands/policy.js +6 -1
- package/dist/src/index.js +5 -5
- package/dist/src/init/settings-generator.js +4 -58
- package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
- package/dist/src/mcp-tools/memory-tools.js +30 -5
- package/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/dist/src/mcp-tools/neural-tools.js +20 -50
- package/dist/src/mcp-tools/policy-tools.js +5 -3
- package/dist/src/memory/intelligence.js +80 -33
- package/dist/src/memory/stats-file-lock.d.ts +7 -0
- package/dist/src/memory/stats-file-lock.js +53 -0
- package/dist/src/missions/schemas.d.ts +113 -113
- package/dist/src/mods/command-registry/schema.d.ts +193 -193
- 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 +1 -0
- package/dist/src/services/policy-runtime.js +261 -40
- 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/input-validator.d.ts +6 -6
- 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 +3 -3
- package/plugins/ruflo-console/dist/tui.mjs +153 -153
- package/plugins/ruflo-metaharness/hooks/screen.ts +7 -1
- package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
- package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +0 -179
- package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +0 -1
- package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +0 -32
- package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +0 -23
- package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +0 -11
- package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +0 -16
- package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +0 -91
- package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +0 -17
- package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +0 -12
- package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +0 -8
- package/plugins/ruflo-metaharness/.claude-flow/neural/patterns.json +0 -2809
- package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +0 -6
- package/plugins/ruflo-metaharness/.claude-flow/policy/state.json +0 -11
- package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-shm +0 -0
- package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-wal +0 -0
- package/plugins/ruflo-metaharness/.swarm/backups/memory-2026-10-14T16-09-38-164Z.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/backups/store-agentdb-memory.db-2026-10-14T16-09-38-166Z.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/ewc-fisher.json +0 -397
- package/plugins/ruflo-metaharness/.swarm/memory.db +0 -0
- package/plugins/ruflo-metaharness/.swarm/memory.db-shm +0 -0
- package/plugins/ruflo-metaharness/.swarm/memory.db-wal +0 -0
- package/plugins/ruflo-metaharness/agentdb.rvf +0 -0
- package/plugins/ruflo-metaharness/agentdb.rvf.lock +0 -0
- package/plugins/ruflo-metaharness/ruvector.db +0 -0
|
@@ -3,20 +3,34 @@
|
|
|
3
3
|
// callback server (an ESM `node:http` import that materialises http's lazy
|
|
4
4
|
// undici getters), bcryptjs, keychain and credential helpers that the policy
|
|
5
5
|
// runtime never uses.
|
|
6
|
-
import { AgenticPolicyEngine, createLegacyCompatibleState, } from '@claude-flow/security/policy/index';
|
|
6
|
+
import { AgenticPolicyEngine, POLICY_STATE_VERSION, createLegacyCompatibleState, } from '@claude-flow/security/policy/index';
|
|
7
7
|
import { isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security/mcp-caller-identity';
|
|
8
8
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
9
9
|
import { execFileSync } from 'node:child_process';
|
|
10
|
-
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';
|
|
11
11
|
import { dirname, join, resolve } from 'node:path';
|
|
12
12
|
import { hostname, userInfo } from 'node:os';
|
|
13
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';
|
|
14
16
|
import { assessAnchors, policyTrustRoot, recordAnchor } from './policy-ledger-anchor.js';
|
|
15
17
|
const POLICY_DIR = join('.claude-flow', 'policy');
|
|
16
18
|
const POLICY_FILE = 'state.json';
|
|
17
19
|
const LOCK_FILE = 'state.lock';
|
|
18
|
-
const
|
|
20
|
+
const RECEIPT_ARCHIVE_FILE = 'receipts.ledger.jsonl';
|
|
19
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
|
+
}
|
|
20
34
|
// A lock is ~170 bytes plus the hostname (at most 255 bytes).
|
|
21
35
|
const LOCK_MAX_BYTES = 1_024;
|
|
22
36
|
// Linux PID namespace of this process, e.g. `pid:[4026531836]`; undefined on
|
|
@@ -45,7 +59,7 @@ const BOOT_ID = (() => {
|
|
|
45
59
|
function paths(projectRoot) {
|
|
46
60
|
const root = resolve(projectRoot);
|
|
47
61
|
const dir = join(root, POLICY_DIR);
|
|
48
|
-
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) };
|
|
49
63
|
}
|
|
50
64
|
function sleep(ms) {
|
|
51
65
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
@@ -68,7 +82,7 @@ function isPidAlive(pid) {
|
|
|
68
82
|
/**
|
|
69
83
|
* Reads a lock only if it is a small regular file: never follows a symlink,
|
|
70
84
|
* never blocks on a FIFO or device, never reads more than LOCK_MAX_BYTES.
|
|
71
|
-
* Anything else returns undefined
|
|
85
|
+
* Anything else returns undefined; unknown owners are preserved.
|
|
72
86
|
*/
|
|
73
87
|
function readLockFile(lockPath) {
|
|
74
88
|
if (!lstatSync(lockPath).isFile())
|
|
@@ -94,8 +108,8 @@ function readLockFile(lockPath) {
|
|
|
94
108
|
* boot and in this PID namespace: anywhere else (a bwrap --unshare-pid
|
|
95
109
|
* sandbox, a container, another machine on a shared filesystem) a live owner
|
|
96
110
|
* also answers ESRCH. Locks without that identity (older ruflo, a crash
|
|
97
|
-
* between create and write, anything unparsable) return false and
|
|
98
|
-
*
|
|
111
|
+
* between create and write, anything unparsable) return false and require
|
|
112
|
+
* explicit operator inspection rather than age-only takeover.
|
|
99
113
|
*/
|
|
100
114
|
function lockOwnerIsDead(lockPath) {
|
|
101
115
|
try {
|
|
@@ -127,6 +141,36 @@ function lockOwnerIsDead(lockPath) {
|
|
|
127
141
|
return false;
|
|
128
142
|
}
|
|
129
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
|
+
}
|
|
130
174
|
/**
|
|
131
175
|
* Exported for ADR-406 mission storage, which takes the same per-file lock
|
|
132
176
|
* (pid, pid namespace and boot id identify a dead owner) rather than a copy.
|
|
@@ -136,33 +180,37 @@ export async function acquireLock(lockPath) {
|
|
|
136
180
|
while (Date.now() - started < LOCK_WAIT_MS) {
|
|
137
181
|
try {
|
|
138
182
|
const fd = openSync(lockPath, 'wx', 0o600);
|
|
139
|
-
|
|
183
|
+
const ownership = JSON.stringify({
|
|
140
184
|
pid: process.pid,
|
|
141
185
|
acquiredAt: Date.now(),
|
|
142
186
|
host: hostname(),
|
|
143
187
|
platform: process.platform,
|
|
144
188
|
pidns: PID_NAMESPACE,
|
|
145
189
|
boot: BOOT_ID,
|
|
146
|
-
|
|
147
|
-
|
|
190
|
+
nonce: randomUUID(),
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(fd, ownership);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
closeSync(fd);
|
|
197
|
+
}
|
|
148
198
|
return () => {
|
|
149
199
|
try {
|
|
150
|
-
|
|
200
|
+
if (readLockFile(lockPath) === ownership)
|
|
201
|
+
unlinkSync(lockPath);
|
|
151
202
|
}
|
|
152
203
|
catch { /* already released */ }
|
|
153
204
|
};
|
|
154
205
|
}
|
|
155
206
|
catch {
|
|
156
|
-
|
|
157
|
-
if (lockOwnerIsDead(lockPath)
|
|
158
|
-
|| Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
|
|
159
|
-
unlinkSync(lockPath);
|
|
160
|
-
}
|
|
161
|
-
catch { /* another process changed the lock */ }
|
|
207
|
+
reclaimDeadLock(lockPath);
|
|
162
208
|
await sleep(10);
|
|
163
209
|
}
|
|
164
210
|
}
|
|
165
|
-
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');
|
|
166
214
|
}
|
|
167
215
|
// #3398: Windows rename over a file another process holds open fails with
|
|
168
216
|
// EPERM/EBUSY/EACCES. Retry (~1.3s total, inside LOCK_WAIT_MS), never leave the
|
|
@@ -170,17 +218,129 @@ export async function acquireLock(lockPath) {
|
|
|
170
218
|
// and consumed approval uses, so dropping it would let an approval be reused.
|
|
171
219
|
const RENAME_RETRY_DELAYS_MS = [25, 50, 100, 150, 250, 300, 400];
|
|
172
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
|
+
}
|
|
173
323
|
async function writeJsonAtomic(file, value) {
|
|
174
|
-
|
|
324
|
+
mkdirDurably(dirname(file));
|
|
175
325
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
176
326
|
let renamed = false;
|
|
177
327
|
try {
|
|
178
|
-
|
|
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
|
+
}
|
|
179
339
|
for (let attempt = 0;; attempt++) {
|
|
180
340
|
try {
|
|
181
341
|
renameSync(temporary, file);
|
|
182
342
|
renamed = true;
|
|
183
|
-
|
|
343
|
+
break;
|
|
184
344
|
}
|
|
185
345
|
catch (error) {
|
|
186
346
|
const code = error?.code ?? '';
|
|
@@ -189,6 +349,9 @@ async function writeJsonAtomic(file, value) {
|
|
|
189
349
|
await sleep(RENAME_RETRY_DELAYS_MS[attempt]);
|
|
190
350
|
}
|
|
191
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));
|
|
192
355
|
}
|
|
193
356
|
finally {
|
|
194
357
|
if (!renamed) {
|
|
@@ -210,8 +373,16 @@ function trustKey(projectRoot, create) {
|
|
|
210
373
|
if (!existsSync(key)) {
|
|
211
374
|
if (!create)
|
|
212
375
|
return undefined;
|
|
213
|
-
|
|
214
|
-
|
|
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));
|
|
215
386
|
}
|
|
216
387
|
const material = readFileSync(key);
|
|
217
388
|
if (material.length !== 32)
|
|
@@ -238,11 +409,11 @@ function verifyStateAnchor(projectRoot, state) {
|
|
|
238
409
|
throw new Error('policy-state-authentication-failed');
|
|
239
410
|
}
|
|
240
411
|
}
|
|
241
|
-
async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by) {
|
|
412
|
+
async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by, assessedTip) {
|
|
242
413
|
await writePolicyStateFiles(projectRoot, statePath, state);
|
|
243
414
|
// #3602: the second anchor is written after the state, so a crash leaves the
|
|
244
415
|
// state AHEAD of the log (caught up on the next write), never behind it.
|
|
245
|
-
await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by);
|
|
416
|
+
await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by, writeJsonAtomic, assessedTip);
|
|
246
417
|
// ADR-404: the ruflo mod reads Claude Code tool rules from a small
|
|
247
418
|
// projection, never from state.json. Written only after the state and its
|
|
248
419
|
// anchor are safely down; a failure here never fails the state write. A
|
|
@@ -316,7 +487,7 @@ export function loadPolicyState(projectRoot = process.cwd()) {
|
|
|
316
487
|
return createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
|
|
317
488
|
}
|
|
318
489
|
const parsed = JSON.parse(readFileSync(target.state, 'utf8'));
|
|
319
|
-
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)) {
|
|
320
491
|
throw new Error(`unsupported-policy-state-version:${String(parsed.version)}`);
|
|
321
492
|
}
|
|
322
493
|
verifyStateAnchor(projectRoot, parsed);
|
|
@@ -337,7 +508,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
|
|
|
337
508
|
if (!existsSync(join(resolve(projectRoot), '.claude-flow'))
|
|
338
509
|
&& !existsSync(join(resolve(projectRoot), '.swarm')))
|
|
339
510
|
return { migrated: false };
|
|
340
|
-
|
|
511
|
+
mkdirDurably(target.dir);
|
|
341
512
|
const release = await acquireLock(target.lock);
|
|
342
513
|
try {
|
|
343
514
|
if (!existsSync(target.state)) {
|
|
@@ -357,7 +528,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
|
|
|
357
528
|
}
|
|
358
529
|
export async function withPolicyTransaction(projectRoot, operation, options = {}) {
|
|
359
530
|
const target = paths(projectRoot);
|
|
360
|
-
|
|
531
|
+
mkdirDurably(target.dir);
|
|
361
532
|
const release = await acquireLock(target.lock);
|
|
362
533
|
try {
|
|
363
534
|
const loaded = loadPolicyState(projectRoot);
|
|
@@ -376,10 +547,30 @@ export async function withPolicyTransaction(projectRoot, operation, options = {}
|
|
|
376
547
|
: gate.result.error ?? 'policy-ledger-verification-failed');
|
|
377
548
|
}
|
|
378
549
|
const result = await operation(engine);
|
|
379
|
-
|
|
550
|
+
const verified = engine.verifyLedger();
|
|
551
|
+
if (!verified.valid)
|
|
380
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
|
+
}
|
|
381
572
|
const nextState = engine.exportState();
|
|
382
|
-
await writePolicyState(projectRoot, target.state, nextState, gate.event);
|
|
573
|
+
await writePolicyState(projectRoot, target.state, nextState, gate.event, undefined, gate.anchorTip);
|
|
383
574
|
return result;
|
|
384
575
|
}
|
|
385
576
|
finally {
|
|
@@ -439,7 +630,9 @@ export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
|
|
|
439
630
|
*/
|
|
440
631
|
function reconcileLedger(projectRoot, engine, options) {
|
|
441
632
|
const state = engine.exportState();
|
|
442
|
-
|
|
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;
|
|
443
636
|
const primary = typeof state.ledgerLength === 'number';
|
|
444
637
|
const base = engine.verifyLedger();
|
|
445
638
|
const fail = (error) => ({ result: { valid: false, length, error }, persist: false });
|
|
@@ -454,7 +647,7 @@ function reconcileLedger(projectRoot, engine, options) {
|
|
|
454
647
|
? { result: { ...base, secondaryAnchor: 'established-explicitly' }, persist: true, event: 'establish-anchor' }
|
|
455
648
|
: { result: { ...base, secondaryAnchor: 'recorded-from-state' }, persist: true, event: 'migrated-from-state' };
|
|
456
649
|
}
|
|
457
|
-
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 };
|
|
458
651
|
}
|
|
459
652
|
// No primary anchor.
|
|
460
653
|
if (length === 0)
|
|
@@ -463,7 +656,7 @@ function reconcileLedger(projectRoot, engine, options) {
|
|
|
463
656
|
// The second anchor is authentic and agrees with the chain: the deleted
|
|
464
657
|
// primary can be restored from it. Truncation was already rejected above.
|
|
465
658
|
engine.verifyLedger({ establishAnchor: true });
|
|
466
|
-
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 };
|
|
467
660
|
}
|
|
468
661
|
if (options.establishAnchor) {
|
|
469
662
|
const established = engine.verifyLedger({ establishAnchor: true });
|
|
@@ -483,23 +676,51 @@ function reconcileLedger(projectRoot, engine, options) {
|
|
|
483
676
|
*/
|
|
484
677
|
export async function verifyPolicyLedger(projectRoot = process.cwd(), options = {}) {
|
|
485
678
|
const target = paths(projectRoot);
|
|
486
|
-
|
|
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.
|
|
487
692
|
const release = await acquireLock(target.lock);
|
|
488
693
|
try {
|
|
489
|
-
const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot),
|
|
490
|
-
signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
|
|
491
|
-
keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
|
|
492
|
-
});
|
|
694
|
+
const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), engineOptions);
|
|
493
695
|
const outcome = reconcileLedger(projectRoot, engine, options);
|
|
494
696
|
if (outcome.persist) {
|
|
495
697
|
const by = outcome.event === 'establish-anchor' ? userInfo().username : undefined;
|
|
496
|
-
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
|
+
}
|
|
497
709
|
}
|
|
498
|
-
return outcome.result;
|
|
499
710
|
}
|
|
500
711
|
finally {
|
|
501
712
|
release();
|
|
502
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
|
+
}
|
|
503
724
|
}
|
|
504
725
|
/**
|
|
505
726
|
* Resolve the caller identity for `authorizeMcpTool`.
|
|
File without changes
|
|
File without changes
|
|
@@ -159,14 +159,14 @@ export declare const SpawnAgentSchema: z.ZodObject<{
|
|
|
159
159
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
160
160
|
}, "strip", z.ZodTypeAny, {
|
|
161
161
|
type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
|
|
162
|
-
id?: string | undefined;
|
|
163
162
|
config?: Record<string, unknown> | undefined;
|
|
164
163
|
timeout?: number | undefined;
|
|
164
|
+
id?: string | undefined;
|
|
165
165
|
}, {
|
|
166
166
|
type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
|
|
167
|
-
id?: string | undefined;
|
|
168
167
|
config?: Record<string, unknown> | undefined;
|
|
169
168
|
timeout?: number | undefined;
|
|
169
|
+
id?: string | undefined;
|
|
170
170
|
}>;
|
|
171
171
|
/**
|
|
172
172
|
* Task input schema
|
|
@@ -181,13 +181,13 @@ export declare const TaskInputSchema: z.ZodObject<{
|
|
|
181
181
|
taskId: string;
|
|
182
182
|
content: string;
|
|
183
183
|
agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
|
|
184
|
-
priority?: "
|
|
184
|
+
priority?: "critical" | "high" | "medium" | "low" | undefined;
|
|
185
185
|
metadata?: Record<string, unknown> | undefined;
|
|
186
186
|
}, {
|
|
187
187
|
taskId: string;
|
|
188
188
|
content: string;
|
|
189
189
|
agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
|
|
190
|
-
priority?: "
|
|
190
|
+
priority?: "critical" | "high" | "medium" | "low" | undefined;
|
|
191
191
|
metadata?: Record<string, unknown> | undefined;
|
|
192
192
|
}>;
|
|
193
193
|
/**
|
|
@@ -234,16 +234,16 @@ export declare const ExecutorConfigSchema: z.ZodObject<{
|
|
|
234
234
|
cwd: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
235
235
|
allowSudo: z.ZodDefault<z.ZodBoolean>;
|
|
236
236
|
}, "strip", z.ZodTypeAny, {
|
|
237
|
-
timeout: number;
|
|
238
237
|
allowedCommands: string[];
|
|
238
|
+
timeout: number;
|
|
239
239
|
maxBuffer: number;
|
|
240
240
|
allowSudo: boolean;
|
|
241
241
|
blockedPatterns?: string[] | undefined;
|
|
242
242
|
cwd?: string | undefined;
|
|
243
243
|
}, {
|
|
244
244
|
allowedCommands: string[];
|
|
245
|
-
timeout?: number | undefined;
|
|
246
245
|
blockedPatterns?: string[] | undefined;
|
|
246
|
+
timeout?: number | undefined;
|
|
247
247
|
maxBuffer?: number | undefined;
|
|
248
248
|
cwd?: string | undefined;
|
|
249
249
|
allowSudo?: boolean | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BudgetLimit, LedgerVerification, PolicyApproval, PolicyDecision, PolicyEngineOptions, PolicyRequest, PolicyRule, PolicyState } from './types.js';
|
|
1
|
+
import type { BudgetLimit, LedgerVerification, PolicyApproval, PolicyDecision, PolicyEngineOptions, PolicyReceipt, PolicyRequest, PolicyRule, PolicyState } from './types.js';
|
|
2
2
|
export declare class AgenticPolicyEngine {
|
|
3
3
|
private readonly now;
|
|
4
4
|
private readonly signingKey?;
|
|
@@ -19,6 +19,12 @@ export declare class AgenticPolicyEngine {
|
|
|
19
19
|
}): PolicyApproval;
|
|
20
20
|
revokeApproval(id: string): boolean;
|
|
21
21
|
evaluate(request: PolicyRequest): PolicyDecision;
|
|
22
|
+
/** Authenticate each physical record before callers deduplicate it.
|
|
23
|
+
* Receipts use one configured key and key ID for this ledger. For existing
|
|
24
|
+
* custom-ID history, restore the original CLAUDE_FLOW_POLICY_KEY_ID when
|
|
25
|
+
* verifying; never rewrite or re-sign history to change its identity.
|
|
26
|
+
* Key rotation/multi-key historical verification is not supported here. */
|
|
27
|
+
verifyReceipt(receipt: unknown): string | undefined;
|
|
22
28
|
/**
|
|
23
29
|
* `establishAnchor` is the ONLY way to create an anchor over receipts that
|
|
24
30
|
* already exist (#3602). A caller must have decided, outside the state file,
|
|
@@ -30,6 +36,16 @@ export declare class AgenticPolicyEngine {
|
|
|
30
36
|
verifyLedger(options?: {
|
|
31
37
|
establishAnchor?: boolean;
|
|
32
38
|
}): LedgerVerification;
|
|
39
|
+
/**
|
|
40
|
+
* Removes the oldest receipts from the hot `receipts` array, keeping only
|
|
41
|
+
* `retain` of the most recent ones in memory (#3164). Pure and in-memory —
|
|
42
|
+
* this class stays side-effect-free; the runtime layer archives the
|
|
43
|
+
* returned receipts to disk and persists the resulting state. Requires an
|
|
44
|
+
* established anchor first: pruning a ledger whose historical length isn't
|
|
45
|
+
* externally anchored would make a future `verifyLedger` unable to tell
|
|
46
|
+
* truncation-by-attacker apart from legitimate pruning.
|
|
47
|
+
*/
|
|
48
|
+
pruneReceipts(retain: number): PolicyReceipt[];
|
|
33
49
|
private applyBudget;
|
|
34
50
|
private applyApproval;
|
|
35
51
|
private consumeBudget;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/policy/engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/policy/engine.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAElB,cAAc,EACd,cAAc,EAEd,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AASpB,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAkB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAgE;IAClG,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAA8B;IACtE,OAAO,CAAC,KAAK,CAAc;gBAEf,OAAO,GAAE,mBAAwB;IAiB7C,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAM,GAAG,mBAAmB;IA2BrK,WAAW,IAAI,WAAW;IAI1B,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI;IAIxC,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI;IAMlD,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAgBlC,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAWnC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,UAAU,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,cAAc;IAmBzH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAOnC,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc;IAoBhD;;;;+EAI2E;IAC3E,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IA4BnD;;;;;;;OAOG;IACH,YAAY,CAAC,OAAO,GAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,kBAAkB;IA8C7E;;;;;;;;OAQG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE;IAW9C,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,aAAa;IAyCrB,OAAO,CAAC,eAAe;CAcxB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,SAAgB,GAAG,WAAW,CAY/E"}
|