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 +3 -0
- package/dist/index.ts +57 -233
- package/dist/sandbox.json +2 -1
- package/package.json +3 -3
- package/sandbox.json +2 -1
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
6015
|
-
const
|
|
6016
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6203
|
+
const decision = evaluateDomainAccess(domain, config.network, [
|
|
6204
|
+
...sessionAllowedDomains,
|
|
6205
|
+
...allowances?.domains ?? []
|
|
6206
|
+
]);
|
|
6207
|
+
if (decision === "deny")
|
|
6371
6208
|
return false;
|
|
6372
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.19.
|
|
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.
|
|
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.
|
|
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",
|