pi-landstrip 0.19.4 → 0.19.7

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 CHANGED
@@ -49,6 +49,9 @@ Worker read boundaries:
49
49
  or shell host reads. Private workers protect parent auth-file and lock roots
50
50
  with this policy and `denyWrite`. Non-empty `denyReadAlways` and private worker
51
51
  auth are unsupported on Windows.
52
+ - `filesystem.denyWriteAlways` cannot be overridden by equal/nested write grants
53
+ or interactive prompts; write attempts are silently rejected. Non-empty
54
+ `denyWriteAlways` is unsupported on Windows.
52
55
  - External skill, prompt, and theme directories may need explicit discovery
53
56
  access. Prefer resource file paths: resolved-file grants do not cover enclosing
54
57
  directories.
package/dist/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  // index.ts
2
2
  import { spawn as spawn2, spawnSync } from "node:child_process";
3
- import { randomBytes } from "node:crypto";
4
3
  import {
5
4
  existsSync as existsSync6,
6
5
  mkdirSync as mkdirSync4,
@@ -27,15 +26,22 @@ import {
27
26
  binaryPath
28
27
  } from "@landstrip/landstrip-api";
29
28
  import {
29
+ buildLandstripPolicy as assembleLandstripPolicy,
30
30
  allowsAllDomains,
31
31
  controlResponseLine,
32
+ deepMergeSandboxConfig as deepMerge,
32
33
  domainMatchesAny,
34
+ evaluateDomainAccess,
33
35
  formatLandstripTraps,
34
36
  isDenialTrap,
35
37
  isFilesystemTrap,
38
+ isPathReadAllowed,
36
39
  isRecord as isRecord3,
37
40
  isQueryTrap,
41
+ matchesPathPattern,
42
+ mergeArray,
38
43
  parseLandstripTraps,
44
+ parseSandboxConfig,
39
45
  canonicalizeGlobPattern,
40
46
  canonicalizePath as canonicalizePath2,
41
47
  expandPath,
@@ -43,9 +49,20 @@ import {
43
49
  normalizePathSeparators,
44
50
  parseTrapLine,
45
51
  pathUnderDirectory,
46
- sessionScopeFor
52
+ sessionScopeFor,
53
+ serializeLandstripPolicy,
54
+ shouldPromptForWrite as sharedShouldPromptForWrite,
55
+ normalizeBlockedPath,
56
+ extractNativeDeniedPath,
57
+ extractNativeWriteDeniedPath,
58
+ extractDeniedPath,
59
+ extractTrapBlockedPath
47
60
  } from "@landstrip/landstrip-api/shared";
48
- import { startFilterProxy } from "@landstrip/landstrip-api/proxy";
61
+ import {
62
+ createProxyCredentials,
63
+ createProxyEnvironment,
64
+ startFilterProxy
65
+ } from "@landstrip/landstrip-api/proxy";
49
66
 
50
67
  // api.ts
51
68
  import { isRecord } from "@landstrip/landstrip-api/shared";
@@ -5519,72 +5536,6 @@ function registerSubagents(pi, integration, permissionPrompts = new PermissionPr
5519
5536
  }
5520
5537
 
5521
5538
  // index.ts
5522
- function requireSandboxObject(value, field) {
5523
- if (!isRecord3(value))
5524
- throw new Error(`${field} must be an object`);
5525
- }
5526
- function rejectUnknownSandboxFields(value, fields, prefix = "") {
5527
- for (const field of Object.keys(value)) {
5528
- if (!fields.includes(field))
5529
- throw new Error(`unknown sandbox field ${prefix}${field}`);
5530
- }
5531
- }
5532
- function validateBooleanFields(value, fields, prefix = "") {
5533
- for (const field of fields) {
5534
- if (value[field] !== undefined && typeof value[field] !== "boolean") {
5535
- throw new Error(`${prefix}${field} must be a boolean`);
5536
- }
5537
- }
5538
- }
5539
- function validateStringArrayFields(value, fields, prefix) {
5540
- for (const field of fields) {
5541
- const entry = value[field];
5542
- if (entry === undefined)
5543
- continue;
5544
- if (!Array.isArray(entry) || [...entry].some((item) => typeof item !== "string")) {
5545
- throw new Error(`${prefix}${field} must be an array of strings`);
5546
- }
5547
- }
5548
- }
5549
- function parseSandboxConfig(value) {
5550
- requireSandboxObject(value, "sandbox config");
5551
- rejectUnknownSandboxFields(value, ["enabled", "shell", "network", "filesystem", "windows"]);
5552
- validateBooleanFields(value, ["enabled"]);
5553
- if (value.shell !== undefined) {
5554
- requireSandboxObject(value.shell, "shell");
5555
- rejectUnknownSandboxFields(value.shell, ["readAccess"], "shell.");
5556
- if (value.shell.readAccess !== undefined && value.shell.readAccess !== "host" && value.shell.readAccess !== "policy") {
5557
- throw new Error("shell.readAccess must be host or policy");
5558
- }
5559
- }
5560
- if (value.network !== undefined) {
5561
- requireSandboxObject(value.network, "network");
5562
- rejectUnknownSandboxFields(value.network, [
5563
- "allowNetwork",
5564
- "allowLocalBinding",
5565
- "allowAllUnixSockets",
5566
- "allowUnixSockets",
5567
- "allowedDomains",
5568
- "deniedDomains"
5569
- ], "network.");
5570
- validateBooleanFields(value.network, ["allowNetwork", "allowLocalBinding", "allowAllUnixSockets"], "network.");
5571
- validateStringArrayFields(value.network, ["allowUnixSockets", "allowedDomains", "deniedDomains"], "network.");
5572
- }
5573
- if (value.filesystem !== undefined) {
5574
- requireSandboxObject(value.filesystem, "filesystem");
5575
- rejectUnknownSandboxFields(value.filesystem, ["denyRead", "denyReadAlways", "allowRead", "allowWrite", "denyWrite"], "filesystem.");
5576
- validateStringArrayFields(value.filesystem, ["denyRead", "denyReadAlways", "allowRead", "allowWrite", "denyWrite"], "filesystem.");
5577
- }
5578
- if (value.windows !== undefined) {
5579
- requireSandboxObject(value.windows, "windows");
5580
- rejectUnknownSandboxFields(value.windows, ["appContainerMode", "allowLoopback"], "windows.");
5581
- validateBooleanFields(value.windows, ["allowLoopback"], "windows.");
5582
- if (value.windows.appContainerMode !== undefined && value.windows.appContainerMode !== "lpac" && value.windows.appContainerMode !== "standard") {
5583
- throw new Error("windows.appContainerMode must be lpac or standard");
5584
- }
5585
- }
5586
- return value;
5587
- }
5588
5539
  function enforcesShellReadPolicy(shellReadAccess, platform = process.platform) {
5589
5540
  return shellReadAccess !== "host" || platform === "win32";
5590
5541
  }
@@ -5596,14 +5547,6 @@ function resolveProcessReadPolicy(audience, shellReadAccess, denyRead, allowRead
5596
5547
  }
5597
5548
  var SUPPORTED_PLATFORMS = new Set(["linux", "darwin", "win32"]);
5598
5549
  var EXIT_STDIO_GRACE_MS = 100;
5599
- var PROXY_ENVIRONMENT_VARIABLES = [
5600
- "HTTP_PROXY",
5601
- "HTTPS_PROXY",
5602
- "ALL_PROXY",
5603
- "http_proxy",
5604
- "https_proxy",
5605
- "all_proxy"
5606
- ];
5607
5550
  var packageDir2 = dirname5(fileURLToPath2(import.meta.url));
5608
5551
  var PERMISSION_OPTIONS = [
5609
5552
  { label: "Allow once", action: "once" },
@@ -5643,42 +5586,6 @@ function loadSandboxConfig(cwd, includeProject) {
5643
5586
  explicitDenyRead: mergeArray(globalOverrides.filesystem?.denyRead ?? [], projectOverrides.filesystem?.denyRead)
5644
5587
  };
5645
5588
  }
5646
- function mergeArray(base, override) {
5647
- if (!override)
5648
- return base;
5649
- return [...new Set([...base, ...override])];
5650
- }
5651
- function deepMerge(base, overrides) {
5652
- const shell = overrides.shell;
5653
- const network = overrides.network;
5654
- const filesystem = overrides.filesystem;
5655
- const windows = overrides.windows;
5656
- return {
5657
- enabled: overrides.enabled ?? base.enabled,
5658
- shell: {
5659
- readAccess: shell?.readAccess ?? base.shell.readAccess
5660
- },
5661
- network: {
5662
- allowNetwork: network?.allowNetwork ?? base.network.allowNetwork,
5663
- allowLocalBinding: network?.allowLocalBinding ?? base.network.allowLocalBinding,
5664
- allowAllUnixSockets: network?.allowAllUnixSockets ?? base.network.allowAllUnixSockets,
5665
- allowUnixSockets: mergeArray(base.network.allowUnixSockets, network?.allowUnixSockets),
5666
- allowedDomains: mergeArray(base.network.allowedDomains, network?.allowedDomains),
5667
- deniedDomains: mergeArray(base.network.deniedDomains, network?.deniedDomains)
5668
- },
5669
- filesystem: {
5670
- denyRead: mergeArray(base.filesystem.denyRead, filesystem?.denyRead),
5671
- denyReadAlways: mergeArray(base.filesystem.denyReadAlways, filesystem?.denyReadAlways),
5672
- allowRead: mergeArray(base.filesystem.allowRead, filesystem?.allowRead),
5673
- allowWrite: mergeArray(base.filesystem.allowWrite, filesystem?.allowWrite),
5674
- denyWrite: mergeArray(base.filesystem.denyWrite, filesystem?.denyWrite)
5675
- },
5676
- windows: {
5677
- appContainerMode: windows?.appContainerMode ?? base.windows.appContainerMode,
5678
- allowLoopback: windows?.allowLoopback ?? base.windows.allowLoopback
5679
- }
5680
- };
5681
- }
5682
5589
  function windowsStatus() {
5683
5590
  const result = spawnSync(binaryPath(), ["windows", "status"], {
5684
5591
  encoding: "utf-8",
@@ -5765,9 +5672,12 @@ function mergeAllowances(base, session, execution) {
5765
5672
  return [...base, ...session, ...execution ?? []];
5766
5673
  }
5767
5674
  function shouldPromptForWrite(path, allowWrite, cwd) {
5768
- return allowWrite.length === 0 || !matchesPattern(path, allowWrite, cwd);
5675
+ return sharedShouldPromptForWrite(path, allowWrite, cwd);
5769
5676
  }
5770
5677
  function matchesPattern(filePath, patterns, cwd, explicitDenyGlobRoots) {
5678
+ if (!explicitDenyGlobRoots) {
5679
+ return matchesPathPattern(filePath, patterns, cwd);
5680
+ }
5771
5681
  const abs = normalizePathSeparators(canonicalizePath2(filePath, cwd));
5772
5682
  return patterns.some((pattern) => {
5773
5683
  const absPattern = normalizePathSeparators(canonicalizeGlobPattern(pattern, cwd));
@@ -5823,53 +5733,8 @@ function matchesPattern(filePath, patterns, cwd, explicitDenyGlobRoots) {
5823
5733
  return abs === absPattern || abs.startsWith(absPattern + sep);
5824
5734
  });
5825
5735
  }
5826
- function normalizeBlockedPath(path, cwd) {
5827
- const nativePath = process.platform === "win32" && /^\/[a-zA-Z](?:\/|$)/.test(path) ? `${path[1]}:${path.slice(2)}` : path;
5828
- return canonicalizePath2(isAbsolute4(nativePath) ? nativePath : join8(cwd, nativePath), cwd);
5829
- }
5830
- function longestPrefixMatch(path, patterns, cwd) {
5831
- let best = -1;
5832
- for (const pattern of patterns) {
5833
- if (!matchesPattern(path, [pattern], cwd))
5834
- continue;
5835
- const canonical = canonicalizeGlobPattern(pattern, cwd);
5836
- if (canonical.length > best)
5837
- best = canonical.length;
5838
- }
5839
- return best;
5840
- }
5841
5736
  function readAllowed(path, allowRead, denyRead, cwd) {
5842
- const deny = longestPrefixMatch(path, denyRead, cwd);
5843
- if (deny < 0)
5844
- return true;
5845
- return longestPrefixMatch(path, allowRead, cwd) >= deny;
5846
- }
5847
- function isPathLike(value) {
5848
- const trimmed = value.trim();
5849
- return trimmed === "~" || trimmed.startsWith("/") || trimmed.startsWith("\\\\") || /^[a-zA-Z]:[\\/]/.test(trimmed) || trimmed.startsWith("~/") || trimmed.startsWith("~\\") || trimmed.startsWith("./") || trimmed.startsWith(".\\") || trimmed.startsWith("../") || trimmed.startsWith("..\\") || trimmed.startsWith(".") || trimmed.includes("/") || trimmed.includes("\\");
5850
- }
5851
- function normalizePathMatch(value, cwd) {
5852
- return isPathLike(value) ? normalizeBlockedPath(value, cwd) : null;
5853
- }
5854
- function extractBlockedPath(trapOutput, cwd) {
5855
- const landstripErrors = parseLandstripTraps(trapOutput).filter(isFilesystemTrap);
5856
- if (landstripErrors.length > 0) {
5857
- return normalizeBlockedPath(landstripErrors[0].path, cwd);
5858
- }
5859
- return null;
5860
- }
5861
- function extractNativeDeniedPath(output, cwd) {
5862
- const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
5863
- let match = output.match(new RegExp(String.raw`['"]([^'"\n]+)['"][^\r\n]{0,120}${denial}`, "i"));
5864
- if (match)
5865
- return normalizePathMatch(match[1], cwd);
5866
- match = output.match(new RegExp(String.raw`(?:^|:\s+)((?:[a-zA-Z]:[\\/]|\\\\|/|\.{1,2}[\\/])[^\r\n]*?):\s+${denial}$`, "im"));
5867
- if (match)
5868
- return normalizePathMatch(match[1], cwd);
5869
- match = output.match(new RegExp(String.raw`^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: ${denial}$`, "im"));
5870
- if (match)
5871
- return normalizePathMatch(match[1], cwd);
5872
- return null;
5737
+ return isPathReadAllowed(path, allowRead, denyRead, cwd);
5873
5738
  }
5874
5739
  function extractDeniedPathMention(output, cwd, denied) {
5875
5740
  const start = /(?<=^|['"`\s=:(])(?=\/|[a-zA-Z]:[\\/])/g;
@@ -5898,27 +5763,6 @@ function extractRetryableNativeReadDeniedPath(output, cwd, allowRead, denyRead)
5898
5763
  return denied(path) ? path : null;
5899
5764
  return extractDeniedPathMention(output, cwd, denied);
5900
5765
  }
5901
- function extractNativeWriteDeniedPath(output, cwd) {
5902
- const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
5903
- let match = output.match(new RegExp(String.raw`(?:[Uu]nable to create|failed to create(?: directory)?|cannot (?:create|touch|mkdir|remove|unlink|rename)|for writing)[^'"\x60\r\n]*['"\x60]([^'"\x60\r\n]+)['"\x60](?:[^\r\n]*\r?\n){0,3}[^\r\n]{0,120}${denial}`, "im"));
5904
- if (match)
5905
- return normalizePathMatch(match[1], cwd);
5906
- match = output.match(new RegExp(String.raw`^[a-zA-Z0-9_-]+: cannot create(?: directory)? '?([^'\n]+?)'?(?: for writing)?: ${denial}$`, "im"));
5907
- if (match)
5908
- return normalizePathMatch(match[1], cwd);
5909
- match = output.match(new RegExp(String.raw`^[a-zA-Z0-9_-]+: couldn't open temporary file ((?:[a-zA-Z]:[\\/]|\\\\|/)[^\r\n]*?): ${denial}$`, "im"));
5910
- if (match)
5911
- return normalizePathMatch(match[1], cwd);
5912
- return null;
5913
- }
5914
- function extractTrapBlockedPath(trapOutput, cwd, operation) {
5915
- for (const error of parseLandstripTraps(trapOutput).filter(isFilesystemTrap)) {
5916
- if (error.operation === operation) {
5917
- return normalizeBlockedPath(error.path, cwd);
5918
- }
5919
- }
5920
- return null;
5921
- }
5922
5766
  function notify(ctx, message, level) {
5923
5767
  if (!ctx.hasUI)
5924
5768
  return;
@@ -6011,13 +5855,10 @@ function writeEnvFile(env, proxyPort, proxyToken) {
6011
5855
  lines.push(`export ${key}='${escaped}'`);
6012
5856
  }
6013
5857
  if (proxyPort !== null) {
6014
- const credentials = proxyToken === undefined ? "" : `landstrip:${proxyToken}@`;
6015
- const url = `http://${credentials}127.0.0.1:${proxyPort}`;
6016
- for (const name of PROXY_ENVIRONMENT_VARIABLES) {
6017
- lines.push(`export ${name}='${url}'`);
5858
+ const proxyVars = createProxyEnvironment(proxyPort, proxyToken);
5859
+ for (const [name, value] of Object.entries(proxyVars)) {
5860
+ lines.push(`export ${name}='${value}'`);
6018
5861
  }
6019
- lines.push("export NO_PROXY=''");
6020
- lines.push("export no_proxy=''");
6021
5862
  }
6022
5863
  const dir = mkdtempSync2(join8(tmpdir2(), "pi-landstrip-env-"));
6023
5864
  const path = join8(dir, "env.sh");
@@ -6052,15 +5893,7 @@ function createPosixShellProvider() {
6052
5893
  }
6053
5894
  function shellEnvironment(env, cwd, proxyPort, proxyToken) {
6054
5895
  const result = { ...process.env, ...env, PWD: resolve4(cwd) };
6055
- if (proxyPort === null)
6056
- return result;
6057
- const credentials = proxyToken === undefined ? "" : `landstrip:${proxyToken}@`;
6058
- const url = `http://${credentials}127.0.0.1:${proxyPort}`;
6059
- for (const name of PROXY_ENVIRONMENT_VARIABLES)
6060
- result[name] = url;
6061
- result.NO_PROXY = "";
6062
- result.no_proxy = "";
6063
- return result;
5896
+ return createProxyEnvironment(proxyPort, proxyToken, result);
6064
5897
  }
6065
5898
  function createLandstripLauncherEnvironment(providerEnv, hostEnv = process.env, platform = process.platform) {
6066
5899
  if (platform !== "win32" || hostEnv.ProgramData === undefined)
@@ -6324,7 +6157,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
6324
6157
  const current = () => {
6325
6158
  const config = loadConfig(ctx.cwd);
6326
6159
  for (const access of normalized) {
6327
- if (matchesPattern(access.path, access.operation === "read" ? config.filesystem.denyReadAlways : config.filesystem.denyWrite, ctx.cwd)) {
6160
+ if (matchesPattern(access.path, access.operation === "read" ? config.filesystem.denyReadAlways : mergeArray(config.filesystem.denyWriteAlways, config.filesystem.denyWrite), ctx.cwd)) {
6328
6161
  return {
6329
6162
  allowed: false,
6330
6163
  prompted: false,
@@ -6367,10 +6200,13 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6367
6200
  async function ensureDomainAllowed(ctx, domain, cwd, allowances, signal) {
6368
6201
  const current = () => {
6369
6202
  const config = loadConfig(cwd);
6370
- if (domainMatchesAny(domain, config.network.deniedDomains))
6203
+ const decision = evaluateDomainAccess(domain, config.network, [
6204
+ ...sessionAllowedDomains,
6205
+ ...allowances?.domains ?? []
6206
+ ]);
6207
+ if (decision === "deny")
6371
6208
  return false;
6372
- const allowedDomains = mergeAllowances(config.network.allowedDomains, sessionAllowedDomains, allowances?.domains);
6373
- if (domainMatchesAny(domain, allowedDomains))
6209
+ if (decision === "allow")
6374
6210
  return true;
6375
6211
  return;
6376
6212
  };
@@ -6390,28 +6226,23 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6390
6226
  ...allowances?.explicitDenyRead?.patterns ?? [],
6391
6227
  ...[...allowances?.explicitDenyRead?.globRoots.values() ?? []].flat()
6392
6228
  ]), getEffectiveAllowRead(config, cwd, allowances));
6393
- return {
6394
- network: {
6395
- allowNetwork: config.network.allowNetwork,
6396
- allowLocalBinding: config.network.allowLocalBinding,
6397
- allowAllUnixSockets: config.network.allowAllUnixSockets,
6398
- allowUnixSockets: config.network.allowUnixSockets,
6399
- ...proxyPort !== null ? { httpProxyPort: proxyPort } : {}
6400
- },
6229
+ return assembleLandstripPolicy({
6230
+ network: config.network,
6401
6231
  filesystem: {
6402
6232
  ...readPolicy,
6403
6233
  denyReadAlways: mergeArray(config.filesystem.denyReadAlways, allowances?.protectedPaths),
6404
6234
  allowWrite: getEffectiveAllowWrite(config, allowances),
6405
- denyWrite: mergeArray(config.filesystem.denyWrite, allowances?.protectedPaths)
6235
+ denyWrite: mergeArray(config.filesystem.denyWrite, allowances?.protectedPaths),
6236
+ denyWriteAlways: mergeArray(config.filesystem.denyWriteAlways, allowances?.protectedPaths)
6406
6237
  },
6407
- windows: config.windows
6408
- };
6238
+ windows: config.windows,
6239
+ httpProxyPort: proxyPort
6240
+ });
6409
6241
  }
6410
6242
  function writePolicyFile(cwd, proxyPort, audience, allowances) {
6411
6243
  const dir = mkdtempSync2(join8(tmpdir2(), "pi-landstrip-"));
6412
6244
  const path = join8(dir, "policy.json");
6413
- writeFileSync4(path, JSON.stringify(buildLandstripPolicy(cwd, proxyPort, audience, allowances), null, 2) + `
6414
- `, "utf-8");
6245
+ writeFileSync4(path, serializeLandstripPolicy(buildLandstripPolicy(cwd, proxyPort, audience, allowances)), "utf-8");
6415
6246
  return { dir, path };
6416
6247
  }
6417
6248
  function startProxy(ctx, cwd, allowances, signal, authorization, portRange) {
@@ -6478,7 +6309,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6478
6309
  if (trap.operation === "read" && allowances.explicitDenyRead && matchesPattern(path, mergeArray(allowances.explicitDenyRead.patterns, config.explicitDenyRead), cwd, allowances.explicitDenyRead.globRoots)) {
6479
6310
  return { action: "deny", reason: "hard-deny" };
6480
6311
  }
6481
- if (trap.operation === "write" && matchesPattern(path, config.filesystem.denyWrite, cwd)) {
6312
+ if (trap.operation === "write" && (matchesPattern(path, config.filesystem.denyWriteAlways, cwd) || matchesPattern(path, config.filesystem.denyWrite, cwd))) {
6482
6313
  return { action: "deny", reason: "hard-deny" };
6483
6314
  }
6484
6315
  const allowed = trap.operation === "read" ? readAllowed(path, getEffectiveAllowRead(config, cwd, allowances), getEffectiveDenyRead(config, cwd), cwd) : !shouldPromptForWrite(path, getEffectiveAllowWrite(config, allowances), cwd);
@@ -6628,8 +6459,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6628
6459
  throw new Error(`Worker startup read path "${path}" conflicts with explicit filesystem.denyRead "${deny}"; change the configured deny or the worker startup paths`);
6629
6460
  }
6630
6461
  }
6631
- const proxyToken = randomBytes(32).toString("base64url");
6632
- const proxyAuthorization = `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString("base64")}`;
6462
+ const { token: proxyToken, authorization: proxyAuthorization } = createProxyCredentials();
6633
6463
  const proxy = shouldStartProxy(config) ? await startProxy(options.ctx, options.cwd, processAllowances, options.signal, proxyAuthorization, restrictedUserProxyPortRange()) : null;
6634
6464
  let policy;
6635
6465
  let trapSocket;
@@ -6656,15 +6486,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6656
6486
  const workerPolicy = policy;
6657
6487
  const workerTrapSocket = trapSocket;
6658
6488
  const workerChildEnd = childEnd;
6659
- const workerEnv = { ...options.env };
6660
- if (proxy) {
6661
- const url = `http://landstrip:${proxyToken}@127.0.0.1:${proxy.port}`;
6662
- for (const name of PROXY_ENVIRONMENT_VARIABLES) {
6663
- workerEnv[name] = url;
6664
- }
6665
- workerEnv.NO_PROXY = "";
6666
- workerEnv.no_proxy = "";
6667
- }
6489
+ const workerEnv = createProxyEnvironment(proxy?.port ?? null, proxyToken, options.env ?? {});
6668
6490
  let spawned = false;
6669
6491
  let disposePromise;
6670
6492
  let preparedChild;
@@ -6856,8 +6678,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6856
6678
  throw new Error(`Working directory does not exist: ${cwd}`);
6857
6679
  const provider = activeShellProvider();
6858
6680
  const config = loadConfig(cwd);
6859
- const proxyToken = randomBytes(32).toString("base64url");
6860
- const proxyAuthorization = `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString("base64")}`;
6681
+ const { token: proxyToken, authorization: proxyAuthorization } = createProxyCredentials();
6861
6682
  const proxy = shouldStartProxy(config) ? await startProxy(ctx, cwd, allowances, signal, proxyAuthorization, restrictedUserProxyPortRange()) : null;
6862
6683
  let policy;
6863
6684
  let invocation;
@@ -6984,7 +6805,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6984
6805
  reject(new Error(`timeout:${timeout}`));
6985
6806
  return;
6986
6807
  }
6987
- const blockedPath = extractBlockedPath(errorFdAcc, cwd) ?? extractNativeDeniedPath(stderrAcc, cwd);
6808
+ const blockedPath = extractTrapBlockedPath(errorFdAcc, cwd) ?? extractNativeDeniedPath(stderrAcc, cwd);
6988
6809
  if (!blockedPath && ctx.hasUI) {
6989
6810
  const traps = parseLandstripTraps(errorFdAcc);
6990
6811
  const denials = traps.filter(isDenialTrap);
@@ -7127,7 +6948,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
7127
6948
  const { globalPath, projectPath } = getPiConfigPaths(ctx.cwd, "sandbox.json");
7128
6949
  const current = () => {
7129
6950
  const config = loadConfig(ctx.cwd);
7130
- if (matchesPattern(blockedPath, operation === "read" ? config.filesystem.denyReadAlways : config.filesystem.denyWrite, ctx.cwd)) {
6951
+ if (matchesPattern(blockedPath, operation === "read" ? config.filesystem.denyReadAlways : mergeArray(config.filesystem.denyWriteAlways, config.filesystem.denyWrite), ctx.cwd)) {
7131
6952
  return false;
7132
6953
  }
7133
6954
  const needsPrompt = operation === "read" ? !readAllowed(blockedPath, getEffectiveAllowRead(config, ctx.cwd, allowances), getEffectiveDenyRead(config, ctx.cwd), ctx.cwd) : shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config, allowances), ctx.cwd);
@@ -7147,16 +6968,18 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
7147
6968
  }, signal);
7148
6969
  if (!access) {
7149
6970
  const config = loadConfig(ctx.cwd);
7150
- if (operation === "write" && matchesPattern(blockedPath, config.filesystem.denyWrite, ctx.cwd)) {
7151
- notify(ctx, `"${blockedPath}" is blocked by denyWrite. Check:
6971
+ if (operation === "write" && (matchesPattern(blockedPath, config.filesystem.denyWriteAlways, ctx.cwd) || matchesPattern(blockedPath, config.filesystem.denyWrite, ctx.cwd))) {
6972
+ const tier = matchesPattern(blockedPath, config.filesystem.denyWriteAlways, ctx.cwd) ? "denyWriteAlways" : "denyWrite";
6973
+ notify(ctx, `"${blockedPath}" is blocked by ${tier}. Check:
7152
6974
  ${projectPath}
7153
6975
  ${globalPath}`, "warning");
7154
6976
  }
7155
6977
  return null;
7156
6978
  }
7157
6979
  let config = loadConfig(ctx.cwd);
7158
- if (operation === "write" && matchesPattern(blockedPath, config.filesystem.denyWrite, ctx.cwd)) {
7159
- notify(ctx, `"${blockedPath}" was added to allowWrite, but denyWrite still blocks it. Check:
6980
+ if (operation === "write" && (matchesPattern(blockedPath, config.filesystem.denyWriteAlways, ctx.cwd) || matchesPattern(blockedPath, config.filesystem.denyWrite, ctx.cwd))) {
6981
+ const tier = matchesPattern(blockedPath, config.filesystem.denyWriteAlways, ctx.cwd) ? "denyWriteAlways" : "denyWrite";
6982
+ notify(ctx, `"${blockedPath}" was added to allowWrite, but ${tier} still blocks it. Check:
7160
6983
  ${projectPath}
7161
6984
  ${globalPath}`, "warning");
7162
6985
  return null;
@@ -7480,6 +7303,7 @@ export {
7480
7303
  pi_landstrip_default as default,
7481
7304
  domainMatchesAny,
7482
7305
  enforcesShellReadPolicy,
7306
+ extractDeniedPath,
7483
7307
  extractNativeDeniedPath,
7484
7308
  extractNativeWriteDeniedPath,
7485
7309
  extractRetryableNativeReadDeniedPath,
package/dist/sandbox.json CHANGED
@@ -23,7 +23,8 @@
23
23
  "**/*.key",
24
24
  ".pi/sandbox.json",
25
25
  "~/.pi/agent/sandbox.json"
26
- ]
26
+ ],
27
+ "denyWriteAlways": []
27
28
  },
28
29
  "windows": {
29
30
  "appContainerMode": "standard",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.19.4",
3
+ "version": "0.19.7",
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.4",
42
+ "@landstrip/landstrip-api": "^0.19.7",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "undici": "^8.9.0",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@earendil-works/pi-ai": "^0.85.1",
50
- "@earendil-works/pi-coding-agent": "^0.85.1",
50
+ "@earendil-works/pi-coding-agent": "^0.87.1",
51
51
  "@earendil-works/pi-tui": "^0.85.1",
52
52
  "@types/node": "^26.4.1",
53
53
  "oxfmt": "^0.67.0",
package/sandbox.json CHANGED
@@ -23,7 +23,8 @@
23
23
  "**/*.key",
24
24
  ".pi/sandbox.json",
25
25
  "~/.pi/agent/sandbox.json"
26
- ]
26
+ ],
27
+ "denyWriteAlways": []
27
28
  },
28
29
  "windows": {
29
30
  "appContainerMode": "standard",