bx-mac 0.4.0 → 0.6.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 +31 -20
- package/dist/bx-native +0 -0
- package/dist/bx.js +62 -17
- package/package.json +3 -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,43 +100,53 @@ 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
|
|
|
130
129
|
### `<project>/.bxignore`
|
|
131
130
|
|
|
132
|
-
Block paths within the working directory.
|
|
131
|
+
Block paths within the working directory. Uses [`.gitignore`-style pattern matching](https://git-scm.com/docs/gitignore#_pattern_format):
|
|
132
|
+
|
|
133
|
+
| Pattern | Matches | Why |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `.env` | `.env` at any depth | No `/` → recursive |
|
|
136
|
+
| `.env.*` | `.env.local`, `sub/.env.production` | No `/` → recursive |
|
|
137
|
+
| `*.pem` | `key.pem`, `sub/deep/cert.pem` | No `/` → recursive |
|
|
138
|
+
| `secrets/` | `secrets/` at any depth | Trailing `/` is a dir marker, not a path separator |
|
|
139
|
+
| `/.env` | Only `<workdir>/.env` | Leading `/` → anchored to root |
|
|
140
|
+
| `config/secrets` | Only `<workdir>/config/secrets` | Contains `/` → relative to workdir |
|
|
141
|
+
|
|
142
|
+
bx searches for `.bxignore` files **recursively** through the entire project tree (skipping `.`-prefixed dirs and `node_modules`), so you can place them in subdirectories to hide secrets close to where they live.
|
|
133
143
|
|
|
134
144
|
```gitignore
|
|
135
145
|
.env
|
|
136
146
|
.env.*
|
|
137
147
|
secrets/
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
*.pem
|
|
149
|
+
*.key
|
|
140
150
|
```
|
|
141
151
|
|
|
142
152
|
For example, a monorepo might have:
|
|
@@ -155,10 +165,11 @@ bx generates a macOS sandbox profile at launch time:
|
|
|
155
165
|
|
|
156
166
|
1. **Scan** `$HOME` for non-hidden directories
|
|
157
167
|
2. **Block** each one individually with `(deny file* (subpath ...))`
|
|
158
|
-
3. **Skip** all working directories, `~/Library`, dotfiles, and
|
|
168
|
+
3. **Skip** all working directories, `~/Library`, dotfiles, and `rw:`/`ro:` paths from `~/.bxignore`
|
|
159
169
|
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. **
|
|
170
|
+
5. **Append** deny rules for protected dotdirs, plain entries in `~/.bxignore`, and `.bxignore` files found recursively in each working directory
|
|
171
|
+
6. **Apply** `(deny file-write*)` rules for `ro:` directories (read allowed, write blocked)
|
|
172
|
+
7. **Write** the profile to `/tmp`, launch the app via `sandbox-exec`, clean up on exit
|
|
162
173
|
|
|
163
174
|
### Why not a simple deny-all + allow?
|
|
164
175
|
|
package/dist/bx-native
ADDED
|
Binary file
|
package/dist/bx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const __VERSION__ = "0.
|
|
2
|
+
const __VERSION__ = "0.6.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";
|
|
@@ -121,10 +121,22 @@ function parseLines(filePath) {
|
|
|
121
121
|
return readFileSync(filePath, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
+
* Convert a .bxignore line to a glob pattern following .gitignore semantics:
|
|
125
|
+
* - Leading "/" anchors to the base dir (stripped before globbing)
|
|
126
|
+
* - Patterns without "/" (except trailing) match recursively via ** / prefix
|
|
127
|
+
* - Patterns with "/" (non-leading, non-trailing) are relative to baseDir
|
|
128
|
+
* - Trailing "/" marks directories only and doesn't count as path separator
|
|
129
|
+
*/
|
|
130
|
+
function toGlobPattern(line) {
|
|
131
|
+
if (line.startsWith("/")) return line.slice(1);
|
|
132
|
+
if ((line.endsWith("/") ? line.slice(0, -1) : line).includes("/")) return line;
|
|
133
|
+
return `**/${line}`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
124
136
|
* Apply a single .bxignore file: resolve glob patterns relative to baseDir.
|
|
125
137
|
*/
|
|
126
138
|
function applyIgnoreFile(filePath, baseDir, ignored) {
|
|
127
|
-
for (const line of parseLines(filePath)) for (const match of globSync(line, { cwd: baseDir })) ignored.push(resolve(baseDir, match));
|
|
139
|
+
for (const line of parseLines(filePath)) for (const match of globSync(toGlobPattern(line), { cwd: baseDir })) ignored.push(resolve(baseDir, match));
|
|
128
140
|
}
|
|
129
141
|
/**
|
|
130
142
|
* Recursively find and apply .bxignore files in a directory tree.
|
|
@@ -147,15 +159,39 @@ function collectIgnoreFilesRecursive(dir, ignored) {
|
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
/**
|
|
150
|
-
* Parse ~/.
|
|
162
|
+
* Parse ~/.bxignore for RW:/RO: prefixed lines and return allowed directories.
|
|
163
|
+
* Lines without prefix are ignored here (handled by collectIgnoredPaths).
|
|
164
|
+
* Also checks for deprecated ~/.bxallow and migrates its entries.
|
|
151
165
|
*/
|
|
152
|
-
function
|
|
166
|
+
function parseHomeConfig(home, workDirs) {
|
|
153
167
|
const allowed = new Set(workDirs);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
168
|
+
const readOnly = /* @__PURE__ */ new Set();
|
|
169
|
+
const bxallowPath = join(home, ".bxallow");
|
|
170
|
+
if (existsSync(bxallowPath)) {
|
|
171
|
+
console.error("sandbox: WARNING — ~/.bxallow is deprecated. Move entries to ~/.bxignore with RW: prefix.");
|
|
172
|
+
for (const line of parseLines(bxallowPath)) {
|
|
173
|
+
const absolute = resolve(home, line);
|
|
174
|
+
if (existsSync(absolute) && statSync(absolute).isDirectory()) allowed.add(absolute);
|
|
175
|
+
}
|
|
157
176
|
}
|
|
158
|
-
|
|
177
|
+
for (const line of parseLines(join(home, ".bxignore"))) {
|
|
178
|
+
let prefix = "";
|
|
179
|
+
let path = line;
|
|
180
|
+
const match = line.match(/^(RW|RO):(.+)$/i);
|
|
181
|
+
if (match) {
|
|
182
|
+
prefix = match[1].toUpperCase();
|
|
183
|
+
path = match[2].trim();
|
|
184
|
+
}
|
|
185
|
+
if (!prefix) continue;
|
|
186
|
+
const absolute = resolve(home, path);
|
|
187
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) continue;
|
|
188
|
+
if (prefix === "RW") allowed.add(absolute);
|
|
189
|
+
else readOnly.add(absolute);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
allowed,
|
|
193
|
+
readOnly
|
|
194
|
+
};
|
|
159
195
|
}
|
|
160
196
|
/**
|
|
161
197
|
* Recursively collect directories to block under parentDir.
|
|
@@ -180,22 +216,27 @@ function collectBlockedDirs(parentDir, home, scriptDir, allowedDirs) {
|
|
|
180
216
|
}
|
|
181
217
|
/**
|
|
182
218
|
* Collect paths to deny from .bxignore files and built-in protected dotdirs.
|
|
183
|
-
* Searches ~/.bxignore and recursively through all workdirs.
|
|
219
|
+
* Searches ~/.bxignore (skipping RW:/RO: lines) and recursively through all workdirs.
|
|
184
220
|
*/
|
|
185
221
|
function collectIgnoredPaths(home, workDirs) {
|
|
186
222
|
const ignored = PROTECTED_DOTDIRS.map((d) => join(home, d));
|
|
187
|
-
|
|
223
|
+
const globalIgnore = join(home, ".bxignore");
|
|
224
|
+
if (existsSync(globalIgnore)) {
|
|
225
|
+
const denyLines = parseLines(globalIgnore).filter((l) => !l.match(/^(RW|RO):/i));
|
|
226
|
+
for (const line of denyLines) for (const match of globSync(toGlobPattern(line), { cwd: home })) ignored.push(resolve(home, match));
|
|
227
|
+
}
|
|
188
228
|
for (const workDir of workDirs) collectIgnoreFilesRecursive(workDir, ignored);
|
|
189
229
|
return ignored;
|
|
190
230
|
}
|
|
191
231
|
/**
|
|
192
232
|
* Generate the SBPL sandbox profile string.
|
|
193
233
|
*/
|
|
194
|
-
function generateProfile(workDirs, blockedDirs, ignoredPaths) {
|
|
234
|
+
function generateProfile(workDirs, blockedDirs, ignoredPaths, readOnlyDirs = []) {
|
|
195
235
|
const denyRules = blockedDirs.map((dir) => ` (subpath "${dir}")`).join("\n");
|
|
196
236
|
const ignoredRules = ignoredPaths.length > 0 ? `\n; Hidden paths from .bxignore\n(deny file*\n${ignoredPaths.map((p) => {
|
|
197
237
|
return existsSync(p) && statSync(p).isDirectory() ? ` (subpath "${p}")` : ` (literal "${p}")`;
|
|
198
238
|
}).join("\n")}\n)\n` : "";
|
|
239
|
+
const readOnlyRules = readOnlyDirs.length > 0 ? `\n; Read-only directories\n(deny file-write*\n${readOnlyDirs.map((dir) => ` (subpath "${dir}")`).join("\n")}\n)\n` : "";
|
|
199
240
|
return `; Auto-generated sandbox profile
|
|
200
241
|
; Working directories: ${workDirs.join(", ")}
|
|
201
242
|
|
|
@@ -206,7 +247,7 @@ function generateProfile(workDirs, blockedDirs, ignoredPaths) {
|
|
|
206
247
|
(deny file*
|
|
207
248
|
${denyRules}
|
|
208
249
|
)
|
|
209
|
-
${ignoredRules}
|
|
250
|
+
${ignoredRules}${readOnlyRules}
|
|
210
251
|
`;
|
|
211
252
|
}
|
|
212
253
|
//#endregion
|
|
@@ -282,9 +323,11 @@ Options:
|
|
|
282
323
|
-h, --help show this help
|
|
283
324
|
|
|
284
325
|
Configuration:
|
|
285
|
-
~/.
|
|
286
|
-
|
|
287
|
-
|
|
326
|
+
~/.bxignore sandbox rules (one per line):
|
|
327
|
+
path block access (deny)
|
|
328
|
+
rw:path allow read-write access
|
|
329
|
+
ro:path allow read-only access
|
|
330
|
+
<workdir>/.bxignore blocked paths in project (.gitignore-style matching)
|
|
288
331
|
|
|
289
332
|
https://github.com/holtwick/bx-mac`);
|
|
290
333
|
process.exit(0);
|
|
@@ -297,11 +340,13 @@ const HOME = process.env.HOME;
|
|
|
297
340
|
const WORK_DIRS = workArgs.map((a) => resolve(a));
|
|
298
341
|
checkWorkDirs(WORK_DIRS, HOME);
|
|
299
342
|
if (mode === "code" && profileSandbox) setupVSCodeProfile(HOME);
|
|
300
|
-
const
|
|
343
|
+
const { allowed, readOnly } = parseHomeConfig(HOME, WORK_DIRS);
|
|
344
|
+
const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, new Set([...allowed, ...readOnly]));
|
|
301
345
|
const ignoredPaths = collectIgnoredPaths(HOME, WORK_DIRS);
|
|
302
346
|
const extraIgnored = ignoredPaths.length - PROTECTED_DOTDIRS.length;
|
|
303
347
|
if (extraIgnored > 0) console.error(`sandbox: .bxignore hides ${extraIgnored} extra path(s)`);
|
|
304
|
-
|
|
348
|
+
if (readOnly.size > 0) console.error(`sandbox: ${readOnly.size} read-only director${readOnly.size === 1 ? "y" : "ies"}`);
|
|
349
|
+
const profile = generateProfile(WORK_DIRS, blockedDirs, ignoredPaths, [...readOnly]);
|
|
305
350
|
const profilePath = join("/tmp", `bx-${process.pid}.sb`);
|
|
306
351
|
writeFileSync(profilePath, profile);
|
|
307
352
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Launch apps in a macOS sandbox — only the project directory is accessible",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "rolldown -c",
|
|
14
|
+
"build:native": "bun build src/index.ts --compile --outfile dist/bx-native --define \"__VERSION__=\\\"$(node -p \"require('./package.json').version\")\\\"\"",
|
|
15
|
+
"sign": "./scripts/sign.sh",
|
|
14
16
|
"test": "vitest run",
|
|
15
17
|
"prepublishOnly": "npm run build",
|
|
16
18
|
"post:release": "./scripts/release.sh"
|