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.
Files changed (3) hide show
  1. package/README.md +17 -17
  2. package/dist/bx.js +48 -15
  3. 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 `~/.bxallow` to grant access to shared utility directories
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 three optional config files — one entry per line, `#` for comments. Project `.bxignore` files are discovered recursively.
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
- Block additional dotdirs or files in your home. Paths relative to `$HOME`.
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
- These are blocked **in addition** to the built-in protected list:
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 `~/.bxallow` paths
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. **Write** the profile to `/tmp`, launch the app via `sandbox-exec`, clean up on exit
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.4.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 ~/.bxallow and return a set of all allowed directories.
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 parseAllowedDirs(home, workDirs) {
154
+ function parseHomeConfig(home, workDirs) {
153
155
  const allowed = new Set(workDirs);
154
- for (const line of parseLines(join(home, ".bxallow"))) {
155
- const absolute = resolve(home, line);
156
- if (existsSync(absolute) && statSync(absolute).isDirectory()) allowed.add(absolute);
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 allowed;
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
- applyIgnoreFile(join(home, ".bxignore"), home, ignored);
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
- ~/.bxallow extra allowed directories (one per line)
286
- ~/.bxignore extra blocked paths in $HOME (one per line)
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 blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, parseAllowedDirs(HOME, WORK_DIRS));
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
- const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths);
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`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bx-mac",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Launch apps in a macOS sandbox — only the project directory is accessible",
5
5
  "type": "module",
6
6
  "bin": {