bx-mac 0.4.0 → 0.5.0
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/README.md +17 -17
- package/dist/bx.js +48 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ bx ~/work/my-project ~/work/shared-lib
|
|
|
28
28
|
- ⚙️ Keeps VSCode, extensions, shell, Node.js, and other tooling fully functional
|
|
29
29
|
- 🔍 Generates sandbox rules dynamically based on your actual `$HOME` contents
|
|
30
30
|
- 📝 Supports `.bxignore` files (searched recursively) to hide secrets like `.env` files within a project
|
|
31
|
-
- 📂 Supports `~/.
|
|
31
|
+
- 📂 Supports `rw:` and `ro:` prefixes in `~/.bxignore` to grant read-write or read-only access to extra directories
|
|
32
32
|
- 🗂️ Supports multiple working directories in a single sandbox
|
|
33
33
|
|
|
34
34
|
## 🚫 What it doesn't do
|
|
@@ -100,30 +100,29 @@ bx --verbose ~/work/my-project
|
|
|
100
100
|
|
|
101
101
|
## 📝 Configuration
|
|
102
102
|
|
|
103
|
-
bx uses
|
|
104
|
-
|
|
105
|
-
### `~/.bxallow`
|
|
106
|
-
|
|
107
|
-
Allow extra directories beyond the working directory. Paths relative to `$HOME`.
|
|
108
|
-
|
|
109
|
-
```gitignore
|
|
110
|
-
# Shared shell scripts and utilities
|
|
111
|
-
work/bin
|
|
112
|
-
shared/libs
|
|
113
|
-
```
|
|
103
|
+
bx uses two optional config files — one entry per line, `#` for comments. Project `.bxignore` files are discovered recursively.
|
|
114
104
|
|
|
115
105
|
### `~/.bxignore`
|
|
116
106
|
|
|
117
|
-
|
|
107
|
+
Unified sandbox rules for your home directory. Paths relative to `$HOME`. Each line is either a deny rule (no prefix) or an access grant (`rw:` / `ro:` prefix, case-insensitive).
|
|
118
108
|
|
|
119
109
|
```gitignore
|
|
110
|
+
# Block additional sensitive paths (no prefix = deny)
|
|
120
111
|
.aws
|
|
121
112
|
.azure
|
|
122
113
|
.kube
|
|
123
114
|
.config/gcloud
|
|
115
|
+
|
|
116
|
+
# Allow read-write access to extra directories
|
|
117
|
+
rw:work/bin
|
|
118
|
+
rw:shared/libs
|
|
119
|
+
|
|
120
|
+
# Allow read-only access (can read but not modify)
|
|
121
|
+
ro:reference/docs
|
|
122
|
+
ro:shared/toolchain
|
|
124
123
|
```
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
Deny rules are applied **in addition** to the built-in protected list:
|
|
127
126
|
|
|
128
127
|
> 🔒 `.Trash` `.ssh` `.gnupg` `.docker` `.zsh_sessions` `.cargo` `.gradle` `.gem`
|
|
129
128
|
|
|
@@ -155,10 +154,11 @@ bx generates a macOS sandbox profile at launch time:
|
|
|
155
154
|
|
|
156
155
|
1. **Scan** `$HOME` for non-hidden directories
|
|
157
156
|
2. **Block** each one individually with `(deny file* (subpath ...))`
|
|
158
|
-
3. **Skip** all working directories, `~/Library`, dotfiles, and
|
|
157
|
+
3. **Skip** all working directories, `~/Library`, dotfiles, and `rw:`/`ro:` paths from `~/.bxignore`
|
|
159
158
|
4. **Descend** into parent directories of allowed paths to block only siblings (because SBPL deny rules always override allow rules)
|
|
160
|
-
5. **Append** deny rules for protected dotdirs, `~/.bxignore`, and `.bxignore` files found recursively in each working directory
|
|
161
|
-
6. **
|
|
159
|
+
5. **Append** deny rules for protected dotdirs, plain entries in `~/.bxignore`, and `.bxignore` files found recursively in each working directory
|
|
160
|
+
6. **Apply** `(deny file-write*)` rules for `ro:` directories (read allowed, write blocked)
|
|
161
|
+
7. **Write** the profile to `/tmp`, launch the app via `sandbox-exec`, clean up on exit
|
|
162
162
|
|
|
163
163
|
### Why not a simple deny-all + allow?
|
|
164
164
|
|
package/dist/bx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const __VERSION__ = "0.
|
|
2
|
+
const __VERSION__ = "0.5.0";
|
|
3
3
|
import { accessSync, constants, cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -147,15 +147,39 @@ function collectIgnoreFilesRecursive(dir, ignored) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
|
-
* Parse ~/.
|
|
150
|
+
* Parse ~/.bxignore for RW:/RO: prefixed lines and return allowed directories.
|
|
151
|
+
* Lines without prefix are ignored here (handled by collectIgnoredPaths).
|
|
152
|
+
* Also checks for deprecated ~/.bxallow and migrates its entries.
|
|
151
153
|
*/
|
|
152
|
-
function
|
|
154
|
+
function parseHomeConfig(home, workDirs) {
|
|
153
155
|
const allowed = new Set(workDirs);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
const readOnly = /* @__PURE__ */ new Set();
|
|
157
|
+
const bxallowPath = join(home, ".bxallow");
|
|
158
|
+
if (existsSync(bxallowPath)) {
|
|
159
|
+
console.error("sandbox: WARNING — ~/.bxallow is deprecated. Move entries to ~/.bxignore with RW: prefix.");
|
|
160
|
+
for (const line of parseLines(bxallowPath)) {
|
|
161
|
+
const absolute = resolve(home, line);
|
|
162
|
+
if (existsSync(absolute) && statSync(absolute).isDirectory()) allowed.add(absolute);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const line of parseLines(join(home, ".bxignore"))) {
|
|
166
|
+
let prefix = "";
|
|
167
|
+
let path = line;
|
|
168
|
+
const match = line.match(/^(RW|RO):(.+)$/i);
|
|
169
|
+
if (match) {
|
|
170
|
+
prefix = match[1].toUpperCase();
|
|
171
|
+
path = match[2].trim();
|
|
172
|
+
}
|
|
173
|
+
if (!prefix) continue;
|
|
174
|
+
const absolute = resolve(home, path);
|
|
175
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) continue;
|
|
176
|
+
if (prefix === "RW") allowed.add(absolute);
|
|
177
|
+
else readOnly.add(absolute);
|
|
157
178
|
}
|
|
158
|
-
return
|
|
179
|
+
return {
|
|
180
|
+
allowed,
|
|
181
|
+
readOnly
|
|
182
|
+
};
|
|
159
183
|
}
|
|
160
184
|
/**
|
|
161
185
|
* Recursively collect directories to block under parentDir.
|
|
@@ -180,22 +204,27 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
|
|
|
180
204
|
}
|
|
181
205
|
/**
|
|
182
206
|
* Collect paths to deny from .bxignore files and built-in protected dotdirs.
|
|
183
|
-
* Searches ~/.bxignore and recursively through all workdirs.
|
|
207
|
+
* Searches ~/.bxignore (skipping RW:/RO: lines) and recursively through all workdirs.
|
|
184
208
|
*/
|
|
185
209
|
function collectIgnoredPaths(home, workDirs) {
|
|
186
210
|
const ignored = PROTECTED_DOTDIRS.map((d) => join(home, d));
|
|
187
|
-
|
|
211
|
+
const globalIgnore = join(home, ".bxignore");
|
|
212
|
+
if (existsSync(globalIgnore)) {
|
|
213
|
+
const denyLines = parseLines(globalIgnore).filter((l) => !l.match(/^(RW|RO):/i));
|
|
214
|
+
for (const line of denyLines) for (const match of globSync(line, { cwd: home })) ignored.push(resolve(home, match));
|
|
215
|
+
}
|
|
188
216
|
for (const workDir of workDirs) collectIgnoreFilesRecursive(workDir, ignored);
|
|
189
217
|
return ignored;
|
|
190
218
|
}
|
|
191
219
|
/**
|
|
192
220
|
* Generate the SBPL sandbox profile string.
|
|
193
221
|
*/
|
|
194
|
-
function generateProfile(workDirs, blockedDirs, ignoredPaths) {
|
|
222
|
+
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = []) {
|
|
195
223
|
const denyRules = blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n");
|
|
196
224
|
const ignoredRules = ignoredPaths.length > 0 ? `\n; Hidden paths from .bxignore\n(deny file*\n${ignoredPaths.map((p) => {
|
|
197
225
|
return existsSync(p) && statSync(p).isDirectory() ? ` (subpath "${p}")` : ` (literal "${p}")`;
|
|
198
226
|
}).join("\n")}\n)\n` : "";
|
|
227
|
+
const readOnlyRules = readOnlyDirs.length > 0 ? `\n; Read-only directories\n(deny file-write*\n${readOnlyDirs.map((dir) => ` (subpath "${dir}")`).join("\n")}\n)\n` : "";
|
|
199
228
|
return `; Auto-generated sandbox profile
|
|
200
229
|
; Working directories: ${workDirs.join(", ")}
|
|
201
230
|
|
|
@@ -206,7 +235,7 @@ function generateProfile(workDirs, blockedDirs, ignoredPaths) {
|
|
|
206
235
|
(deny file*
|
|
207
236
|
${denyRules}
|
|
208
237
|
)
|
|
209
|
-
${ignoredRules}
|
|
238
|
+
${ignoredRules}${readOnlyRules}
|
|
210
239
|
`;
|
|
211
240
|
}
|
|
212
241
|
//#endregion
|
|
@@ -282,8 +311,10 @@ Options:
|
|
|
282
311
|
-h, --help show this help
|
|
283
312
|
|
|
284
313
|
Configuration:
|
|
285
|
-
~/.
|
|
286
|
-
|
|
314
|
+
~/.bxignore sandbox rules (one per line):
|
|
315
|
+
path block access (deny)
|
|
316
|
+
rw:path allow read-write access
|
|
317
|
+
ro:path allow read-only access
|
|
287
318
|
<workdir>/.bxignore blocked paths in project (supports globs, searched recursively)
|
|
288
319
|
|
|
289
320
|
https://github.com/holtwick/bx-mac`);
|
|
@@ -297,11 +328,13 @@ const HOME = process.env.HOME;
|
|
|
297
328
|
const WORK_DIRS = workArgs.map((a) => resolve(a));
|
|
298
329
|
checkWorkDirs(WORK_DIRS, HOME);
|
|
299
330
|
if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
|
|
300
|
-
const
|
|
331
|
+
const { allowed, readOnly } = parseHomeConfig(HOME, WORK_DIRS);
|
|
332
|
+
const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
|
|
301
333
|
const ignoredPaths = collectIgnoredPaths(HOME, WORK_DIRS);
|
|
302
334
|
const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
|
|
303
335
|
if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
|
|
304
|
-
|
|
336
|
+
if (readOnly.size > 0) console.error(`sandbox: ${readOnly.size} read-only director${readOnly.size === 1 ? "y" : "ies"}`);
|
|
337
|
+
const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths, [...readOnly]);
|
|
305
338
|
const profilePath = join("/tmp", `bx-${process.pid}.sb`);
|
|
306
339
|
writeFileSync(profilePath, profile);
|
|
307
340
|
const dirLabel = WORK_DIRS.length === 1 ? WORK_DIRS[0] : `${WORK_DIRS.length} directories`;
|