pi-landstrip 0.19.18 → 0.19.19

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 +5 -3
  2. package/dist/index.ts +33 -12
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -32,9 +32,11 @@ and platform limits, including Windows read grants and hard-denial restrictions.
32
32
 
33
33
  Worker boundaries:
34
34
 
35
- - Bootstrap grants override bundled `denyRead`, never global/trusted-project
36
- denies. Denied startup resources prevent launch. Legacy `/home` or `/Users`
37
- denies are explicit overrides; remove them only if unintended.
35
+ - Workers can read their startup files despite the bundled `denyRead` defaults.
36
+ If your own sandbox settings block a needed path with `denyRead`, Pi asks for
37
+ approval before starting the worker. Declining prevents startup. Existing
38
+ `allowRead` grants and session approvals are reused. `denyReadAlways` blocks
39
+ startup without asking.
38
40
  - Hard read denials also apply to shell host reads. Private workers protect
39
41
  parent auth-file/lock roots with `denyReadAlways` and `denyWrite`;
40
42
  private worker authentication is unsupported on Windows.
package/dist/index.ts CHANGED
@@ -5583,7 +5583,8 @@ function loadSandboxConfig(cwd, includeProject) {
5583
5583
  const globalConfig = deepMerge(JSON.parse(readFileSync7(join8(packageDir2, "sandbox.json"), "utf-8")), globalOverrides);
5584
5584
  return {
5585
5585
  ...deepMerge(globalConfig, projectOverrides),
5586
- explicitDenyRead: mergeArray(globalOverrides.filesystem?.denyRead ?? [], projectOverrides.filesystem?.denyRead)
5586
+ explicitDenyRead: mergeArray(globalOverrides.filesystem?.denyRead ?? [], projectOverrides.filesystem?.denyRead),
5587
+ explicitAllowRead: mergeArray(globalOverrides.filesystem?.allowRead ?? [], projectOverrides.filesystem?.allowRead)
5587
5588
  };
5588
5589
  }
5589
5590
  function windowsStatus() {
@@ -6135,9 +6136,13 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
6135
6136
  if (scope !== filePath)
6136
6137
  notify(ctx, `${verb} allowed (${choice}): ${scope}`, "info");
6137
6138
  }
6138
- async function applyReadChoice(ctx, choice, filePath, cwd, allowances) {
6139
+ async function applyReadChoice(ctx, choice, filePath, cwd, allowances, scope = sessionScopeFor(filePath, cwd)) {
6139
6140
  const { globalPath, projectPath } = getPiConfigPaths(cwd, "sandbox.json");
6140
- const scope = sessionScopeFor(filePath, cwd);
6141
+ if (allowances?.explicitDenyRead && !readAllowed(filePath, [scope], mergeArray(loadConfig(cwd).filesystem.denyRead, [
6142
+ ...allowances.explicitDenyRead.patterns,
6143
+ ...[...allowances.explicitDenyRead.globRoots.values()].flat()
6144
+ ]), cwd))
6145
+ scope = filePath;
6141
6146
  const configPath = choice === "project" ? projectPath : choice === "global" ? globalPath : undefined;
6142
6147
  if (configPath) {
6143
6148
  await addConfigValue(configPath, scope, (config) => config.filesystem?.allowRead ?? [], (config, allowRead) => {
@@ -6147,6 +6152,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
6147
6152
  const target = choice === "once" ? allowances?.readPaths : sessionAllowedReadPaths;
6148
6153
  if (target && !target.includes(scope))
6149
6154
  target.push(scope);
6155
+ allowances?.explicitDenyRead?.approvedPaths.push(scope);
6150
6156
  noteScope(ctx, "Read", choice, filePath, scope);
6151
6157
  }
6152
6158
  async function applyWriteChoice(ctx, choice, filePath, cwd, allowances) {
@@ -6291,8 +6297,8 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6291
6297
  });
6292
6298
  });
6293
6299
  }
6294
- async function resolveTrapQuery(trap, ctx, cwd, allowances, promptOnBlock, signal) {
6295
- if (!isFilesystemTrap(trap)) {
6300
+ async function resolveTrapQuery(trap, ctx, cwd, allowances, promptOnBlock, signal, readScope) {
6301
+ if (trap.kind !== "filesystem") {
6296
6302
  const key = `${trap.operation}\x00${trap.target}`;
6297
6303
  const current = () => sessionAllowedTargets.includes(key) || allowances.targets.includes(key) ? { action: "allow" } : undefined;
6298
6304
  const existing = current();
@@ -6320,7 +6326,13 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6320
6326
  return { action: "deny", reason: "hard-deny" };
6321
6327
  }
6322
6328
  if (trap.operation === "read" && allowances.explicitDenyRead && matchesPattern(path, mergeArray(allowances.explicitDenyRead.patterns, config.explicitDenyRead), cwd, allowances.explicitDenyRead.globRoots)) {
6323
- return { action: "deny", reason: "hard-deny" };
6329
+ return isPathReadAllowed(path, mergeArray(config.explicitAllowRead, [
6330
+ ...sessionAllowedReadPaths,
6331
+ ...allowances.explicitDenyRead.approvedPaths
6332
+ ]), mergeArray(config.explicitDenyRead, [
6333
+ ...allowances.explicitDenyRead.patterns,
6334
+ ...[...allowances.explicitDenyRead.globRoots.values()].flat()
6335
+ ]), cwd, { requireAllowMatch: true }) ? { action: "allow" } : undefined;
6324
6336
  }
6325
6337
  if (trap.operation === "write" && (matchesPattern(path, config.filesystem.denyWriteAlways, cwd) || matchesPattern(path, config.filesystem.denyWrite, cwd))) {
6326
6338
  return { action: "deny", reason: "hard-deny" };
@@ -6339,7 +6351,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6339
6351
  if (choice === "abort" || promptSignal.aborted)
6340
6352
  return { action: "deny", reason: "rejected" };
6341
6353
  if (trap.operation === "read") {
6342
- await applyReadChoice(ctx, choice, path, cwd, allowances);
6354
+ await applyReadChoice(ctx, choice, path, cwd, allowances, readScope);
6343
6355
  } else {
6344
6356
  await applyWriteChoice(ctx, choice, path, cwd, allowances);
6345
6357
  }
@@ -6461,15 +6473,24 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6461
6473
  const explicitDenyGlobRoots = new Map;
6462
6474
  processAllowances.explicitDenyRead = {
6463
6475
  patterns: config.explicitDenyRead,
6464
- globRoots: explicitDenyGlobRoots
6476
+ globRoots: explicitDenyGlobRoots,
6477
+ approvedPaths: []
6465
6478
  };
6466
- for (const path of [options.cwd, options.command, ...processAllowances.readPaths]) {
6479
+ const startupPaths = [
6480
+ ...new Set([options.cwd, options.command, ...processAllowances.readPaths])
6481
+ ];
6482
+ for (const path of startupPaths) {
6467
6483
  if (/[*?[\]]/.test(path)) {
6468
6484
  throw new Error(`Worker startup read path "${path}" must be a concrete path, not a glob`);
6469
6485
  }
6470
- const deny = config.explicitDenyRead.find((pattern) => matchesPattern(path, [pattern], options.cwd, explicitDenyGlobRoots));
6471
- if (deny !== undefined) {
6472
- throw new Error(`Worker startup read path "${path}" conflicts with explicit filesystem.denyRead "${deny}"; change the configured deny or the worker startup paths`);
6486
+ if (matchesPattern(path, mergeArray(config.filesystem.denyReadAlways, processAllowances.protectedPaths), options.cwd)) {
6487
+ throw new Error(`Worker startup read path "${path}" is denied by sandbox policy`);
6488
+ }
6489
+ }
6490
+ for (const path of startupPaths) {
6491
+ const result = await resolveTrapQuery({ kind: "filesystem", operation: "read", path }, options.ctx, options.cwd, processAllowances, true, options.signal, normalizeBlockedPath(path, options.cwd));
6492
+ if (result.action !== "allow") {
6493
+ throw new Error(`Worker startup read access denied: ${path} (${result.reason})`);
6473
6494
  }
6474
6495
  }
6475
6496
  const { token: proxyToken, authorization: proxyAuthorization } = createProxyCredentials();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.19.18",
3
+ "version": "0.19.19",
4
4
  "description": "Sandboxed Bash and process-backed agents for Pi",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -39,7 +39,7 @@
39
39
  "ci:test": "npm run test"
40
40
  },
41
41
  "dependencies": {
42
- "@landstrip/landstrip-api": "^0.19.18",
42
+ "@landstrip/landstrip-api": "^0.19.19",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "undici": "^8.9.0",