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 +2 -2
- package/src/modules/cli/dist/src/config/moflo-config.js +2 -2
- package/src/modules/cli/dist/src/version.js +1 -1
- package/src/modules/cli/package.json +1 -1
- package/src/modules/spells/dist/commands/bash-command.js +6 -2
- package/src/modules/spells/dist/core/bwrap-sandbox.js +65 -6
- package/src/modules/spells/dist/core/platform-sandbox.js +1 -1
- package/src/modules/spells/dist/core/sandbox-profile.js +54 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.8.
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
@@ -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
|
|
54
|
-
|
|
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:
|
|
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 {
|