@claude-flow/cli 3.32.25 → 3.32.29
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/auto-memory-hook.mjs +430 -430
- package/.claude/helpers/helpers.manifest.json +6 -6
- package/.claude/helpers/hook-handler.cjs +565 -565
- package/.claude/helpers/intelligence.cjs +1058 -1058
- package/.claude/helpers/statusline.cjs +1060 -1060
- package/catalog-manifest.json +4 -4
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.js +5 -3
- package/dist/src/commands/memory.js +49 -5
- package/dist/src/commands/metaharness.js +100 -2
- package/dist/src/commands/policy.d.ts +4 -0
- package/dist/src/commands/policy.js +107 -0
- package/dist/src/index.js +18 -0
- package/dist/src/mcp-client.js +25 -1
- package/dist/src/mcp-tools/capability-brain.d.ts +134 -0
- package/dist/src/mcp-tools/capability-brain.js +697 -0
- package/dist/src/mcp-tools/guidance-tools.d.ts +2 -0
- package/dist/src/mcp-tools/guidance-tools.js +369 -37
- package/dist/src/mcp-tools/index.d.ts +4 -1
- package/dist/src/mcp-tools/index.js +3 -1
- package/dist/src/mcp-tools/memory-tools.js +26 -0
- package/dist/src/mcp-tools/metaharness-tools.js +106 -1
- package/dist/src/mcp-tools/policy-tools.d.ts +3 -0
- package/dist/src/mcp-tools/policy-tools.js +121 -0
- package/dist/src/memory/memory-bridge.d.ts +11 -0
- package/dist/src/memory/memory-bridge.js +100 -21
- package/dist/src/memory/memory-initializer.d.ts +22 -1
- package/dist/src/memory/memory-initializer.js +184 -39
- package/dist/src/services/bounded-worker-pool.d.ts +28 -0
- package/dist/src/services/bounded-worker-pool.js +90 -0
- package/dist/src/services/flywheel-proposer.d.ts +87 -0
- package/dist/src/services/flywheel-proposer.js +165 -0
- package/dist/src/services/flywheel-receipt.d.ts +136 -0
- package/dist/src/services/flywheel-receipt.js +309 -0
- package/dist/src/services/flywheel-transaction.d.ts +77 -0
- package/dist/src/services/flywheel-transaction.js +378 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +11 -0
- package/dist/src/services/harness-flywheel-runtime.js +85 -2
- package/dist/src/services/harness-flywheel.d.ts +27 -1
- package/dist/src/services/harness-flywheel.js +138 -27
- package/dist/src/services/policy-runtime.d.ts +38 -0
- package/dist/src/services/policy-runtime.js +340 -0
- package/package.json +23 -5
- package/plugins/ruflo-metaharness/scripts/smoke.sh +22 -14
- package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +3 -1
- package/.claude/.proven-config-version +0 -1
- package/.claude/proven-config.json +0 -42
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
* 4. GATE the winner through the shipped runHarnessLoop on the HELD-OUT split:
|
|
12
12
|
* held_out_improves AND redblue(anchor-no-regress) AND drift<=thr AND
|
|
13
13
|
* replay-deterministic AND receipt_coverage AND canary-no-worse.
|
|
14
|
-
* 5. On accept →
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the unsigned champion for optional promotion to the signed global channel.
|
|
14
|
+
* 5. On accept → emit + persist an immutable evaluation receipt. Evaluation
|
|
15
|
+
* never mutates active policy. Explicit promotion is handled by the
|
|
16
|
+
* ADR-322A transaction service.
|
|
18
17
|
*
|
|
19
18
|
* Trust split: LOCAL self-optimization is unsigned (an install trusting its own
|
|
20
19
|
* measured gate); CROSS-install propagation still requires the config-signed
|
|
@@ -27,6 +26,9 @@ import { hashCorpus } from './harness-benchmark.js';
|
|
|
27
26
|
import { harvestSelfSupervisedTasks, blendCorpus } from './harness-corpus-harvester.js';
|
|
28
27
|
import { applyChampionParams } from '../config/harness-feedback-applier.js';
|
|
29
28
|
import { appendLedger, bootstrapDeltaCILow } from './harness-improvement-ledger.js';
|
|
29
|
+
import { createFlywheelReceipt, sha256Ref, } from './flywheel-receipt.js';
|
|
30
|
+
import { readFlywheelTransactionState, registerFlywheelReceipt, } from './flywheel-transaction.js';
|
|
31
|
+
import { runBoundedPool } from './bounded-worker-pool.js';
|
|
30
32
|
export const DEFAULT_CONFIG = { alpha: 0.5, subjectWeight: 2.0, mmrLambda: 0.7, bodyWeight: 1.0, typePenaltyFactor: 1.0 };
|
|
31
33
|
const EPS = 1e-3;
|
|
32
34
|
const cfgCanon = (c) => JSON.stringify(Object.fromEntries(Object.keys(c).sort().map((k) => [k, c[k]])));
|
|
@@ -50,7 +52,7 @@ function grade(ranked, expected) {
|
|
|
50
52
|
const idx = ranked.findIndex((r) => r.id === expected);
|
|
51
53
|
return idx >= 0 ? 1 / (idx + 1) : 0;
|
|
52
54
|
}
|
|
53
|
-
function
|
|
55
|
+
export function retrievalPolicyNeighbors(base) {
|
|
54
56
|
const steps = { alpha: 0.1, subjectWeight: 0.5, mmrLambda: 0.1, bodyWeight: 0.5, typePenaltyFactor: 0.25 };
|
|
55
57
|
const out = [];
|
|
56
58
|
const seen = new Set();
|
|
@@ -89,7 +91,7 @@ function split(tasks, frac) {
|
|
|
89
91
|
* Returns a rich result AND (as a side effect) appends to the improvement ledger
|
|
90
92
|
* and — on accept — applies the champion locally + chains it.
|
|
91
93
|
*/
|
|
92
|
-
export async function
|
|
94
|
+
export async function evaluateFlywheelCandidate(projectRoot, deps) {
|
|
93
95
|
try {
|
|
94
96
|
const patterns = await deps.getPatterns();
|
|
95
97
|
if (!patterns || patterns.length < 8)
|
|
@@ -100,7 +102,9 @@ export async function runFlywheelTick(projectRoot, deps) {
|
|
|
100
102
|
const blended = blendCorpus(deps.anchorTasks, harvested);
|
|
101
103
|
const anchorIdSet = new Set(blended.anchorIds);
|
|
102
104
|
const baseline = { ...DEFAULT_CONFIG, ...(deps.activeParams?.() ?? {}) };
|
|
103
|
-
const candidates =
|
|
105
|
+
const candidates = deps.candidatePolicies?.length
|
|
106
|
+
? deps.candidatePolicies.map((candidate) => ({ ...candidate }))
|
|
107
|
+
: retrievalPolicyNeighbors(baseline);
|
|
104
108
|
// OBJECTIVE = the human-labeled anchor (the relevance we actually care about,
|
|
105
109
|
// where headroom is known to exist). GUARD = the large, growing harvested set
|
|
106
110
|
// (don't wreck broad retrieval while tuning the objective). Optimize the
|
|
@@ -112,12 +116,32 @@ export async function runFlywheelTick(projectRoot, deps) {
|
|
|
112
116
|
// Precompute retrieval for baseline + all candidates over every task (async
|
|
113
117
|
// I/O up front → the harness scoring stays pure/sync).
|
|
114
118
|
const cache = new Map();
|
|
115
|
-
const configs = [baseline, ...candidates];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
const configs = [...new Map([baseline, ...candidates].map((config) => [cfgKey(config), config])).values()];
|
|
120
|
+
const evalTasks = configs.flatMap((cfg) => blended.tasks.map((t) => {
|
|
121
|
+
const cacheKey = `${t.id}::${cfgKey(cfg)}`;
|
|
122
|
+
return {
|
|
123
|
+
id: cacheKey,
|
|
124
|
+
run: async (signal) => {
|
|
125
|
+
if (signal.aborted)
|
|
126
|
+
throw signal.reason;
|
|
127
|
+
return (await deps.search(t.input.q, cfg, signal)) || [];
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}));
|
|
131
|
+
const batch = await runBoundedPool(evalTasks, {
|
|
132
|
+
maxConcurrency: deps.maxConcurrency ?? 2,
|
|
133
|
+
timeoutMs: deps.evaluationTimeoutMs ?? 120_000,
|
|
134
|
+
});
|
|
135
|
+
for (const item of batch.results) {
|
|
136
|
+
if (item.status === 'fulfilled')
|
|
137
|
+
cache.set(item.id, item.value ?? []);
|
|
138
|
+
}
|
|
139
|
+
const failedEvaluations = batch.results.filter((item) => item.status !== 'fulfilled');
|
|
140
|
+
if (failedEvaluations.length > 0) {
|
|
141
|
+
return {
|
|
142
|
+
ran: false,
|
|
143
|
+
reason: `candidate evaluation incomplete (${failedEvaluations.length}/${batch.results.length}); peak concurrency ${batch.peakConcurrency}`,
|
|
144
|
+
};
|
|
121
145
|
}
|
|
122
146
|
const evalFn = (input, cfg) => cache.get(`${input.id}::${cfgKey(cfg)}`) ?? [];
|
|
123
147
|
const gradeFn = (output, expected) => grade(output, expected);
|
|
@@ -171,7 +195,76 @@ export async function runFlywheelTick(projectRoot, deps) {
|
|
|
171
195
|
const heldDeltas = held.map((t) => heldScoreFor(candidate, t) - heldScoreFor(baseline, t));
|
|
172
196
|
const deltaCILow = bootstrapDeltaCILow(heldDeltas);
|
|
173
197
|
const significant = deltaCILow > 0;
|
|
174
|
-
const
|
|
198
|
+
const provisionalGates = Object.fromEntries(Object.entries(result.verdict?.terms ?? {}).map(([k, v]) => [k, v.pass]));
|
|
199
|
+
const txState = readFlywheelTransactionState(projectRoot);
|
|
200
|
+
const safetyEnvelopeRef = deps.safetyEnvelopeRef ?? sha256Ref(JSON.stringify({
|
|
201
|
+
schema: 'ruflo.safety-envelope/local-default-v1',
|
|
202
|
+
authorizationExpansion: false,
|
|
203
|
+
networkExpansion: false,
|
|
204
|
+
spendExpansion: false,
|
|
205
|
+
}));
|
|
206
|
+
const receipt = createFlywheelReceipt({
|
|
207
|
+
lineageId: deps.lineageId,
|
|
208
|
+
evaluationRunId: deps.evaluationRunId,
|
|
209
|
+
baselineRef: refOf(baseline),
|
|
210
|
+
expectedLedgerHead: txState.ledgerHead,
|
|
211
|
+
candidatePolicy: candidate,
|
|
212
|
+
safetyEnvelopeRef,
|
|
213
|
+
requestedProposer: deps.requestedProposer ?? 'local',
|
|
214
|
+
effectiveProposer: deps.effectiveProposer ?? 'local',
|
|
215
|
+
proposerSubstitution: deps.proposerSubstitution,
|
|
216
|
+
corpusVersion: blended.version,
|
|
217
|
+
corpusHash: blended.corpusHash,
|
|
218
|
+
baselineScore,
|
|
219
|
+
candidateScore,
|
|
220
|
+
heldOutDeltas: heldDeltas,
|
|
221
|
+
frozenAnchorRegression: guardRegressed ? 1 : 0,
|
|
222
|
+
gates: provisionalGates,
|
|
223
|
+
resourceEvidence: {
|
|
224
|
+
p95LatencyMicros: 0,
|
|
225
|
+
costMicrosPerTask: 0,
|
|
226
|
+
tokensPerTask: 0,
|
|
227
|
+
failureRate: '0',
|
|
228
|
+
evaluationCostMicros: 0,
|
|
229
|
+
currency: 'USD',
|
|
230
|
+
},
|
|
231
|
+
evidence: {
|
|
232
|
+
corpusRoles: {
|
|
233
|
+
selectionTaskIds: train.map((task) => task.id),
|
|
234
|
+
promotionHoldoutTaskIds: held.map((task) => task.id),
|
|
235
|
+
guardTaskIds: guard.map((task) => task.id),
|
|
236
|
+
},
|
|
237
|
+
verification: {
|
|
238
|
+
redblue: result.verify?.redblue ?? 'SKIPPED',
|
|
239
|
+
drift: result.verify?.drift ?? -1,
|
|
240
|
+
driftThreshold: result.verify?.driftThreshold ?? 0.2,
|
|
241
|
+
driftVerdict: result.verify?.driftVerdict ?? 'skipped',
|
|
242
|
+
adversarialPass: result.verify?.adversarialPass ?? false,
|
|
243
|
+
},
|
|
244
|
+
canary: {
|
|
245
|
+
candidate: result.canary?.candidate ?? {},
|
|
246
|
+
baseline: result.canary?.baseline ?? {},
|
|
247
|
+
pass: result.canary?.pass ?? false,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
termVerification: Object.keys(provisionalGates).map((term) => ({
|
|
251
|
+
term,
|
|
252
|
+
verification: 'recomputed',
|
|
253
|
+
evidenceRef: sha256Ref(JSON.stringify({
|
|
254
|
+
term,
|
|
255
|
+
corpusHash: blended.corpusHash,
|
|
256
|
+
heldOutDeltas: heldDeltas,
|
|
257
|
+
verification: result.verify,
|
|
258
|
+
canary: result.canary,
|
|
259
|
+
})),
|
|
260
|
+
})),
|
|
261
|
+
now: deps.now,
|
|
262
|
+
privateKeyPem: deps.receiptPrivateKeyPem,
|
|
263
|
+
publicKeyPem: deps.receiptPublicKeyPem,
|
|
264
|
+
bootstrapIterations: deps.bootstrapIterations,
|
|
265
|
+
});
|
|
266
|
+
await registerFlywheelReceipt(projectRoot, receipt, deps.now ?? Date.now());
|
|
267
|
+
const finalAccept = result.accepted && significant && receipt.payload.decision === 'accepted';
|
|
175
268
|
const entry = {
|
|
176
269
|
ts: deps.now ?? Date.now(),
|
|
177
270
|
corpusVersion: blended.version, corpusHash: blended.corpusHash,
|
|
@@ -180,28 +273,46 @@ export async function runFlywheelTick(projectRoot, deps) {
|
|
|
180
273
|
baselineScore, candidateScore, delta: candidateScore - baselineScore,
|
|
181
274
|
deltaCILow, significant, loopAccepted: result.accepted,
|
|
182
275
|
anchorRegressed, accepted: finalAccept,
|
|
183
|
-
gates:
|
|
276
|
+
gates: provisionalGates,
|
|
184
277
|
reason: finalAccept ? result.reason : (result.accepted ? `held back — improvement not significant (CI low ${deltaCILow.toFixed(4)})` : result.reason),
|
|
185
278
|
};
|
|
186
|
-
let applied = false;
|
|
187
|
-
if (finalAccept && result.manifest) {
|
|
188
|
-
entry.championRef = refOf(candidate);
|
|
189
|
-
// Apply locally (self-optimization) + chain to the previous champion.
|
|
190
|
-
const ap = applyChampionParams(projectRoot, {
|
|
191
|
-
championId: refOf(candidate), params: candidate,
|
|
192
|
-
layer: 'repo/local', previous: refOf(baseline), now: deps.now,
|
|
193
|
-
});
|
|
194
|
-
applied = ap.applied;
|
|
195
|
-
}
|
|
196
279
|
appendLedger(`${projectRoot}/.claude-flow/metrics`, entry);
|
|
197
280
|
return {
|
|
198
|
-
ran: true, reason: entry.reason, accepted: finalAccept, applied,
|
|
281
|
+
ran: true, reason: entry.reason, accepted: finalAccept, applied: false,
|
|
199
282
|
baselineScore, candidateScore, delta: candidateScore - baselineScore,
|
|
200
|
-
anchorRegressed, championRef:
|
|
283
|
+
anchorRegressed, championRef: finalAccept ? refOf(candidate) : undefined,
|
|
284
|
+
corpusVersion: blended.version, candidateConfig: candidate,
|
|
285
|
+
receiptId: receipt.payload.receiptId, receipt, promotable: finalAccept && !!receipt.signature,
|
|
201
286
|
};
|
|
202
287
|
}
|
|
203
288
|
catch (e) {
|
|
204
289
|
return { ran: false, reason: `error: ${e?.message ?? e}` };
|
|
205
290
|
}
|
|
206
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Compatibility wrapper. New callers get evaluation-only semantics. The legacy
|
|
294
|
+
* implicit apply path exists for one release behind an explicit opt-in flag.
|
|
295
|
+
*/
|
|
296
|
+
export async function runFlywheelTick(projectRoot, deps) {
|
|
297
|
+
const result = await evaluateFlywheelCandidate(projectRoot, deps);
|
|
298
|
+
if (process.env.RUFLO_FLYWHEEL_LEGACY_APPLY === '1'
|
|
299
|
+
&& result.accepted
|
|
300
|
+
&& result.candidateConfig
|
|
301
|
+
&& result.championRef) {
|
|
302
|
+
const applied = applyChampionParams(projectRoot, {
|
|
303
|
+
championId: result.championRef,
|
|
304
|
+
params: result.candidateConfig,
|
|
305
|
+
layer: 'repo/local',
|
|
306
|
+
previous: result.receipt?.payload.baselineRef,
|
|
307
|
+
now: deps.now,
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
...result,
|
|
311
|
+
applied: applied.applied,
|
|
312
|
+
legacyDeprecation: true,
|
|
313
|
+
reason: `${result.reason}; deprecated implicit apply path`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
207
318
|
//# sourceMappingURL=harness-flywheel.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AgenticPolicyEngine, type BudgetLimit, type CapabilityEnvelope, type PolicyApproval, type PolicyDecision, type PolicyRequest, type PolicyRule, type PolicyState } from '@claude-flow/security';
|
|
2
|
+
export declare function loadPolicyState(projectRoot?: string): PolicyState;
|
|
3
|
+
export declare function autoMigratePolicyStateIfNeeded(projectRoot?: string): Promise<{
|
|
4
|
+
migrated: boolean;
|
|
5
|
+
statePath?: string;
|
|
6
|
+
mode?: PolicyState['mode'];
|
|
7
|
+
}>;
|
|
8
|
+
export declare function withPolicyTransaction<T>(projectRoot: string, operation: (engine: AgenticPolicyEngine) => T | Promise<T>, options?: {
|
|
9
|
+
approvalIssuerVerifier?: (issuer: string) => boolean;
|
|
10
|
+
}): Promise<T>;
|
|
11
|
+
export declare function evaluatePolicyRequest(request: PolicyRequest, projectRoot?: string): Promise<PolicyDecision>;
|
|
12
|
+
export declare function setPolicyMode(mode: PolicyState['mode'], projectRoot?: string): Promise<void>;
|
|
13
|
+
export declare function upsertPolicyRule(rule: PolicyRule, projectRoot?: string): Promise<void>;
|
|
14
|
+
export declare function setPolicyBudget(limit: BudgetLimit, projectRoot?: string): Promise<void>;
|
|
15
|
+
export declare function issuePolicyApproval(approval: Omit<PolicyApproval, 'uses' | 'issuedAt'> & {
|
|
16
|
+
uses?: number;
|
|
17
|
+
issuedAt?: number;
|
|
18
|
+
}, projectRoot?: string, approvalIssuerVerifier?: (issuer: string) => boolean): Promise<PolicyApproval>;
|
|
19
|
+
export declare function revokePolicyApproval(id: string, projectRoot?: string): Promise<boolean>;
|
|
20
|
+
export declare function verifyPolicyLedger(projectRoot?: string): Promise<ReturnType<AgenticPolicyEngine['verifyLedger']>>;
|
|
21
|
+
export declare function authorizeMcpTool(toolName: string, input: Record<string, unknown>, context?: Record<string, unknown>, attributes?: Readonly<{
|
|
22
|
+
actionType?: string;
|
|
23
|
+
network?: boolean;
|
|
24
|
+
destructive?: boolean;
|
|
25
|
+
namespaceAccess?: 'read' | 'write';
|
|
26
|
+
envelope?: CapabilityEnvelope;
|
|
27
|
+
costUsd?: number;
|
|
28
|
+
tokens?: number;
|
|
29
|
+
concurrency?: number;
|
|
30
|
+
}>): Promise<PolicyDecision>;
|
|
31
|
+
/** Trusted classification derived from the registered tool name, never input. */
|
|
32
|
+
export declare function classifyMcpTool(toolName: string): {
|
|
33
|
+
actionType: string;
|
|
34
|
+
network: boolean;
|
|
35
|
+
destructive: boolean;
|
|
36
|
+
namespaceAccess?: 'read' | 'write';
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=policy-runtime.d.ts.map
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { AgenticPolicyEngine, createLegacyCompatibleState, } from '@claude-flow/security';
|
|
2
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { userInfo } from 'node:os';
|
|
7
|
+
const POLICY_DIR = join('.claude-flow', 'policy');
|
|
8
|
+
const POLICY_FILE = 'state.json';
|
|
9
|
+
const LOCK_FILE = 'state.lock';
|
|
10
|
+
const LOCK_STALE_MS = 30_000;
|
|
11
|
+
const LOCK_WAIT_MS = 5_000;
|
|
12
|
+
function paths(projectRoot) {
|
|
13
|
+
const root = resolve(projectRoot);
|
|
14
|
+
const dir = join(root, POLICY_DIR);
|
|
15
|
+
return { dir, state: join(dir, POLICY_FILE), lock: join(dir, LOCK_FILE) };
|
|
16
|
+
}
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
19
|
+
}
|
|
20
|
+
async function acquireLock(lockPath) {
|
|
21
|
+
const started = Date.now();
|
|
22
|
+
while (Date.now() - started < LOCK_WAIT_MS) {
|
|
23
|
+
try {
|
|
24
|
+
const fd = openSync(lockPath, 'wx', 0o600);
|
|
25
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
return () => {
|
|
28
|
+
try {
|
|
29
|
+
unlinkSync(lockPath);
|
|
30
|
+
}
|
|
31
|
+
catch { /* already released */ }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
try {
|
|
36
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
|
|
37
|
+
unlinkSync(lockPath);
|
|
38
|
+
}
|
|
39
|
+
catch { /* another process changed the lock */ }
|
|
40
|
+
await sleep(10);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error('policy-state-lock-timeout');
|
|
44
|
+
}
|
|
45
|
+
function writeJsonAtomic(file, value) {
|
|
46
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
47
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
48
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
49
|
+
renameSync(temporary, file);
|
|
50
|
+
}
|
|
51
|
+
function trustPaths(projectRoot) {
|
|
52
|
+
const trustRoot = join(userInfo().homedir, '.config', 'ruflo', 'policy-trust');
|
|
53
|
+
const projectId = createHash('sha256').update(realpathSync(projectRoot)).digest('hex');
|
|
54
|
+
const dir = join(trustRoot, projectId);
|
|
55
|
+
return { key: join(dir, 'anchor.key'), anchor: join(dir, 'state.anchor.json') };
|
|
56
|
+
}
|
|
57
|
+
function trustKey(projectRoot, create) {
|
|
58
|
+
const { key } = trustPaths(projectRoot);
|
|
59
|
+
if (!existsSync(key)) {
|
|
60
|
+
if (!create)
|
|
61
|
+
return undefined;
|
|
62
|
+
mkdirSync(dirname(key), { recursive: true, mode: 0o700 });
|
|
63
|
+
writeFileSync(key, randomBytes(32), { mode: 0o600, flag: 'wx' });
|
|
64
|
+
}
|
|
65
|
+
const material = readFileSync(key);
|
|
66
|
+
if (material.length !== 32)
|
|
67
|
+
throw new Error('invalid-policy-trust-key');
|
|
68
|
+
return material;
|
|
69
|
+
}
|
|
70
|
+
function stateAuthentication(state, key) {
|
|
71
|
+
return createHmac('sha256', key).update(JSON.stringify(state)).digest('hex');
|
|
72
|
+
}
|
|
73
|
+
function verifyStateAnchor(projectRoot, state) {
|
|
74
|
+
const { anchor } = trustPaths(projectRoot);
|
|
75
|
+
if (!existsSync(anchor))
|
|
76
|
+
return;
|
|
77
|
+
if (!state)
|
|
78
|
+
throw new Error('policy-state-missing-for-anchored-project');
|
|
79
|
+
const key = trustKey(projectRoot, false);
|
|
80
|
+
if (!key)
|
|
81
|
+
throw new Error('policy-trust-key-missing');
|
|
82
|
+
const record = JSON.parse(readFileSync(anchor, 'utf8'));
|
|
83
|
+
const expected = stateAuthentication(state, key);
|
|
84
|
+
const actual = record.authentication ?? '';
|
|
85
|
+
if (!/^[a-f0-9]{64}$/.test(actual)
|
|
86
|
+
|| !timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(actual, 'hex'))) {
|
|
87
|
+
throw new Error('policy-state-authentication-failed');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function writePolicyState(projectRoot, statePath, state) {
|
|
91
|
+
const anchorPath = trustPaths(projectRoot).anchor;
|
|
92
|
+
if (state.mode === 'enforce' || existsSync(anchorPath)) {
|
|
93
|
+
const key = trustKey(projectRoot, true);
|
|
94
|
+
const anchor = {
|
|
95
|
+
version: 1,
|
|
96
|
+
projectRoot: realpathSync(projectRoot),
|
|
97
|
+
mode: state.mode,
|
|
98
|
+
authentication: stateAuthentication(state, key),
|
|
99
|
+
updatedAt: Date.now(),
|
|
100
|
+
};
|
|
101
|
+
// On first enforcement, establish the external trust record first. A
|
|
102
|
+
// crash then leaves either a valid pair or an anchored mismatch that
|
|
103
|
+
// fails closed; it can never leave enforce state silently unanchored.
|
|
104
|
+
if (!existsSync(anchorPath)) {
|
|
105
|
+
writeJsonAtomic(anchorPath, anchor);
|
|
106
|
+
writeJsonAtomic(statePath, state);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
writeJsonAtomic(statePath, state);
|
|
110
|
+
writeJsonAtomic(anchorPath, anchor);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
writeJsonAtomic(statePath, state);
|
|
114
|
+
}
|
|
115
|
+
function detectLegacyCapabilities(projectRoot) {
|
|
116
|
+
const candidates = [
|
|
117
|
+
'.swarm/memory.db',
|
|
118
|
+
'.claude-flow/memory.db',
|
|
119
|
+
'.claude-flow/data/memory.db',
|
|
120
|
+
'agentdb.rvf',
|
|
121
|
+
'agentdb-memory.db',
|
|
122
|
+
];
|
|
123
|
+
const found = candidates.filter((candidate) => existsSync(join(projectRoot, candidate)));
|
|
124
|
+
const flags = [
|
|
125
|
+
process.env.CLAUDE_FLOW_STRICT_AUTH === 'true' ? 'strict-auth' : null,
|
|
126
|
+
process.env.CLAUDE_FLOW_STRICT_MEMORY === 'true' ? 'strict-memory' : null,
|
|
127
|
+
].filter(Boolean);
|
|
128
|
+
return `pre-ADR-324; capabilities=${[...found, ...flags].join(',') || 'none-detected'}`;
|
|
129
|
+
}
|
|
130
|
+
function configuredPolicyMode(projectRoot) {
|
|
131
|
+
let configured;
|
|
132
|
+
for (const relative of ['.agents/config.toml', '.codex/config.toml']) {
|
|
133
|
+
const file = join(resolve(projectRoot), relative);
|
|
134
|
+
if (!existsSync(file))
|
|
135
|
+
continue;
|
|
136
|
+
const content = readFileSync(file, 'utf8');
|
|
137
|
+
const section = content.match(/(?:^|\n)\[policy\]\s*\n([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[1];
|
|
138
|
+
const mode = section?.match(/(?:^|\n)\s*mode\s*=\s*"(legacy|observe|enforce)"/)?.[1];
|
|
139
|
+
if (mode)
|
|
140
|
+
configured = mode;
|
|
141
|
+
}
|
|
142
|
+
return configured;
|
|
143
|
+
}
|
|
144
|
+
export function loadPolicyState(projectRoot = process.cwd()) {
|
|
145
|
+
const target = paths(projectRoot);
|
|
146
|
+
if (!existsSync(target.state)) {
|
|
147
|
+
verifyStateAnchor(projectRoot, undefined);
|
|
148
|
+
return createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
|
|
149
|
+
}
|
|
150
|
+
const parsed = JSON.parse(readFileSync(target.state, 'utf8'));
|
|
151
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.rules) || !Array.isArray(parsed.receipts)) {
|
|
152
|
+
throw new Error(`unsupported-policy-state-version:${String(parsed.version)}`);
|
|
153
|
+
}
|
|
154
|
+
verifyStateAnchor(projectRoot, parsed);
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()) {
|
|
158
|
+
const target = paths(projectRoot);
|
|
159
|
+
if (existsSync(target.state)) {
|
|
160
|
+
const configured = configuredPolicyMode(projectRoot);
|
|
161
|
+
const current = loadPolicyState(projectRoot);
|
|
162
|
+
if (configured && current.configuredMode !== configured) {
|
|
163
|
+
await withPolicyTransaction(projectRoot, (engine) => engine.setConfiguredMode(configured));
|
|
164
|
+
}
|
|
165
|
+
return { migrated: false, statePath: target.state, mode: loadPolicyState(projectRoot).mode };
|
|
166
|
+
}
|
|
167
|
+
// Only upgrade existing Ruflo installations. A random directory should not
|
|
168
|
+
// acquire policy state merely because `ruflo --version` ran there.
|
|
169
|
+
if (!existsSync(join(resolve(projectRoot), '.claude-flow'))
|
|
170
|
+
&& !existsSync(join(resolve(projectRoot), '.swarm')))
|
|
171
|
+
return { migrated: false };
|
|
172
|
+
mkdirSync(target.dir, { recursive: true, mode: 0o700 });
|
|
173
|
+
const release = await acquireLock(target.lock);
|
|
174
|
+
try {
|
|
175
|
+
if (!existsSync(target.state)) {
|
|
176
|
+
const state = createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
|
|
177
|
+
const configured = configuredPolicyMode(projectRoot);
|
|
178
|
+
if (configured) {
|
|
179
|
+
state.mode = configured;
|
|
180
|
+
state.configuredMode = configured;
|
|
181
|
+
}
|
|
182
|
+
writePolicyState(projectRoot, target.state, state);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
release();
|
|
187
|
+
}
|
|
188
|
+
return { migrated: true, statePath: target.state, mode: loadPolicyState(projectRoot).mode };
|
|
189
|
+
}
|
|
190
|
+
export async function withPolicyTransaction(projectRoot, operation, options = {}) {
|
|
191
|
+
const target = paths(projectRoot);
|
|
192
|
+
mkdirSync(target.dir, { recursive: true, mode: 0o700 });
|
|
193
|
+
const release = await acquireLock(target.lock);
|
|
194
|
+
try {
|
|
195
|
+
const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), {
|
|
196
|
+
signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
|
|
197
|
+
keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
|
|
198
|
+
evidenceVerifier: verifyPolicyEvidence,
|
|
199
|
+
approvalIssuerVerifier: options.approvalIssuerVerifier,
|
|
200
|
+
});
|
|
201
|
+
const result = await operation(engine);
|
|
202
|
+
const nextState = engine.exportState();
|
|
203
|
+
if (!engine.verifyLedger().valid)
|
|
204
|
+
throw new Error('policy-ledger-verification-failed');
|
|
205
|
+
writePolicyState(projectRoot, target.state, nextState);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
release();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function verifyPolicyEvidence(evidence) {
|
|
213
|
+
if (!evidence.keyId || !evidence.contentHash || !evidence.signature)
|
|
214
|
+
return false;
|
|
215
|
+
let keys;
|
|
216
|
+
try {
|
|
217
|
+
keys = JSON.parse(process.env.CLAUDE_FLOW_POLICY_EVIDENCE_KEYS ?? '{}');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const key = keys[evidence.keyId];
|
|
223
|
+
if (!key || key.length < 16 || !/^sha256:[a-f0-9]{64}$/i.test(evidence.contentHash))
|
|
224
|
+
return false;
|
|
225
|
+
const signedClaims = JSON.stringify({
|
|
226
|
+
id: evidence.id,
|
|
227
|
+
provenance: evidence.provenance,
|
|
228
|
+
attestor: evidence.attestor,
|
|
229
|
+
observedAt: evidence.observedAt,
|
|
230
|
+
contentHash: evidence.contentHash,
|
|
231
|
+
keyId: evidence.keyId,
|
|
232
|
+
});
|
|
233
|
+
const expected = createHmac('sha256', key).update(signedClaims).digest('hex');
|
|
234
|
+
const provided = evidence.signature.replace(/^hmac-sha256:/, '');
|
|
235
|
+
if (!/^[a-f0-9]{64}$/i.test(provided))
|
|
236
|
+
return false;
|
|
237
|
+
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'));
|
|
238
|
+
}
|
|
239
|
+
export async function evaluatePolicyRequest(request, projectRoot = process.cwd()) {
|
|
240
|
+
return withPolicyTransaction(projectRoot, (engine) => engine.evaluate(request));
|
|
241
|
+
}
|
|
242
|
+
export async function setPolicyMode(mode, projectRoot = process.cwd()) {
|
|
243
|
+
return withPolicyTransaction(projectRoot, (engine) => engine.setMode(mode));
|
|
244
|
+
}
|
|
245
|
+
export async function upsertPolicyRule(rule, projectRoot = process.cwd()) {
|
|
246
|
+
return withPolicyTransaction(projectRoot, (engine) => engine.upsertRule(rule));
|
|
247
|
+
}
|
|
248
|
+
export async function setPolicyBudget(limit, projectRoot = process.cwd()) {
|
|
249
|
+
return withPolicyTransaction(projectRoot, (engine) => engine.setBudget(limit));
|
|
250
|
+
}
|
|
251
|
+
export async function issuePolicyApproval(approval, projectRoot = process.cwd(), approvalIssuerVerifier) {
|
|
252
|
+
return withPolicyTransaction(projectRoot, (engine) => engine.issueApproval(approval), { approvalIssuerVerifier });
|
|
253
|
+
}
|
|
254
|
+
export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
|
|
255
|
+
return withPolicyTransaction(projectRoot, (engine) => engine.revokeApproval(id));
|
|
256
|
+
}
|
|
257
|
+
export async function verifyPolicyLedger(projectRoot = process.cwd()) {
|
|
258
|
+
return withPolicyTransaction(projectRoot, (engine) => engine.verifyLedger());
|
|
259
|
+
}
|
|
260
|
+
export async function authorizeMcpTool(toolName, input, context = {}, attributes = {}) {
|
|
261
|
+
let projectRoot = typeof context.projectRoot === 'string' ? context.projectRoot : process.cwd();
|
|
262
|
+
let processEnvelope;
|
|
263
|
+
if (process.env.CLAUDE_FLOW_CAPABILITY_ENVELOPE) {
|
|
264
|
+
try {
|
|
265
|
+
const parsed = JSON.parse(process.env.CLAUDE_FLOW_CAPABILITY_ENVELOPE);
|
|
266
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
267
|
+
throw new Error('not an object');
|
|
268
|
+
}
|
|
269
|
+
processEnvelope = parsed;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
throw new Error('invalid-worker-capability-envelope');
|
|
273
|
+
}
|
|
274
|
+
// Linked git worktrees share one immutable common git directory. Derive
|
|
275
|
+
// the coordinator checkout from that directory so a worker cannot fall
|
|
276
|
+
// back to independent legacy policy state in its isolated worktree.
|
|
277
|
+
try {
|
|
278
|
+
const cwd = realpathSync(process.cwd());
|
|
279
|
+
const common = execFileSync('git', ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
280
|
+
projectRoot = dirname(realpathSync(common));
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
throw new Error('authoritative-worker-policy-root-unavailable');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return evaluatePolicyRequest({
|
|
287
|
+
identity: {
|
|
288
|
+
id: process.env.CLAUDE_FLOW_PRINCIPAL_ID ?? 'legacy-cli',
|
|
289
|
+
type: process.env.CLAUDE_FLOW_PRINCIPAL_ID ? 'agent' : 'legacy',
|
|
290
|
+
},
|
|
291
|
+
action: {
|
|
292
|
+
type: attributes.actionType ?? 'mcp.tool.call',
|
|
293
|
+
resource: toolName,
|
|
294
|
+
tool: toolName,
|
|
295
|
+
server: typeof context.serverId === 'string' ? context.serverId : 'ruflo',
|
|
296
|
+
namespace: typeof input.namespace === 'string' ? input.namespace : undefined,
|
|
297
|
+
environment: typeof context.environment === 'string' ? context.environment : undefined,
|
|
298
|
+
costUsd: attributes.costUsd,
|
|
299
|
+
tokens: attributes.tokens,
|
|
300
|
+
concurrency: attributes.concurrency,
|
|
301
|
+
network: attributes.network === true,
|
|
302
|
+
destructive: attributes.destructive === true,
|
|
303
|
+
},
|
|
304
|
+
context: {
|
|
305
|
+
envelope: attributes.envelope ?? processEnvelope,
|
|
306
|
+
approvalIds: Array.isArray(context.approvalIds) ? context.approvalIds.map(String) : undefined,
|
|
307
|
+
evidence: Array.isArray(context.evidence) ? context.evidence : undefined,
|
|
308
|
+
metadata: {
|
|
309
|
+
inputDigest: `sha256:${createHash('sha256').update(JSON.stringify(input)).digest('hex')}`,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
}, projectRoot);
|
|
313
|
+
}
|
|
314
|
+
/** Trusted classification derived from the registered tool name, never input. */
|
|
315
|
+
export function classifyMcpTool(toolName) {
|
|
316
|
+
const normalized = toolName.toLowerCase();
|
|
317
|
+
const policyAdmin = normalized.startsWith('policy_')
|
|
318
|
+
&& !['policy_evaluate', 'policy_status'].includes(normalized);
|
|
319
|
+
const memoryRead = /^(?:memory|agentdb)_(?:pattern-)?(?:search|query|get|retrieve|list|status|stats)/.test(normalized);
|
|
320
|
+
const memoryWrite = /^(?:memory|agentdb)_(?:pattern-)?(?:store|insert|update|delete|clear|purge|init)/.test(normalized);
|
|
321
|
+
const terminal = /^(?:terminal_execute|bash|shell|exec)/.test(normalized);
|
|
322
|
+
const destructive = policyAdmin
|
|
323
|
+
|| terminal
|
|
324
|
+
|| /(delete|remove|clear|purge|revoke|promote|deploy|integrate|cleanup|terminate|stop)/.test(normalized);
|
|
325
|
+
const network = terminal
|
|
326
|
+
|| /(github|browser|web_|http_|fetch|managed_agent|federation|ipfs|openrouter|provider)/.test(normalized);
|
|
327
|
+
return {
|
|
328
|
+
actionType: policyAdmin
|
|
329
|
+
? `policy.admin.${normalized.slice('policy_'.length)}`
|
|
330
|
+
: memoryRead
|
|
331
|
+
? 'memory.read'
|
|
332
|
+
: memoryWrite
|
|
333
|
+
? 'memory.write'
|
|
334
|
+
: 'mcp.tool.call',
|
|
335
|
+
network,
|
|
336
|
+
destructive,
|
|
337
|
+
namespaceAccess: memoryRead ? 'read' : memoryWrite ? 'write' : undefined,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=policy-runtime.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.32.
|
|
3
|
+
"version": "3.32.29",
|
|
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",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
],
|
|
87
87
|
"scripts": {
|
|
88
88
|
"build": "tsc",
|
|
89
|
+
"check:metaharness-pins": "node ../../../scripts/check-metaharness-pins.mjs",
|
|
89
90
|
"test": "vitest run",
|
|
90
91
|
"test:plugin-store": "npx tsx src/plugins/tests/standalone-test.ts",
|
|
91
92
|
"test:pattern-store": "npx tsx src/transfer/store/tests/standalone-test.ts",
|
|
@@ -104,23 +105,40 @@
|
|
|
104
105
|
"@claude-flow/mcp": "3.0.0-alpha.8",
|
|
105
106
|
"@claude-flow/neural": "3.0.0-alpha.9",
|
|
106
107
|
"@claude-flow/shared": "3.0.0-alpha.7",
|
|
107
|
-
"@metaharness/router": "^0.3.2",
|
|
108
108
|
"@noble/ed25519": "2.3.0",
|
|
109
109
|
"@ruvector/rabitq-wasm": "0.1.0",
|
|
110
|
-
"metaharness": "^0.4.1",
|
|
111
110
|
"semver": "7.7.3",
|
|
112
111
|
"sql.js": "^1.13.0",
|
|
113
112
|
"yaml": "^2.8.0"
|
|
114
113
|
},
|
|
115
114
|
"optionalDependencies": {
|
|
116
115
|
"@claude-flow/memory": "^3.0.0-alpha.21",
|
|
117
|
-
"@claude-flow/security": "^3.0.0-alpha.
|
|
118
|
-
"@metaharness/darwin": "^0.8.0",
|
|
116
|
+
"@claude-flow/security": "^3.0.0-alpha.14",
|
|
119
117
|
"agentdb": "^3.0.0-alpha.17",
|
|
120
118
|
"agentic-flow": "^3.0.0-alpha.1",
|
|
121
119
|
"better-sqlite3": "^12.9.0",
|
|
122
120
|
"ruvector": "^0.2.27"
|
|
123
121
|
},
|
|
122
|
+
"peerDependencies": {
|
|
123
|
+
"@metaharness/darwin": "^0.8.0",
|
|
124
|
+
"@metaharness/flywheel": "^0.1.7",
|
|
125
|
+
"@metaharness/router": "^0.3.2",
|
|
126
|
+
"metaharness": "^0.4.1"
|
|
127
|
+
},
|
|
128
|
+
"peerDependenciesMeta": {
|
|
129
|
+
"@metaharness/darwin": {
|
|
130
|
+
"optional": true
|
|
131
|
+
},
|
|
132
|
+
"@metaharness/flywheel": {
|
|
133
|
+
"optional": true
|
|
134
|
+
},
|
|
135
|
+
"@metaharness/router": {
|
|
136
|
+
"optional": true
|
|
137
|
+
},
|
|
138
|
+
"metaharness": {
|
|
139
|
+
"optional": true
|
|
140
|
+
}
|
|
141
|
+
},
|
|
124
142
|
"publishConfig": {
|
|
125
143
|
"access": "public",
|
|
126
144
|
"tag": "latest"
|