moflo 4.8.57 → 4.8.59

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.8.57",
3
+ "version": "4.8.59",
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",
@@ -111,7 +111,7 @@
111
111
  "@types/js-yaml": "^4.0.9",
112
112
  "@types/node": "^20.19.37",
113
113
  "eslint": "^8.0.0",
114
- "moflo": "^4.8.56",
114
+ "moflo": "^4.8.58",
115
115
  "tsx": "^4.21.0",
116
116
  "typescript": "^5.9.3",
117
117
  "vitest": "^4.0.0"
@@ -78,7 +78,7 @@ const DEFAULT_CONFIG = {
78
78
  helpers: true,
79
79
  },
80
80
  sandbox: {
81
- enabled: true,
81
+ enabled: false,
82
82
  tier: 'auto',
83
83
  },
84
84
  epic: {
@@ -359,7 +359,7 @@ auto_update:
359
359
  # OS-level sandbox for spell bash steps
360
360
  # Denylist always runs regardless of this setting
361
361
  sandbox:
362
- enabled: true # false to disable OS sandbox (keeps denylist)
362
+ enabled: false # true to enable OS sandbox (denylist runs either way)
363
363
  tier: auto # auto | denylist-only | full
364
364
  # auto = best available, graceful fallback
365
365
  # denylist-only = skip OS sandbox
@@ -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.57';
5
+ export const VERSION = '4.8.59';
6
6
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moflo/cli",
3
- "version": "4.8.57",
3
+ "version": "4.8.59",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -146,10 +146,14 @@ export const bashCommand = {
146
146
  const projectRoot = context.variables.projectRoot || process.cwd();
147
147
  const caps = context.effectiveCaps ?? [];
148
148
  if (tool === 'sandbox-exec') {
149
- sandboxWrap = wrapWithSandboxExec(command, caps, projectRoot);
149
+ sandboxWrap = wrapWithSandboxExec(command, caps, projectRoot, {
150
+ permissionLevel: context.permissionLevel,
151
+ });
150
152
  }
151
153
  else if (tool === 'bwrap') {
152
- sandboxWrap = wrapWithBwrap(command, caps, projectRoot);
154
+ sandboxWrap = wrapWithBwrap(command, caps, projectRoot, {
155
+ permissionLevel: context.permissionLevel,
156
+ });
153
157
  }
154
158
  }
155
159
  catch (err) {
@@ -9,7 +9,44 @@
9
9
  *
10
10
  * @see https://github.com/eric-cielo/moflo/issues/411
11
11
  */
12
+ import { homedir } from 'node:os';
13
+ import { posix } from 'node:path';
12
14
  import { resolveScopePath } from './sandbox-utils.js';
15
+ /**
16
+ * Home-directory paths that common CLI tools need writable to persist state.
17
+ * These are bound via `--bind-try` so missing entries are silently skipped.
18
+ *
19
+ * Rationale: `elevated`/`autonomous` steps routinely spawn tools like `claude`,
20
+ * `gh`, `git`, and `npm` that write config/credentials/cache under $HOME. A
21
+ * pure `--ro-bind / /` makes those tools fail with EROFS. We narrow the bind
22
+ * set to well-known config paths so the sandbox still protects system dirs
23
+ * (/etc, /usr, /var) and the rest of $HOME.
24
+ */
25
+ const TOOL_HOME_PATHS = [
26
+ // Claude Code
27
+ '.claude',
28
+ '.claude.json',
29
+ // GitHub CLI
30
+ '.config/gh',
31
+ // git
32
+ '.gitconfig',
33
+ '.git-credentials',
34
+ // npm
35
+ '.npmrc',
36
+ '.npm',
37
+ // Shared XDG locations
38
+ '.config',
39
+ '.cache',
40
+ '.local/share',
41
+ '.local/state',
42
+ ];
43
+ /**
44
+ * Permission levels that spawn arbitrary CLI tools and therefore need tool
45
+ * home paths bound writable.
46
+ */
47
+ function needsToolHomeAccess(level) {
48
+ return level === 'elevated' || level === 'autonomous';
49
+ }
13
50
  /**
14
51
  * Build bwrap CLI arguments from step capabilities.
15
52
  *
@@ -20,8 +57,12 @@ import { resolveScopePath } from './sandbox-utils.js';
20
57
  * - fs:write scoped -> --bind (read-write) for each scope path
21
58
  * - fs:write unscoped -> --bind (read-write) for projectRoot
22
59
  * - net -> omit --unshare-net
60
+ *
61
+ * When `options.permissionLevel` is `elevated` or `autonomous`, also bind a
62
+ * narrow allowlist of CLI-tool home paths writable via `--bind-try` so that
63
+ * spawned subcommands (claude, gh, git, npm) can persist their state.
23
64
  */
24
- export function buildBwrapArgs(command, capabilities, projectRoot) {
65
+ export function buildBwrapArgs(command, capabilities, projectRoot, options = {}) {
25
66
  const args = [];
26
67
  // ── Root filesystem (read-only by default) ──────────────────────────
27
68
  args.push('--ro-bind', '/', '/');
@@ -32,16 +73,35 @@ export function buildBwrapArgs(command, capabilities, projectRoot) {
32
73
  args.push('--tmpfs', '/tmp');
33
74
  // ── fs:write — grant read-write bind mounts ─────────────────────────
34
75
  const fsWrite = capabilities.find(c => c.type === 'fs:write');
76
+ const writableScopes = new Set();
35
77
  if (fsWrite) {
36
78
  if (fsWrite.scope && fsWrite.scope.length > 0) {
37
79
  for (const scopePath of fsWrite.scope) {
38
80
  const resolved = resolveScopePath(scopePath, projectRoot);
39
81
  args.push('--bind', resolved, resolved);
82
+ writableScopes.add(resolved);
40
83
  }
41
84
  }
42
85
  else {
43
86
  // Unscoped fs:write -> writable project root
44
87
  args.push('--bind', projectRoot, projectRoot);
88
+ writableScopes.add(projectRoot);
89
+ }
90
+ }
91
+ // ── Tool home paths (elevated/autonomous only) ──────────────────────
92
+ // Bind well-known CLI-tool config/cache paths writable so spawned
93
+ // subcommands (claude, gh, git, npm) can persist their state. Uses
94
+ // --bind-try so missing paths are ignored instead of erroring.
95
+ if (needsToolHomeAccess(options.permissionLevel)) {
96
+ const home = options.homeDir ?? homedir();
97
+ if (home) {
98
+ for (const rel of TOOL_HOME_PATHS) {
99
+ const resolved = posix.join(home, rel);
100
+ if (writableScopes.has(resolved))
101
+ continue;
102
+ args.push('--bind-try', resolved, resolved);
103
+ writableScopes.add(resolved);
104
+ }
45
105
  }
46
106
  }
47
107
  // ── fs:read scoped — explicit read-only bind mounts ─────────────────
@@ -50,9 +110,8 @@ export function buildBwrapArgs(command, capabilities, projectRoot) {
50
110
  if (fsRead && fsRead.scope && fsRead.scope.length > 0) {
51
111
  for (const scopePath of fsRead.scope) {
52
112
  const resolved = resolveScopePath(scopePath, projectRoot);
53
- // Only add if not already covered by an fs:write --bind for same path
54
- const alreadyWritable = fsWrite?.scope?.some(wp => resolveScopePath(wp, projectRoot) === resolved);
55
- if (!alreadyWritable) {
113
+ // Only add if not already covered by a writable bind for same path
114
+ if (!writableScopes.has(resolved)) {
56
115
  args.push('--ro-bind', resolved, resolved);
57
116
  }
58
117
  }
@@ -77,8 +136,8 @@ export function buildBwrapArgs(command, capabilities, projectRoot) {
77
136
  * Unlike sandbox-exec, bwrap uses CLI args only — no temp files are created,
78
137
  * so `cleanup()` is a no-op.
79
138
  */
80
- export function wrapWithBwrap(command, capabilities, projectRoot) {
81
- const args = buildBwrapArgs(command, capabilities, projectRoot);
139
+ export function wrapWithBwrap(command, capabilities, projectRoot, options = {}) {
140
+ const args = buildBwrapArgs(command, capabilities, projectRoot, options);
82
141
  return {
83
142
  bin: 'bwrap',
84
143
  args,
@@ -15,7 +15,7 @@ import { execSync } from 'node:child_process';
15
15
  import { existsSync } from 'node:fs';
16
16
  import { platform } from 'node:os';
17
17
  export const DEFAULT_SANDBOX_CONFIG = {
18
- enabled: true,
18
+ enabled: false,
19
19
  tier: 'auto',
20
20
  };
21
21
  // ============================================================================
@@ -7,10 +7,42 @@
7
7
  * @see https://github.com/eric-cielo/moflo/issues/410
8
8
  */
9
9
  import { writeFileSync, unlinkSync } from 'node:fs';
10
- import { join } from 'node:path';
11
- import { tmpdir } from 'node:os';
10
+ import { join, posix } from 'node:path';
11
+ import { tmpdir, homedir } from 'node:os';
12
12
  import { randomBytes } from 'node:crypto';
13
13
  import { resolveScopePath } from './sandbox-utils.js';
14
+ /**
15
+ * Home-directory paths that common CLI tools need writable to persist state.
16
+ * Rationale matches the Linux bwrap wrapper: `elevated`/`autonomous` steps
17
+ * spawn tools (claude, gh, git, npm) that write under $HOME; a pure
18
+ * deny-default profile makes those tools fail. System dirs (/etc, /usr,
19
+ * /private/var/root, etc.) remain denied.
20
+ */
21
+ const TOOL_HOME_PATHS = [
22
+ // Claude Code (dotfile + Application Support on macOS)
23
+ '.claude',
24
+ '.claude.json',
25
+ 'Library/Application Support/Claude',
26
+ 'Library/Application Support/claude-cli-nodejs',
27
+ 'Library/Caches/Claude',
28
+ 'Library/Preferences',
29
+ // GitHub CLI
30
+ '.config/gh',
31
+ // git
32
+ '.gitconfig',
33
+ '.git-credentials',
34
+ // npm
35
+ '.npmrc',
36
+ '.npm',
37
+ // Shared XDG locations
38
+ '.config',
39
+ '.cache',
40
+ '.local/share',
41
+ '.local/state',
42
+ ];
43
+ function needsToolHomeAccess(level) {
44
+ return level === 'elevated' || level === 'autonomous';
45
+ }
14
46
  // ============================================================================
15
47
  // Profile Generation
16
48
  // ============================================================================
@@ -28,7 +60,7 @@ import { resolveScopePath } from './sandbox-utils.js';
28
60
  * - net capability → allow network*
29
61
  * - No net capability → network denied (default deny covers it)
30
62
  */
31
- export function generateSandboxProfile(capabilities, projectRoot) {
63
+ export function generateSandboxProfile(capabilities, projectRoot, options = {}) {
32
64
  const lines = [
33
65
  '(version 1)',
34
66
  '(deny default)',
@@ -85,6 +117,23 @@ export function generateSandboxProfile(capabilities, projectRoot) {
85
117
  lines.push(`(allow file-write* (subpath "${escapeSbPath(projectRoot)}"))`);
86
118
  }
87
119
  }
120
+ // ── Tool home paths (elevated/autonomous only) ──────────────────────
121
+ // Allow writes to well-known CLI-tool home paths so spawned subcommands
122
+ // (claude, gh, git, npm) can persist config/credentials/cache. Uses
123
+ // `(subpath ...)` so non-existent paths are simply unmatched — no error.
124
+ if (needsToolHomeAccess(options.permissionLevel)) {
125
+ const home = options.homeDir ?? homedir();
126
+ if (home) {
127
+ lines.push('');
128
+ lines.push('; Tool home paths (elevated)');
129
+ for (const rel of TOOL_HOME_PATHS) {
130
+ // sandbox-exec is macOS-only → always POSIX paths
131
+ const resolved = posix.join(home, rel);
132
+ lines.push(`(allow file-read* (subpath "${escapeSbPath(resolved)}"))`);
133
+ lines.push(`(allow file-write* (subpath "${escapeSbPath(resolved)}"))`);
134
+ }
135
+ }
136
+ }
88
137
  // ── net ──────────────────────────────────────────────────────────────
89
138
  const hasNet = capabilities.some(c => c.type === 'net');
90
139
  if (hasNet) {
@@ -104,8 +153,8 @@ export function generateSandboxProfile(capabilities, projectRoot) {
104
153
  * Writes a temporary `.sb` profile and returns the sandbox-exec invocation.
105
154
  * The caller MUST call `cleanup()` after the process exits.
106
155
  */
107
- export function wrapWithSandboxExec(command, capabilities, projectRoot) {
108
- const profile = generateSandboxProfile(capabilities, projectRoot);
156
+ export function wrapWithSandboxExec(command, capabilities, projectRoot, options = {}) {
157
+ const profile = generateSandboxProfile(capabilities, projectRoot, options);
109
158
  const profilePath = join(tmpdir(), `moflo-sandbox-${randomBytes(8).toString('hex')}.sb`);
110
159
  writeFileSync(profilePath, profile, 'utf8');
111
160
  return {