moflo 4.8.60 → 4.8.63
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/package.json +3 -2
- package/src/modules/cli/dist/src/version.js +1 -1
- package/src/modules/cli/package.json +1 -1
- package/src/modules/memory/dist/cache-manager.js +47 -6
- package/src/modules/spells/dist/core/bwrap-sandbox.js +8 -0
- package/src/modules/spells/dist/core/platform-sandbox.js +21 -1
- package/src/modules/spells/dist/factory/runner-bridge.js +17 -0
- package/src/modules/spells/dist/index.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.63",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
95
|
"js-yaml": "^4.1.1",
|
|
96
|
+
"lru-cache": "^11.3.5",
|
|
96
97
|
"semver": "^7.7.4",
|
|
97
98
|
"sql.js": "^1.14.1",
|
|
98
99
|
"valibot": "^1.3.1"
|
|
@@ -111,7 +112,7 @@
|
|
|
111
112
|
"@types/js-yaml": "^4.0.9",
|
|
112
113
|
"@types/node": "^20.19.37",
|
|
113
114
|
"eslint": "^8.0.0",
|
|
114
|
-
"moflo": "^4.8.
|
|
115
|
+
"moflo": "^4.8.61",
|
|
115
116
|
"tsx": "^4.21.0",
|
|
116
117
|
"typescript": "^5.9.3",
|
|
117
118
|
"vitest": "^4.0.0"
|
|
@@ -24,6 +24,9 @@ export class CacheManager extends EventEmitter {
|
|
|
24
24
|
head = null;
|
|
25
25
|
tail = null;
|
|
26
26
|
currentMemory = 0;
|
|
27
|
+
// Cache size estimates to avoid repeated JSON.stringify
|
|
28
|
+
sizeCache = new Map();
|
|
29
|
+
maxSizeCacheEntries = 10000;
|
|
27
30
|
// Statistics
|
|
28
31
|
stats = {
|
|
29
32
|
hits: 0,
|
|
@@ -75,17 +78,21 @@ export class CacheManager extends EventEmitter {
|
|
|
75
78
|
// Check if key already exists
|
|
76
79
|
const existingNode = this.cache.get(key);
|
|
77
80
|
if (existingNode) {
|
|
78
|
-
// Update existing entry
|
|
81
|
+
// Update existing entry - recalculate size if data changed
|
|
82
|
+
const oldSize = existingNode.estimatedSize;
|
|
83
|
+
const newSize = this.estimateSizeCached(key, data);
|
|
79
84
|
existingNode.value.data = data;
|
|
80
85
|
existingNode.value.cachedAt = now;
|
|
81
86
|
existingNode.value.expiresAt = now + entryTtl;
|
|
82
87
|
existingNode.value.lastAccessedAt = now;
|
|
88
|
+
existingNode.estimatedSize = newSize;
|
|
89
|
+
this.currentMemory = this.currentMemory - oldSize + newSize;
|
|
83
90
|
this.moveToFront(existingNode);
|
|
84
91
|
this.stats.writes++;
|
|
85
92
|
return;
|
|
86
93
|
}
|
|
87
|
-
// Calculate memory for new entry
|
|
88
|
-
const entryMemory = this.
|
|
94
|
+
// Calculate memory for new entry (with caching)
|
|
95
|
+
const entryMemory = this.estimateSizeCached(key, data);
|
|
89
96
|
// Evict entries if needed for memory pressure
|
|
90
97
|
if (this.config.maxMemory) {
|
|
91
98
|
while (this.currentMemory + entryMemory > this.config.maxMemory &&
|
|
@@ -110,6 +117,7 @@ export class CacheManager extends EventEmitter {
|
|
|
110
117
|
value: cachedEntry,
|
|
111
118
|
prev: null,
|
|
112
119
|
next: null,
|
|
120
|
+
estimatedSize: entryMemory, // Store computed size
|
|
113
121
|
};
|
|
114
122
|
// Add to cache
|
|
115
123
|
this.cache.set(key, node);
|
|
@@ -128,7 +136,8 @@ export class CacheManager extends EventEmitter {
|
|
|
128
136
|
}
|
|
129
137
|
this.removeNode(node);
|
|
130
138
|
this.cache.delete(key);
|
|
131
|
-
this.currentMemory -=
|
|
139
|
+
this.currentMemory -= node.estimatedSize; // Use cached size
|
|
140
|
+
this.sizeCache.delete(key); // Clean up size cache
|
|
132
141
|
this.emit('cache:delete', { key });
|
|
133
142
|
return true;
|
|
134
143
|
}
|
|
@@ -149,11 +158,13 @@ export class CacheManager extends EventEmitter {
|
|
|
149
158
|
* Clear all entries from the cache
|
|
150
159
|
*/
|
|
151
160
|
clear() {
|
|
161
|
+
const previousSize = this.cache.size;
|
|
152
162
|
this.cache.clear();
|
|
163
|
+
this.sizeCache.clear();
|
|
153
164
|
this.head = null;
|
|
154
165
|
this.tail = null;
|
|
155
166
|
this.currentMemory = 0;
|
|
156
|
-
this.emit('cache:cleared', { previousSize
|
|
167
|
+
this.emit('cache:cleared', { previousSize });
|
|
157
168
|
}
|
|
158
169
|
/**
|
|
159
170
|
* Get cache statistics
|
|
@@ -255,8 +266,37 @@ export class CacheManager extends EventEmitter {
|
|
|
255
266
|
isExpired(entry) {
|
|
256
267
|
return Date.now() > entry.expiresAt;
|
|
257
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* OPTIMIZED: Cache size estimates to avoid repeated JSON.stringify
|
|
271
|
+
* This is called on every cache set and eviction, so caching saves significant CPU
|
|
272
|
+
*/
|
|
273
|
+
estimateSizeCached(key, data) {
|
|
274
|
+
// Check if we have a cached size estimate
|
|
275
|
+
const cached = this.sizeCache.get(key);
|
|
276
|
+
if (cached !== undefined) {
|
|
277
|
+
return cached;
|
|
278
|
+
}
|
|
279
|
+
// Compute size
|
|
280
|
+
const size = this.estimateSize(data);
|
|
281
|
+
// Cache the result (with LRU eviction)
|
|
282
|
+
if (this.sizeCache.size >= this.maxSizeCacheEntries) {
|
|
283
|
+
const firstKey = this.sizeCache.keys().next().value;
|
|
284
|
+
if (firstKey !== undefined)
|
|
285
|
+
this.sizeCache.delete(firstKey);
|
|
286
|
+
}
|
|
287
|
+
this.sizeCache.set(key, size);
|
|
288
|
+
return size;
|
|
289
|
+
}
|
|
258
290
|
estimateSize(data) {
|
|
259
291
|
try {
|
|
292
|
+
// For primitive types, use fast path
|
|
293
|
+
if (typeof data === 'string') {
|
|
294
|
+
return data.length * 2;
|
|
295
|
+
}
|
|
296
|
+
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
297
|
+
return 8;
|
|
298
|
+
}
|
|
299
|
+
// For objects, use JSON.stringify (but only once due to caching)
|
|
260
300
|
return JSON.stringify(data).length * 2; // Rough UTF-16 estimate
|
|
261
301
|
}
|
|
262
302
|
catch {
|
|
@@ -298,9 +338,10 @@ export class CacheManager extends EventEmitter {
|
|
|
298
338
|
if (!this.tail)
|
|
299
339
|
return;
|
|
300
340
|
const evictedKey = this.tail.key;
|
|
301
|
-
const evictedSize = this.
|
|
341
|
+
const evictedSize = this.tail.estimatedSize; // Use cached size
|
|
302
342
|
this.removeNode(this.tail);
|
|
303
343
|
this.cache.delete(evictedKey);
|
|
344
|
+
this.sizeCache.delete(evictedKey); // Clean up size cache
|
|
304
345
|
this.currentMemory -= evictedSize;
|
|
305
346
|
this.stats.evictions++;
|
|
306
347
|
this.emit('cache:eviction', { key: evictedKey });
|
|
@@ -129,6 +129,14 @@ export function buildBwrapArgs(command, capabilities, projectRoot, options = {})
|
|
|
129
129
|
}
|
|
130
130
|
// ── PID isolation (always) ──────────────────────────────────────────
|
|
131
131
|
args.push('--unshare-pid');
|
|
132
|
+
// ── Lifetime bound to parent ────────────────────────────────────────
|
|
133
|
+
// Without this, child processes spawned by the sandboxed command (e.g.
|
|
134
|
+
// node workers from `claude -p`) keep the PID namespace alive past the
|
|
135
|
+
// entry script's exit — bwrap waits for the namespace to drain even
|
|
136
|
+
// though the user-visible work is done. --die-with-parent makes bwrap
|
|
137
|
+
// and everything inside terminate when the spawning runner exits or
|
|
138
|
+
// sends a signal, guaranteeing cleanup on success and on timeout.
|
|
139
|
+
args.push('--die-with-parent');
|
|
132
140
|
// ── Command ─────────────────────────────────────────────────────────
|
|
133
141
|
args.push('bash', '-c', command);
|
|
134
142
|
return args;
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* @see https://github.com/eric-cielo/moflo/issues/409
|
|
13
13
|
*/
|
|
14
14
|
import { execSync } from 'node:child_process';
|
|
15
|
-
import { existsSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { platform } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
17
18
|
export const DEFAULT_SANDBOX_CONFIG = {
|
|
18
19
|
enabled: false,
|
|
19
20
|
tier: 'auto',
|
|
@@ -129,6 +130,25 @@ export function resolveSandboxConfig(raw) {
|
|
|
129
130
|
function isValidTier(value) {
|
|
130
131
|
return value === 'auto' || value === 'denylist-only' || value === 'full';
|
|
131
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Load sandbox config from a project's moflo.yaml.
|
|
135
|
+
* Returns DEFAULT_SANDBOX_CONFIG on any failure (missing file, parse error, etc.).
|
|
136
|
+
*
|
|
137
|
+
* Lets the spell engine auto-discover sandbox settings from the project root
|
|
138
|
+
* without forcing every caller (MCP tools, CLI adapters) to load moflo.yaml.
|
|
139
|
+
*/
|
|
140
|
+
export async function loadSandboxConfigFromProject(projectRoot) {
|
|
141
|
+
try {
|
|
142
|
+
const content = readFileSync(join(projectRoot, 'moflo.yaml'), 'utf-8');
|
|
143
|
+
const mod = await import('js-yaml');
|
|
144
|
+
const yaml = mod.default ?? mod;
|
|
145
|
+
const raw = yaml.load(content);
|
|
146
|
+
return resolveSandboxConfig(raw?.sandbox);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return DEFAULT_SANDBOX_CONFIG;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
132
152
|
/**
|
|
133
153
|
* Combine detected capability with user config to determine effective sandbox behavior.
|
|
134
154
|
*
|
|
@@ -8,7 +8,20 @@
|
|
|
8
8
|
* This module is the integration point between MCP spell tools
|
|
9
9
|
* and the SpellCaster engine.
|
|
10
10
|
*/
|
|
11
|
+
import { loadSandboxConfigFromProject } from '../core/platform-sandbox.js';
|
|
11
12
|
import { createRunner, runSpellFromContent } from './runner-factory.js';
|
|
13
|
+
/**
|
|
14
|
+
* Resolve sandbox config: prefer caller-supplied; fall back to auto-loading
|
|
15
|
+
* from moflo.yaml at projectRoot. Returns undefined when neither is available
|
|
16
|
+
* (runner falls back to DEFAULT_SANDBOX_CONFIG with denylist-only).
|
|
17
|
+
*/
|
|
18
|
+
async function resolveSandbox(explicit, projectRoot) {
|
|
19
|
+
if (explicit)
|
|
20
|
+
return explicit;
|
|
21
|
+
if (!projectRoot)
|
|
22
|
+
return undefined;
|
|
23
|
+
return loadSandboxConfigFromProject(projectRoot);
|
|
24
|
+
}
|
|
12
25
|
// Track active spells for cancellation
|
|
13
26
|
const activeSpells = new Map();
|
|
14
27
|
// ============================================================================
|
|
@@ -22,6 +35,7 @@ export async function bridgeRunSpell(content, sourceFile, args, options = {}) {
|
|
|
22
35
|
const controller = new AbortController();
|
|
23
36
|
activeSpells.set(spellId, controller);
|
|
24
37
|
try {
|
|
38
|
+
const sandboxConfig = await resolveSandbox(options.sandboxConfig, options.projectRoot);
|
|
25
39
|
const result = await runSpellFromContent(content, sourceFile, {
|
|
26
40
|
spellId,
|
|
27
41
|
args,
|
|
@@ -30,6 +44,7 @@ export async function bridgeRunSpell(content, sourceFile, args, options = {}) {
|
|
|
30
44
|
memory: options.memory,
|
|
31
45
|
credentials: options.credentials,
|
|
32
46
|
...(options.projectRoot ? { projectRoot: options.projectRoot } : {}),
|
|
47
|
+
...(sandboxConfig ? { sandboxConfig } : {}),
|
|
33
48
|
});
|
|
34
49
|
return result;
|
|
35
50
|
}
|
|
@@ -45,11 +60,13 @@ export async function bridgeExecuteSpell(definition, args, options = {}) {
|
|
|
45
60
|
const controller = new AbortController();
|
|
46
61
|
activeSpells.set(spellId, controller);
|
|
47
62
|
try {
|
|
63
|
+
const sandboxConfig = await resolveSandbox(options.sandboxConfig, options.projectRoot);
|
|
48
64
|
const runner = createRunner({ memory: options.memory, credentials: options.credentials });
|
|
49
65
|
return await runner.run(definition, args, {
|
|
50
66
|
spellId,
|
|
51
67
|
signal: controller.signal,
|
|
52
68
|
...(options.projectRoot ? { projectRoot: options.projectRoot } : {}),
|
|
69
|
+
...(sandboxConfig ? { sandboxConfig } : {}),
|
|
53
70
|
});
|
|
54
71
|
}
|
|
55
72
|
finally {
|
|
@@ -19,7 +19,7 @@ export { GatedConnectorAccessor } from './core/gated-connector-accessor.js';
|
|
|
19
19
|
export { checkCapabilities, } from './core/capability-validator.js';
|
|
20
20
|
export { CapabilityGateway, CapabilityDeniedError, DenyAllGateway, DENY_ALL_GATEWAY, discloseStep, discloseSpell, formatStepDisclosure, formatSpellDisclosure, } from './core/capability-gateway.js';
|
|
21
21
|
export { collectPrerequisites, checkPrerequisites, formatPrerequisiteErrors, commandExists, } from './core/prerequisite-checker.js';
|
|
22
|
-
export { detectSandboxCapability, resetSandboxCache, resolveSandboxConfig, resolveEffectiveSandbox, formatSandboxLog, DEFAULT_SANDBOX_CONFIG, } from './core/platform-sandbox.js';
|
|
22
|
+
export { detectSandboxCapability, resetSandboxCache, resolveSandboxConfig, resolveEffectiveSandbox, formatSandboxLog, loadSandboxConfigFromProject, DEFAULT_SANDBOX_CONFIG, } from './core/platform-sandbox.js';
|
|
23
23
|
export { resolveScopePath, } from './core/sandbox-utils.js';
|
|
24
24
|
export { generateSandboxProfile, wrapWithSandboxExec, } from './core/sandbox-profile.js';
|
|
25
25
|
export { buildBwrapArgs, wrapWithBwrap, } from './core/bwrap-sandbox.js';
|