moflo 4.8.64 → 4.8.66

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.64",
3
+ "version": "4.8.66",
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.63",
115
+ "moflo": "^4.8.65",
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:
@@ -60,7 +60,8 @@ async function executeAndTrack(engine, definition, args) {
60
60
  const spellId = `sp-${Date.now()}`;
61
61
  const tracked = trackStart(spellId, definition.name, definition.description);
62
62
  try {
63
- const result = await engine.bridgeExecuteSpell(definition, args, { spellId });
63
+ const sandboxConfig = await engine.loadSandboxConfigFromProject(findProjectRoot());
64
+ const result = await engine.bridgeExecuteSpell(definition, args, { spellId, sandboxConfig });
64
65
  trackResult(tracked, result);
65
66
  return serializeResult(result);
66
67
  }
@@ -202,7 +203,8 @@ export const spellTools = [
202
203
  }
203
204
  // Run from raw content via bridge
204
205
  const engine = await loadSpellEngine();
205
- const result = await engine.bridgeRunSpell(content, sourceFile, args, { dryRun });
206
+ const sandboxConfig = await engine.loadSandboxConfigFromProject(findProjectRoot());
207
+ const result = await engine.bridgeRunSpell(content, sourceFile, args, { dryRun, sandboxConfig });
206
208
  const tracked = trackStart(result.spellId, spellName);
207
209
  trackResult(tracked, result);
208
210
  return serializeResult(result);
@@ -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.64';
5
+ export const VERSION = '4.8.66';
6
6
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moflo/cli",
3
- "version": "4.8.64",
3
+ "version": "4.8.66",
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 {