bx-mac 1.4.0 โ 1.5.1
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 +19 -3
- package/dist/bx.js +100 -17
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -234,23 +234,32 @@ Unified sandbox rules for your home directory. Paths relative to `$HOME`. Each l
|
|
|
234
234
|
.kube
|
|
235
235
|
.config/gcloud
|
|
236
236
|
|
|
237
|
-
# Allow read-write access to extra
|
|
237
|
+
# Allow read-write access to extra paths
|
|
238
238
|
rw:work/bin
|
|
239
239
|
rw:shared/libs
|
|
240
|
+
rw:~/projects/* # globs supported, ~ is expanded
|
|
240
241
|
|
|
241
242
|
# Allow read-only access (can read but not modify)
|
|
242
243
|
ro:reference/docs
|
|
243
244
|
ro:shared/toolchain
|
|
245
|
+
ro:.npmrc # files work too; overrides built-in block
|
|
244
246
|
```
|
|
245
247
|
|
|
246
|
-
|
|
248
|
+
Plain deny rules have two scopes:
|
|
247
249
|
|
|
248
|
-
|
|
250
|
+
- **At `$HOME` top level only** (no recursive `**` walk). `secrets/` matches `~/secrets`, not `~/nested/secrets`; `.config/gcloud` matches as a literal.
|
|
251
|
+
- **Recursively inside each workdir** - same semantics as a project-level `.bxignore`, so `secrets/` and `*.pem` apply across every project you open. The recursive scan skips `node_modules`, `.git`, caches, `DerivedData`, `Pods`, and similar subtrees for speed.
|
|
252
|
+
|
|
253
|
+
Deny rules are applied **in addition** to the built-in protected lists. `rw:` and `ro:` entries however **override** them - `ro:.npmrc` exposes the otherwise-blocked `~/.npmrc` read-only, `rw:.aws` opens the AWS credentials directory completely. Files (not just directories) are accepted, `~/...` is expanded, and globs (`*`, `**`) are matched against `$HOME`. Use override entries deliberately - you are weakening the default protection. `bx` prints a warning on stderr when an override exposes a built-in protected path.
|
|
254
|
+
|
|
255
|
+
Built-in protected lists:
|
|
249
256
|
|
|
250
257
|
> ๐ **Dotdirs:** `.ssh` `.gnupg` `.docker` `.zsh_sessions` `.cargo` `.gradle` `.gem`
|
|
251
258
|
>
|
|
252
259
|
> ๐๏ธ **Library (opinionated):** `Accounts` `Calendars` `Contacts` `Cookies` `Finance` `Mail` `Messages` `Mobile Documents` `Photos` `Safari` and [others (see full list)](src/profile.ts) โ plus containers of password managers & finance apps
|
|
253
260
|
|
|
261
|
+
**Limitation:** Overrides only work on whole protected paths. `rw:.aws/profile.json` does not selectively unblock a file inside `~/.aws` - the parent deny still wins (Apple SBPL: deny beats allow). Use `rw:.aws` to open the entire directory.
|
|
262
|
+
|
|
254
263
|
### `<project>/.bxignore`
|
|
255
264
|
|
|
256
265
|
Block paths within the working directory. Uses [`.gitignore`-style pattern matching](https://git-scm.com/docs/gitignore#_pattern_format):
|
|
@@ -284,6 +293,13 @@ my-project/deploy/.bxignore # deployment credentials
|
|
|
284
293
|
|
|
285
294
|
Each `.bxignore` resolves its patterns relative to its own directory.
|
|
286
295
|
|
|
296
|
+
Project `.bxignore` also accepts `ro:` entries to make paths within the workdir read-only (write-protect generated files, vendored libraries, etc.). `rw:` is silently ignored here - the workdir is already read-write by default.
|
|
297
|
+
|
|
298
|
+
```gitignore
|
|
299
|
+
ro:vendor/
|
|
300
|
+
ro:generated/schema.ts
|
|
301
|
+
```
|
|
302
|
+
|
|
287
303
|
### Self-protecting directories
|
|
288
304
|
|
|
289
305
|
You can make any directory protect itself โ no global configuration needed. There are two ways:
|
package/dist/bx.js
CHANGED
|
@@ -1045,6 +1045,50 @@ function toGlobPattern(line) {
|
|
|
1045
1045
|
function resolveGlobMatches(pattern, baseDir) {
|
|
1046
1046
|
return globSync(toGlobPattern(pattern), { cwd: baseDir }).map((match) => resolve(baseDir, match));
|
|
1047
1047
|
}
|
|
1048
|
+
const SCAN_EXCLUDE_NAMES = new Set([
|
|
1049
|
+
"Library",
|
|
1050
|
+
"Applications",
|
|
1051
|
+
"node_modules",
|
|
1052
|
+
".git",
|
|
1053
|
+
".Trash",
|
|
1054
|
+
".cache",
|
|
1055
|
+
".npm",
|
|
1056
|
+
".pnpm-store",
|
|
1057
|
+
".yarn",
|
|
1058
|
+
".cargo",
|
|
1059
|
+
".rustup",
|
|
1060
|
+
".gradle",
|
|
1061
|
+
".gem",
|
|
1062
|
+
".m2",
|
|
1063
|
+
".nvm",
|
|
1064
|
+
".bun",
|
|
1065
|
+
".deno",
|
|
1066
|
+
"DerivedData",
|
|
1067
|
+
"Pods"
|
|
1068
|
+
]);
|
|
1069
|
+
function scanExcludeFilter(entry) {
|
|
1070
|
+
const name = typeof entry === "string" ? entry.split("/").pop() ?? "" : entry?.name ?? "";
|
|
1071
|
+
return SCAN_EXCLUDE_NAMES.has(name);
|
|
1072
|
+
}
|
|
1073
|
+
function resolveGlobMatchesBatch(patterns, baseDir) {
|
|
1074
|
+
if (patterns.length === 0) return [];
|
|
1075
|
+
return globSync(patterns.map(toGlobPattern), {
|
|
1076
|
+
cwd: baseDir,
|
|
1077
|
+
exclude: scanExcludeFilter
|
|
1078
|
+
}).map((match) => resolve(baseDir, match));
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Resolve patterns against `baseDir` at top level only (no recursive `**`
|
|
1082
|
+
* expansion). Trailing slashes are stripped; leading slashes are stripped
|
|
1083
|
+
* (already anchored). Used for `~/.bxignore` in $HOME to keep the lookup
|
|
1084
|
+
* cheap while still matching literal/glob paths at the home root.
|
|
1085
|
+
*/
|
|
1086
|
+
function resolveTopLevelMatches(patterns, baseDir) {
|
|
1087
|
+
if (patterns.length === 0) return [];
|
|
1088
|
+
const cleaned = patterns.map((p) => p.startsWith("/") ? p.slice(1) : p).map((p) => p.endsWith("/") ? p.slice(0, -1) : p).filter((p) => p.length > 0);
|
|
1089
|
+
if (cleaned.length === 0) return [];
|
|
1090
|
+
return globSync(cleaned, { cwd: baseDir }).map((m) => resolve(baseDir, m));
|
|
1091
|
+
}
|
|
1048
1092
|
/**
|
|
1049
1093
|
* A directory is self-protected if it contains a `.bxprotect` file
|
|
1050
1094
|
* or a `.bxignore` with a bare `/` entry. Self-protected directories
|
|
@@ -1055,19 +1099,27 @@ function isSelfProtected(dir) {
|
|
|
1055
1099
|
if (existsSync(join(dir, ".bxprotect"))) return true;
|
|
1056
1100
|
return parseLines(join(dir, ".bxignore")).some((l) => l === "/" || l === ".");
|
|
1057
1101
|
}
|
|
1058
|
-
function applyIgnoreFile(filePath, baseDir, ignored) {
|
|
1102
|
+
function applyIgnoreFile(filePath, baseDir, ignored, readOnly) {
|
|
1059
1103
|
for (const line of parseLines(filePath)) {
|
|
1060
1104
|
if (line === "/" || line === ".") continue;
|
|
1105
|
+
const accessMatch = line.match(ACCESS_PREFIX_RE);
|
|
1106
|
+
if (accessMatch) {
|
|
1107
|
+
if (!readOnly) continue;
|
|
1108
|
+
const [, prefix, rawPath] = accessMatch;
|
|
1109
|
+
if (prefix.toUpperCase() !== "RO") continue;
|
|
1110
|
+
for (const m of resolveGlobMatches(rawPath.trim(), baseDir)) readOnly.add(realpathSafe(m));
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1061
1113
|
ignored.push(...resolveGlobMatches(line, baseDir));
|
|
1062
1114
|
}
|
|
1063
1115
|
}
|
|
1064
|
-
function collectIgnoreFilesRecursive(dir, ignored) {
|
|
1116
|
+
function collectIgnoreFilesRecursive(dir, ignored, readOnly) {
|
|
1065
1117
|
if (isSelfProtected(dir)) {
|
|
1066
1118
|
ignored.push(dir);
|
|
1067
1119
|
return;
|
|
1068
1120
|
}
|
|
1069
1121
|
const ignoreFile = join(dir, ".bxignore");
|
|
1070
|
-
if (existsSync(ignoreFile)) applyIgnoreFile(ignoreFile, dir, ignored);
|
|
1122
|
+
if (existsSync(ignoreFile)) applyIgnoreFile(ignoreFile, dir, ignored, readOnly);
|
|
1071
1123
|
let entries;
|
|
1072
1124
|
try {
|
|
1073
1125
|
entries = readdirSync(dir);
|
|
@@ -1078,11 +1130,29 @@ function collectIgnoreFilesRecursive(dir, ignored) {
|
|
|
1078
1130
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
1079
1131
|
const fullPath = join(dir, name);
|
|
1080
1132
|
try {
|
|
1081
|
-
if (statSync(fullPath).isDirectory()) collectIgnoreFilesRecursive(fullPath, ignored);
|
|
1133
|
+
if (statSync(fullPath).isDirectory()) collectIgnoreFilesRecursive(fullPath, ignored, readOnly);
|
|
1082
1134
|
} catch {}
|
|
1083
1135
|
}
|
|
1084
1136
|
}
|
|
1085
1137
|
const ACCESS_PREFIX_RE = /^(RW|RO):(.+)$/i;
|
|
1138
|
+
function expandHomePath(home, raw) {
|
|
1139
|
+
const trimmed = raw.trim();
|
|
1140
|
+
if (trimmed === "~") return home;
|
|
1141
|
+
if (trimmed.startsWith("~/")) return join(home, trimmed.slice(2));
|
|
1142
|
+
return resolve(home, trimmed);
|
|
1143
|
+
}
|
|
1144
|
+
function realpathSafe(p) {
|
|
1145
|
+
try {
|
|
1146
|
+
return realpathSync(p);
|
|
1147
|
+
} catch {
|
|
1148
|
+
return p;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function resolveAccessTargets(home, raw) {
|
|
1152
|
+
const expanded = expandHomePath(home, raw);
|
|
1153
|
+
if (existsSync(expanded)) return [realpathSafe(expanded)];
|
|
1154
|
+
return globSync(raw.trim().replace(/^~\//, ""), { cwd: home }).map((m) => realpathSafe(join(home, m))).filter((p) => existsSync(p));
|
|
1155
|
+
}
|
|
1086
1156
|
function parseHomeConfig(home, workDirs) {
|
|
1087
1157
|
const allowed = new Set(workDirs);
|
|
1088
1158
|
const readOnly = /* @__PURE__ */ new Set();
|
|
@@ -1090,10 +1160,10 @@ function parseHomeConfig(home, workDirs) {
|
|
|
1090
1160
|
const match = line.match(ACCESS_PREFIX_RE);
|
|
1091
1161
|
if (!match) continue;
|
|
1092
1162
|
const [, prefix, rawPath] = match;
|
|
1093
|
-
const
|
|
1094
|
-
if (
|
|
1095
|
-
|
|
1096
|
-
|
|
1163
|
+
const targets = resolveAccessTargets(home, rawPath);
|
|
1164
|
+
if (targets.length === 0) continue;
|
|
1165
|
+
const target = prefix.toUpperCase() === "RW" ? allowed : readOnly;
|
|
1166
|
+
for (const t of targets) target.add(t);
|
|
1097
1167
|
}
|
|
1098
1168
|
return {
|
|
1099
1169
|
allowed,
|
|
@@ -1143,22 +1213,26 @@ function collectProtectedContainers(home) {
|
|
|
1143
1213
|
}
|
|
1144
1214
|
return matched;
|
|
1145
1215
|
}
|
|
1216
|
+
function isOverridden(path, overrides) {
|
|
1217
|
+
return overrides.has(path) || overrides.has(realpathSafe(path));
|
|
1218
|
+
}
|
|
1146
1219
|
function collectReadOnlyDotfiles(home, overrides = /* @__PURE__ */ new Set()) {
|
|
1147
|
-
return PROTECTED_HOME_DOTFILES_RO.map((f) => join(home, f)).filter((p) => !
|
|
1220
|
+
return PROTECTED_HOME_DOTFILES_RO.map((f) => join(home, f)).filter((p) => !isOverridden(p, overrides));
|
|
1148
1221
|
}
|
|
1149
|
-
function collectIgnoredPaths(home, workDirs, overrides = /* @__PURE__ */ new Set()) {
|
|
1222
|
+
function collectIgnoredPaths(home, workDirs, overrides = /* @__PURE__ */ new Set(), readOnly) {
|
|
1150
1223
|
const ignored = [
|
|
1151
1224
|
...PROTECTED_DOTDIRS.map((d) => join(home, d)),
|
|
1152
1225
|
...PROTECTED_HOME_DOTFILES.map((f) => join(home, f)),
|
|
1153
1226
|
...PROTECTED_LIBRARY_DIRS.map((d) => join(home, "Library", d)),
|
|
1154
1227
|
...new Set(collectProtectedContainers(home))
|
|
1155
|
-
].filter((p) => !
|
|
1228
|
+
].filter((p) => !isOverridden(p, overrides));
|
|
1156
1229
|
const globalIgnore = join(home, ".bxignore");
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1230
|
+
const globalDenyLines = existsSync(globalIgnore) ? parseLines(globalIgnore).filter((l) => !ACCESS_PREFIX_RE.test(l)) : [];
|
|
1231
|
+
for (const m of resolveTopLevelMatches(globalDenyLines, home)) if (!isOverridden(m, overrides)) ignored.push(m);
|
|
1232
|
+
for (const workDir of workDirs) {
|
|
1233
|
+
for (const m of resolveGlobMatchesBatch(globalDenyLines, workDir)) if (!isOverridden(m, overrides)) ignored.push(m);
|
|
1234
|
+
collectIgnoreFilesRecursive(workDir, ignored, readOnly);
|
|
1160
1235
|
}
|
|
1161
|
-
for (const workDir of workDirs) collectIgnoreFilesRecursive(workDir, ignored);
|
|
1162
1236
|
return ignored;
|
|
1163
1237
|
}
|
|
1164
1238
|
function sbplEscape(path) {
|
|
@@ -1699,7 +1773,7 @@ function printDryRunTree({ home, blockedDirs, ignoredPaths, readOnlyDirs, workDi
|
|
|
1699
1773
|
}
|
|
1700
1774
|
//#endregion
|
|
1701
1775
|
//#region src/index.ts
|
|
1702
|
-
const VERSION = "1.
|
|
1776
|
+
const VERSION = "1.5.1";
|
|
1703
1777
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1704
1778
|
if (process$1.argv.includes("--version") || process$1.argv.includes("-v")) {
|
|
1705
1779
|
console.log(`bx ${VERSION}`);
|
|
@@ -1749,8 +1823,9 @@ async function main() {
|
|
|
1749
1823
|
if (profileSandbox) await setupVSCodeProfile(HOME, profileSandbox);
|
|
1750
1824
|
const { allowed, readOnly } = parseHomeConfig(HOME, workDirs);
|
|
1751
1825
|
const allAccessible = new Set([...allowed, ...readOnly]);
|
|
1826
|
+
warnDangerousOverrides(HOME, allAccessible);
|
|
1752
1827
|
const blockedDirs = collectBlockedDirs(HOME, HOME, __dirname, allAccessible);
|
|
1753
|
-
const ignoredPaths = collectIgnoredPaths(HOME, workDirs, allAccessible);
|
|
1828
|
+
const ignoredPaths = collectIgnoredPaths(HOME, workDirs, allAccessible, readOnly);
|
|
1754
1829
|
const readOnlyDotfiles = collectReadOnlyDotfiles(HOME, allAccessible);
|
|
1755
1830
|
printPolicySummary(mode, workDirs, blockedDirs, ignoredPaths, readOnly);
|
|
1756
1831
|
const profile = generateProfile(workDirs, blockedDirs, ignoredPaths, [...readOnly], HOME, readOnlyDotfiles);
|
|
@@ -1878,6 +1953,14 @@ function printPolicySummary(mode, workDirs, blockedDirs, ignoredPaths, readOnly)
|
|
|
1878
1953
|
if (extraIgnored > 0) parts.push(`${extraIgnored} from .bxignore`);
|
|
1879
1954
|
console.error(fmt.detail(parts.join(" ยท ")));
|
|
1880
1955
|
}
|
|
1956
|
+
function warnDangerousOverrides(home, accessible) {
|
|
1957
|
+
const hits = [
|
|
1958
|
+
...PROTECTED_DOTDIRS.map((d) => join(home, d)),
|
|
1959
|
+
...PROTECTED_HOME_DOTFILES.map((f) => join(home, f)),
|
|
1960
|
+
...PROTECTED_LIBRARY_DIRS.map((d) => join(home, "Library", d))
|
|
1961
|
+
].filter((p) => accessible.has(p));
|
|
1962
|
+
for (const p of hits) console.error(fmt.detail(`warning: ~/.bxignore override exposes built-in protected path ${p}`));
|
|
1963
|
+
}
|
|
1881
1964
|
function printLaunchDetails(cmd, cwd) {
|
|
1882
1965
|
const quote = (a) => JSON.stringify(a);
|
|
1883
1966
|
console.error(fmt.detail(`bin: ${cmd.bin}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bx-mac",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Sandbox any macOS app โ only your project directory stays accessible",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"darwin"
|
|
48
48
|
],
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@types/node": "^25.
|
|
51
|
-
"rolldown": "^1.0.0-rc.
|
|
50
|
+
"@types/node": "^25.6.0",
|
|
51
|
+
"rolldown": "^1.0.0-rc.16",
|
|
52
52
|
"smol-toml": "^1.6.1",
|
|
53
|
-
"vitest": "^4.1.
|
|
53
|
+
"vitest": "^4.1.4"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=22"
|