moflo 4.8.63 → 4.8.65

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.
@@ -584,8 +584,16 @@ status_line:
584
584
  show_adrs: true # ADR compliance (dashboard only)
585
585
  show_agentdb: true # AgentDB vectors/size (dashboard only)
586
586
  show_tests: true # Test file count (dashboard only)
587
+
588
+ # Spell step sandboxing (OS-level process isolation for bash steps)
589
+ # Platform support: macOS (sandbox-exec), Linux/WSL (bwrap). Windows has no OS sandbox.
590
+ sandbox:
591
+ enabled: false # Set to true to wrap bash steps in an OS sandbox
592
+ tier: auto # auto | denylist-only | full
587
593
  ```
588
594
 
595
+ If your `moflo.yaml` predates the `sandbox:` block, it is auto-appended on the next session start — you never need to re-run `moflo init` after a version bump.
596
+
589
597
  ### Key Behaviors
590
598
 
591
599
  | Config | Effect |
@@ -606,6 +614,9 @@ status_line:
606
614
  | `model_routing.enabled: true` | Auto-select haiku/sonnet/opus based on task complexity |
607
615
  | `status_line.mode: dashboard` | Switch to multi-line status display |
608
616
  | `status_line.show_swarm: false` | Hide swarm agent count from status bar |
617
+ | `sandbox.enabled: true` | Wrap bash steps in an OS sandbox (macOS/Linux/WSL) — absolute disable when `false`, regardless of tier |
618
+ | `sandbox.tier: full` | Require OS sandbox; throw at runtime if the platform tool is unavailable |
619
+ | `sandbox.tier: denylist-only` | Keep Layer 1 denylist only; skip OS isolation even when enabled |
609
620
 
610
621
  ---
611
622
 
@@ -210,6 +210,29 @@ The engine **automatically rewrites** `claude -p` commands in bash steps — str
210
210
  | **[SENSITIVE]** | `agent`, `net`, `browser` | Can read external data or spawn processes |
211
211
  | **[DESTRUCTIVE]** | `shell`, `fs:write`, `browser:evaluate`, `credentials` | Can permanently modify/delete data |
212
212
 
213
+ ## OS-Level Sandbox Configuration (`moflo.yaml`)
214
+
215
+ Capabilities and the gateway always apply. An **additional** OS-level process sandbox (Layer 3) wraps bash steps on macOS (`sandbox-exec`) and Linux/WSL (`bwrap`). It is controlled by the `sandbox:` block in `moflo.yaml`:
216
+
217
+ ```yaml
218
+ sandbox:
219
+ enabled: false # Master toggle — false = OS sandbox off (denylist + gateway still apply)
220
+ tier: auto # auto | denylist-only | full
221
+ ```
222
+
223
+ Semantics (from `resolveEffectiveSandbox()` in `src/modules/spells/src/core/platform-sandbox.ts`):
224
+
225
+ | `enabled` | `tier` | Tool available | OS sandbox runs? | Notes |
226
+ |-----------|--------|----------------|------------------|-------|
227
+ | `false` | (any) | (any) | No | **Absolute disable** — master toggle wins |
228
+ | `true` | `auto` | Yes | Yes | Use detected tool (bwrap/sandbox-exec) |
229
+ | `true` | `auto` | No | No | Graceful fallback; logs "not available" |
230
+ | `true` | `denylist-only` | (any) | No | Layer 1 only, skip OS isolation |
231
+ | `true` | `full` | Yes | Yes | Require OS sandbox |
232
+ | `true` | `full` | No | — | **Throws** at spell start |
233
+
234
+ Existing projects that predate this block get it auto-appended on session start — never require `moflo init` to re-run after a version bump.
235
+
213
236
  ## See Also
214
237
 
215
238
  - `.claude/guidance/shipped/moflo-spell-engine.md` — Spell engine usage and YAML format
@@ -0,0 +1,80 @@
1
+ /**
2
+ * moflo.yaml section upgrader.
3
+ *
4
+ * Users must never be required to re-run `moflo init` after upgrading moflo.
5
+ * When we ship a new top-level config section (e.g. `sandbox:`), this module
6
+ * idempotently appends the missing block — with sensible defaults and inline
7
+ * comments — to the user's existing moflo.yaml, without touching any values
8
+ * they've already set.
9
+ *
10
+ * See: .claude/guidance/internal/upgrade-contract.md
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
14
+
15
+ /**
16
+ * Registry of top-level config sections that moflo ships with default blocks.
17
+ *
18
+ * Each entry: { key, block } where `block` is the raw YAML snippet (including
19
+ * its leading comment) to append when the top-level `<key>:` is absent.
20
+ *
21
+ * When adding a new top-level section to the init template in moflo-init.ts,
22
+ * also add the same block here so existing users get it on their next session.
23
+ */
24
+ export const REQUIRED_SECTIONS = [
25
+ {
26
+ key: 'sandbox',
27
+ block: `# Spell step sandboxing (OS-level process isolation for bash steps)
28
+ # Platform support: macOS (sandbox-exec), Linux/WSL (bwrap). Windows has no OS sandbox.
29
+ # Tiers:
30
+ # auto — Use best available sandbox for this platform (recommended when enabled)
31
+ # denylist-only — Layer 1 only: block catastrophic commands, no OS isolation
32
+ # full — Require full OS isolation; throws if the sandbox tool is unavailable
33
+ sandbox:
34
+ enabled: false # Set to true to wrap bash steps in an OS sandbox
35
+ tier: auto # auto | denylist-only | full
36
+ `,
37
+ },
38
+ ];
39
+
40
+ /**
41
+ * Return true if the YAML text already defines the given top-level key.
42
+ * Matches `^<key>:` at column 0 on any line, which is how YAML roots look.
43
+ */
44
+ export function hasTopLevelSection(yamlText, key) {
45
+ const pattern = new RegExp(`^${key}\\s*:`, 'm');
46
+ return pattern.test(yamlText);
47
+ }
48
+
49
+ /**
50
+ * Compute what ensureYamlSections() would append, without writing anything.
51
+ * Returns the list of section keys that are missing from the given yaml text.
52
+ */
53
+ export function missingSections(yamlText, registry = REQUIRED_SECTIONS) {
54
+ return registry
55
+ .filter((entry) => !hasTopLevelSection(yamlText, entry.key))
56
+ .map((entry) => entry.key);
57
+ }
58
+
59
+ /**
60
+ * Append any missing registered sections to the yaml file at `yamlPath`.
61
+ *
62
+ * - Idempotent: sections already present are left alone.
63
+ * - Non-destructive: user values are never read, parsed, or rewritten.
64
+ * - Returns the list of section keys that were appended (empty if no change).
65
+ */
66
+ export function ensureYamlSections(yamlPath, registry = REQUIRED_SECTIONS) {
67
+ if (!existsSync(yamlPath)) return [];
68
+
69
+ const original = readFileSync(yamlPath, 'utf-8');
70
+ const toAppend = registry.filter((entry) => !hasTopLevelSection(original, entry.key));
71
+ if (toAppend.length === 0) return [];
72
+
73
+ const needsTrailingNewline = !original.endsWith('\n');
74
+ const separator = needsTrailingNewline ? '\n\n' : original.endsWith('\n\n') ? '' : '\n';
75
+ const appended = toAppend.map((entry) => entry.block.trimEnd()).join('\n\n');
76
+ const next = `${original}${separator}${appended}\n`;
77
+
78
+ writeFileSync(yamlPath, next, 'utf-8');
79
+ return toAppend.map((entry) => entry.key);
80
+ }
@@ -355,6 +355,25 @@ try {
355
355
  }
356
356
  } catch { /* non-fatal */ }
357
357
 
358
+ // ── 3d-yaml. Append missing top-level sections to moflo.yaml ───────────────
359
+ // Users must never be required to re-run `moflo init` after a version bump.
360
+ // When moflo ships a new top-level config section (e.g. sandbox:), append it
361
+ // with defaults + comments if the user's yaml doesn't already have it.
362
+ // Fully idempotent and never touches user-set values.
363
+ // See: .claude/guidance/internal/upgrade-contract.md
364
+ try {
365
+ const upgraderPaths = [
366
+ resolve(projectRoot, 'node_modules/moflo/bin/lib/yaml-upgrader.mjs'),
367
+ resolve(projectRoot, 'bin/lib/yaml-upgrader.mjs'),
368
+ ];
369
+ const upgraderPath = upgraderPaths.find((p) => existsSync(p));
370
+ const mofloYaml = resolve(projectRoot, 'moflo.yaml');
371
+ if (upgraderPath && existsSync(mofloYaml)) {
372
+ const { ensureYamlSections } = await import(`file://${upgraderPath.replace(/\\/g, '/')}`);
373
+ ensureYamlSections(mofloYaml);
374
+ }
375
+ } catch { /* non-fatal — yaml stays as-is, user can still edit manually */ }
376
+
358
377
  // ── 3d. Ensure global `flo` shim exists ─────────────────────────────────────
359
378
  // Installs a tiny shim into npm's global bin so bare `flo` resolves to the
360
379
  // local project's node_modules/.bin/flo. Idempotent — skips if already present.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.8.63",
3
+ "version": "4.8.65",
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",
@@ -112,7 +112,7 @@
112
112
  "@types/js-yaml": "^4.0.9",
113
113
  "@types/node": "^20.19.37",
114
114
  "eslint": "^8.0.0",
115
- "moflo": "^4.8.61",
115
+ "moflo": "^4.8.64",
116
116
  "tsx": "^4.21.0",
117
117
  "typescript": "^5.9.3",
118
118
  "vitest": "^4.0.0"
@@ -340,6 +340,16 @@ mcp:
340
340
  tool_defer: deferred # Defer 150+ tool schemas; loaded on demand via ToolSearch
341
341
  auto_start: false # Auto-start MCP server on session begin
342
342
 
343
+ # Spell step sandboxing (OS-level process isolation for bash steps)
344
+ # Platform support: macOS (sandbox-exec), Linux/WSL (bwrap). Windows has no OS sandbox.
345
+ # Tiers:
346
+ # auto — Use best available sandbox for this platform (recommended when enabled)
347
+ # denylist-only — Layer 1 only: block catastrophic commands, no OS isolation
348
+ # full — Require full OS isolation; throws if the sandbox tool is unavailable
349
+ sandbox:
350
+ enabled: false # Set to true to wrap bash steps in an OS sandbox
351
+ tier: auto # auto | denylist-only | full
352
+
343
353
  # Status line display (shown at bottom of Claude Code)
344
354
  # mode: "compact" (default), "single-line", or "dashboard" (full multi-line)
345
355
  status_line:
@@ -0,0 +1,79 @@
1
+ /**
2
+ * AgentDB-backed VectorStore adapter for AIDefence.
3
+ *
4
+ * Wraps the CLI memory-bridge (AgentDB v3 + HNSW + embeddings) so the
5
+ * 6 aidefence_* MCP tools persist learned threat patterns and mitigation
6
+ * strategies across process restarts with 150x-12,500x faster search.
7
+ *
8
+ * @moflo/aidefence stays pure-lib: this adapter lives in the CLI package
9
+ * where the bridge is already available. Arbitrary values are JSON-serialised
10
+ * into the bridge's `content` field; namespaces are prefixed with
11
+ * `aidefence:` to isolate from general memory entries.
12
+ */
13
+ import { bridgeStoreEntry, bridgeSearchEntries, bridgeGetEntry, bridgeDeleteEntry, isBridgeAvailable, } from '../memory/memory-bridge.js';
14
+ // Structural duck-typed shape of @moflo/aidefence's VectorStore interface.
15
+ // Declared locally to avoid cross-package type resolution fragility; TypeScript
16
+ // verifies compatibility structurally when passed to createAIDefence().
17
+ const NS_PREFIX = 'aidefence:';
18
+ function prefixNs(namespace) {
19
+ return `${NS_PREFIX}${namespace}`;
20
+ }
21
+ function safeParse(raw) {
22
+ try {
23
+ return JSON.parse(raw);
24
+ }
25
+ catch {
26
+ return raw;
27
+ }
28
+ }
29
+ export class AgentDBAIDefenceStore {
30
+ async store(params) {
31
+ await bridgeStoreEntry({
32
+ namespace: prefixNs(params.namespace),
33
+ key: params.key,
34
+ value: JSON.stringify(params.value),
35
+ ttl: params.ttl,
36
+ upsert: true,
37
+ });
38
+ }
39
+ async search(params) {
40
+ const queryStr = typeof params.query === 'string' ? params.query : JSON.stringify(params.query);
41
+ const result = await bridgeSearchEntries({
42
+ namespace: prefixNs(params.namespace),
43
+ query: queryStr,
44
+ limit: params.k ?? 10,
45
+ threshold: params.minSimilarity ?? 0,
46
+ });
47
+ if (!result?.results)
48
+ return [];
49
+ return result.results.map(r => ({
50
+ key: r.key,
51
+ value: safeParse(r.content),
52
+ similarity: r.score,
53
+ }));
54
+ }
55
+ async get(namespace, key) {
56
+ const result = await bridgeGetEntry({
57
+ namespace: prefixNs(namespace),
58
+ key,
59
+ });
60
+ if (!result?.found || !result.entry)
61
+ return null;
62
+ return safeParse(result.entry.content);
63
+ }
64
+ async delete(namespace, key) {
65
+ await bridgeDeleteEntry({
66
+ namespace: prefixNs(namespace),
67
+ key,
68
+ });
69
+ }
70
+ }
71
+ /**
72
+ * Return an AgentDB-backed store if the memory bridge is available,
73
+ * otherwise null so the caller can fall back to the default in-memory store.
74
+ */
75
+ export async function tryCreateAgentDBStore() {
76
+ const available = await isBridgeAvailable();
77
+ return available ? new AgentDBAIDefenceStore() : null;
78
+ }
79
+ //# sourceMappingURL=aidefence-agentdb-store.js.map
@@ -10,6 +10,7 @@
10
10
  * Created with ❤️ by motailz.com
11
11
  */
12
12
  import { autoInstallPackage } from './auto-install.js';
13
+ import { tryCreateAgentDBStore } from './aidefence-agentdb-store.js';
13
14
  import { createRequire } from 'module';
14
15
  // Create require for resolving module paths
15
16
  const require = createRequire(import.meta.url);
@@ -17,6 +18,20 @@ const require = createRequire(import.meta.url);
17
18
  let aidefenceInstance = null;
18
19
  // Track if we've attempted install this session
19
20
  let installAttempted = false;
21
+ /**
22
+ * Build createAIDefence config, attaching an AgentDB-backed vector store
23
+ * when the memory bridge is available. Falls back to the package default
24
+ * (InMemoryVectorStore) otherwise so the MCP tools still function standalone.
25
+ */
26
+ async function buildAIDefenceConfig() {
27
+ const store = await tryCreateAgentDBStore();
28
+ if (store) {
29
+ console.error('[claude-flow] aidefence: using AgentDB-backed vector store (HNSW)');
30
+ return { enableLearning: true, vectorStore: store };
31
+ }
32
+ console.error('[claude-flow] aidefence: AgentDB bridge unavailable, using in-memory store');
33
+ return { enableLearning: true };
34
+ }
20
35
  /**
21
36
  * Get or create AIDefence instance (throws if unavailable)
22
37
  */
@@ -28,7 +43,8 @@ async function getAIDefence() {
28
43
  // First attempt - try to load via dynamic import (ESM)
29
44
  try {
30
45
  const aidefence = await import(packageName);
31
- const instance = aidefence.createAIDefence({ enableLearning: true });
46
+ const config = await buildAIDefenceConfig();
47
+ const instance = aidefence.createAIDefence(config);
32
48
  if (!instance) {
33
49
  throw new Error('createAIDefence returned null');
34
50
  }
@@ -60,7 +76,8 @@ async function getAIDefence() {
60
76
  const modulePath = require.resolve(packageName);
61
77
  const cacheBust = `?t=${Date.now()}`;
62
78
  const aidefence = await import(pathToFileURL(modulePath).href + cacheBust);
63
- const instance = aidefence.createAIDefence({ enableLearning: true });
79
+ const config = await buildAIDefenceConfig();
80
+ const instance = aidefence.createAIDefence(config);
64
81
  if (!instance) {
65
82
  throw new Error('createAIDefence returned null after install');
66
83
  }
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.8.63';
5
+ export const VERSION = '4.8.65';
6
6
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moflo/cli",
3
- "version": "4.8.63",
3
+ "version": "4.8.65",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -152,11 +152,12 @@ export async function loadSandboxConfigFromProject(projectRoot) {
152
152
  /**
153
153
  * Combine detected capability with user config to determine effective sandbox behavior.
154
154
  *
155
+ * @param config — resolved user config (enabled + tier)
156
+ * @param capability — optional capability override (for tests; skips OS detection)
155
157
  * @returns EffectiveSandbox — includes display status for spell-start logging.
156
158
  * @throws Error if tier is 'full' but no OS sandbox is available.
157
159
  */
158
- export function resolveEffectiveSandbox(config) {
159
- const capability = detectSandboxCapability();
160
+ export function resolveEffectiveSandbox(config, capability = detectSandboxCapability()) {
160
161
  // Config disabled or tier is denylist-only => no OS sandbox
161
162
  if (!config.enabled || config.tier === 'denylist-only') {
162
163
  return {