local-operator-ui 0.19.3 → 0.19.4

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 (2) hide show
  1. package/out/main/index.js +95 -3
  2. package/package.json +1 -1
package/out/main/index.js CHANGED
@@ -1336,6 +1336,81 @@ function withPythonBytecodeCache(env, userDataDir) {
1336
1336
  if (existing && !insideAppBundle(existing)) return { ...env };
1337
1337
  return { ...env, PYTHONPYCACHEPREFIX: pythonBytecodeCacheDir(userDataDir) };
1338
1338
  }
1339
+ const BUNDLED_PYTHON_TREE_NAMES = ["python", "python_aarch64"];
1340
+ function bundledPythonTreePaths(resourcesPath) {
1341
+ return BUNDLED_PYTHON_TREE_NAMES.map((name) => path.join(resourcesPath, name));
1342
+ }
1343
+ const FIND = "/usr/bin/find";
1344
+ const CHMOD = "/bin/chmod";
1345
+ const DIRECTORY_ACE = "everyone deny add_file,add_subdirectory";
1346
+ const BYTECODE_ACE = "everyone deny write,append";
1347
+ const MAX_FIND_OUTPUT_BYTES = 16 * 1024 * 1024;
1348
+ const PARTIAL_SEAL_NAMED = 3;
1349
+ function describePythonTreeSeal(seal) {
1350
+ if (!seal.supported) {
1351
+ return "Bundled interpreter bytecode seal is macOS-only (there is no code seal to protect elsewhere); the bytecode cache prefix still applies";
1352
+ }
1353
+ const trees = seal.sealed.join(", ") || "(none present)";
1354
+ if (seal.failures.length === 0) {
1355
+ return `Bundled interpreter trees sealed against bytecode writes: ${trees}; ${seal.directories} directory path(s) selected and now refusing new entries, ${seal.bytecodeFiles} bytecode file(s) selected and now refusing rewrites`;
1356
+ }
1357
+ const named = seal.failures.slice(0, PARTIAL_SEAL_NAMED).join("; ");
1358
+ const rest = seal.failures.length - PARTIAL_SEAL_NAMED;
1359
+ return `Bundled interpreter bytecode seal INCOMPLETE: ${trees}; ${seal.directories} directory path(s) and ${seal.bytecodeFiles} bytecode file(s) selected, ${seal.failures.length} refused and left with the access they had - bytecode writes into the bundle are not prevented on every path. First refusals: ${named}${rest > 0 ? ` (+${rest} more)` : ""}`;
1360
+ }
1361
+ function sealPythonInterpreterTrees(trees) {
1362
+ const result = {
1363
+ sealed: [],
1364
+ directories: 0,
1365
+ bytecodeFiles: 0,
1366
+ failures: [],
1367
+ supported: process.platform === "darwin"
1368
+ };
1369
+ if (!result.supported) return result;
1370
+ for (const tree of trees) {
1371
+ let root;
1372
+ try {
1373
+ root = fs.lstatSync(tree);
1374
+ } catch {
1375
+ continue;
1376
+ }
1377
+ if (!root.isDirectory()) continue;
1378
+ result.sealed.push(tree);
1379
+ result.directories += applyAce(
1380
+ tree,
1381
+ ["-type", "d"],
1382
+ DIRECTORY_ACE,
1383
+ result.failures
1384
+ );
1385
+ result.bytecodeFiles += applyAce(
1386
+ tree,
1387
+ ["-type", "f", "-name", "*.pyc"],
1388
+ BYTECODE_ACE,
1389
+ result.failures
1390
+ );
1391
+ }
1392
+ return result;
1393
+ }
1394
+ function applyAce(tree, selector, ace, failures) {
1395
+ const run = node_child_process.spawnSync(
1396
+ FIND,
1397
+ [tree, ...selector, "-exec", CHMOD, "+a", ace, "{}", "+", "-print0"],
1398
+ { encoding: "buffer", maxBuffer: MAX_FIND_OUTPUT_BYTES }
1399
+ );
1400
+ if (run.error) {
1401
+ failures.push(`${tree}: ${String(run.error)}`);
1402
+ return 0;
1403
+ }
1404
+ for (const line of String(run.stderr).split("\n")) {
1405
+ if (line.trim().length > 0) failures.push(line.trim());
1406
+ }
1407
+ if (run.status !== 0 && String(run.stderr).trim().length === 0) {
1408
+ failures.push(`${tree}: ${FIND} exited ${run.status} with no explanation`);
1409
+ }
1410
+ let selected = 0;
1411
+ for (const byte of run.stdout) if (byte === 0) selected += 1;
1412
+ return selected;
1413
+ }
1339
1414
  var main = { exports: {} };
1340
1415
  const version = "16.5.0";
1341
1416
  const require$$4 = {
@@ -2878,9 +2953,15 @@ LOG_FILE="$APP_DATA_DIR/backend-install.log"
2878
2953
  # in-place update with -67028 errSecCSBadBundleFormat. A file codesign reports
2879
2954
  # as *added* can be deleted and the seal comes back; a *modified* one cannot,
2880
2955
  # which is why nothing may ship a .pyc at all. The app sets this variable when
2881
- # it spawns us; defaulting it here means a standalone run of this script cannot
2882
- # pollute the bundle either. $HOME is the right place for the same reason the
2883
- # venv lives there: it is never inside the thing that gets signed and swapped.
2956
+ # it spawns us; defaulting it here keeps a standalone run of this script in
2957
+ # agreement with the app about where bytecode goes. It is not a complete answer:
2958
+ # the app does not spawn every python that runs this interpreter, and a python
2959
+ # started by something else - a shell, a CLI script, an agent - has no prefix at
2960
+ # all (measured: a venv over this tree wrote 25 .pyc into it that way). The half
2961
+ # that covers those is the app's seal on the tree itself
2962
+ # (src/main/python-bytecode-cache.ts), which a standalone run of this script does
2963
+ # not apply. $HOME is the right place for the cache for the same reason the venv
2964
+ # lives there: it is never inside the thing that gets signed and swapped.
2884
2965
  : "\${PYTHONPYCACHEPREFIX:=$APP_DATA_DIR/python-bytecode-cache}"
2885
2966
  export PYTHONPYCACHEPREFIX
2886
2967
 
@@ -3487,6 +3568,17 @@ class BackendInstaller {
3487
3568
  );
3488
3569
  }
3489
3570
  this.resourcesPath = process.env.NODE_ENV === "development" ? path.join(process.cwd(), "resources") : path.join(process.resourcesPath);
3571
+ if (electron.app.isPackaged) {
3572
+ const seal = sealPythonInterpreterTrees(
3573
+ bundledPythonTreePaths(process.resourcesPath)
3574
+ );
3575
+ const message = describePythonTreeSeal(seal);
3576
+ if (seal.supported && seal.failures.length > 0) {
3577
+ logger.warn(message, LogFileType.INSTALLER);
3578
+ } else {
3579
+ logger.info(message, LogFileType.INSTALLER);
3580
+ }
3581
+ }
3490
3582
  this.pythonPath = this.findPython();
3491
3583
  logger.info(
3492
3584
  `Backend Installer initialized. Virtual environment path: ${this.venvPath}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-operator-ui",
3
- "version": "0.19.3",
3
+ "version": "0.19.4",
4
4
  "productName": "Local Operator",
5
5
  "description": "User interface for AI agent assistants that run Python code safely on your device through an intuitive chat interface. Supports local and cloud LLM models with built-in security features.",
6
6
  "main": "./out/main/index.js",