pi-landstrip 0.18.19 → 0.18.20
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 +16 -5
- package/dist/index.ts +48 -17
- package/dist/sandbox.json +3 -0
- package/package.json +2 -2
- package/sandbox.json +3 -0
package/README.md
CHANGED
|
@@ -32,12 +32,14 @@ the sandbox is explicitly disabled.
|
|
|
32
32
|
|
|
33
33
|
The root Pi process is trusted. Pi filesystem tools and plugin callbacks run
|
|
34
34
|
outside the OS sandbox. Agent permissions still control tool dispatch, but they
|
|
35
|
-
are not an OS isolation boundary.
|
|
35
|
+
are not an OS isolation boundary. On Linux and macOS, primary Bash reads therefore
|
|
36
|
+
use the same host view by default; writes remain sandboxed. Subagent processes
|
|
37
|
+
retain the configured read and write policy.
|
|
36
38
|
|
|
37
|
-
The default [sandbox policy](./sandbox.json) limits
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
domains.
|
|
39
|
+
The default [sandbox policy](./sandbox.json) limits writes to the project and
|
|
40
|
+
restricts worker reads within the user's home to the project and a few bootstrap
|
|
41
|
+
files. Direct network access is blocked. When the platform permits loopback, the
|
|
42
|
+
extension's proxy filters allowed HTTP and HTTPS domains.
|
|
41
43
|
|
|
42
44
|
When a command needs more access, Pi can allow it once, for the session, or in
|
|
43
45
|
the project or global policy. Project approvals go to `.pi/sandbox.json` and
|
|
@@ -61,6 +63,15 @@ Subagents remain separate processes but lose Landstrip OS isolation. Pi warns
|
|
|
61
63
|
once per session. Trusted project config overrides global config; `/sandbox`
|
|
62
64
|
updates a project sandbox file when present, otherwise the global file.
|
|
63
65
|
|
|
66
|
+
### Shell read access
|
|
67
|
+
|
|
68
|
+
`"shell": { "readAccess": "host" }` keeps primary Bash and `!`/`!!` reads
|
|
69
|
+
consistent with trusted Pi filesystem tools. Set `readAccess` to `"policy"` to
|
|
70
|
+
apply `filesystem.denyRead` and `filesystem.allowRead` to shell commands as an
|
|
71
|
+
intentional stricter boundary. Worker processes always use the filesystem read
|
|
72
|
+
policy. Windows always uses policy reads because its sandbox requires an explicit
|
|
73
|
+
read allowlist.
|
|
74
|
+
|
|
64
75
|
### Permission layers
|
|
65
76
|
|
|
66
77
|
| Layer | Checked | Controls |
|
package/dist/index.ts
CHANGED
|
@@ -3909,6 +3909,15 @@ function registerSubagents(pi, integration) {
|
|
|
3909
3909
|
}
|
|
3910
3910
|
|
|
3911
3911
|
// index.ts
|
|
3912
|
+
function enforcesShellReadPolicy(shellReadAccess, platform = process.platform) {
|
|
3913
|
+
return shellReadAccess !== "host" || platform === "win32";
|
|
3914
|
+
}
|
|
3915
|
+
function resolveProcessReadPolicy(audience, shellReadAccess, denyRead, allowRead, platform = process.platform) {
|
|
3916
|
+
if (audience === "shell" && !enforcesShellReadPolicy(shellReadAccess, platform)) {
|
|
3917
|
+
return { denyRead: [], allowRead: [] };
|
|
3918
|
+
}
|
|
3919
|
+
return { denyRead, allowRead };
|
|
3920
|
+
}
|
|
3912
3921
|
var SUPPORTED_PLATFORMS = new Set(["linux", "darwin", "win32"]);
|
|
3913
3922
|
var prohibitedProxyAddresses = new BlockList;
|
|
3914
3923
|
for (const [network, prefix] of [
|
|
@@ -3998,11 +4007,15 @@ function mergeArray(base, override) {
|
|
|
3998
4007
|
return [...new Set([...base, ...override])];
|
|
3999
4008
|
}
|
|
4000
4009
|
function deepMerge(base, overrides) {
|
|
4010
|
+
const shell = overrides.shell;
|
|
4001
4011
|
const network = overrides.network;
|
|
4002
4012
|
const filesystem = overrides.filesystem;
|
|
4003
4013
|
const windows = overrides.windows;
|
|
4004
4014
|
return {
|
|
4005
4015
|
enabled: overrides.enabled ?? base.enabled,
|
|
4016
|
+
shell: {
|
|
4017
|
+
readAccess: shell?.readAccess ?? base.shell.readAccess
|
|
4018
|
+
},
|
|
4006
4019
|
network: {
|
|
4007
4020
|
allowNetwork: network?.allowNetwork ?? base.network.allowNetwork,
|
|
4008
4021
|
allowLocalBinding: network?.allowLocalBinding ?? base.network.allowLocalBinding,
|
|
@@ -4247,10 +4260,10 @@ function longestPrefixMatch(path, patterns, cwd) {
|
|
|
4247
4260
|
return best;
|
|
4248
4261
|
}
|
|
4249
4262
|
function readAllowed(path, allowRead, denyRead, cwd) {
|
|
4250
|
-
const
|
|
4251
|
-
if (
|
|
4252
|
-
return
|
|
4253
|
-
return
|
|
4263
|
+
const deny = longestPrefixMatch(path, denyRead, cwd);
|
|
4264
|
+
if (deny < 0)
|
|
4265
|
+
return true;
|
|
4266
|
+
return longestPrefixMatch(path, allowRead, cwd) >= deny;
|
|
4254
4267
|
}
|
|
4255
4268
|
function isPathLike(value) {
|
|
4256
4269
|
const trimmed = value.trim();
|
|
@@ -4288,6 +4301,10 @@ function extractNativeDeniedPath(output, cwd) {
|
|
|
4288
4301
|
return normalizePathMatch(match[1], cwd);
|
|
4289
4302
|
return null;
|
|
4290
4303
|
}
|
|
4304
|
+
function extractRetryableNativeReadDeniedPath(output, cwd, allowRead, denyRead) {
|
|
4305
|
+
const path = extractNativeDeniedPath(output, cwd);
|
|
4306
|
+
return path && !readAllowed(path, allowRead, denyRead, cwd) ? path : null;
|
|
4307
|
+
}
|
|
4291
4308
|
function extractNativeWriteDeniedPath(output, cwd) {
|
|
4292
4309
|
const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
|
|
4293
4310
|
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"));
|
|
@@ -4845,8 +4862,9 @@ function createLandstripIntegration(options = {}) {
|
|
|
4845
4862
|
return true;
|
|
4846
4863
|
}, signal);
|
|
4847
4864
|
}
|
|
4848
|
-
function buildLandstripPolicy(cwd, proxyPort, allowances) {
|
|
4865
|
+
function buildLandstripPolicy(cwd, proxyPort, audience, allowances) {
|
|
4849
4866
|
const config = loadConfig(cwd);
|
|
4867
|
+
const readPolicy = resolveProcessReadPolicy(audience, config.shell.readAccess, getEffectiveDenyRead(config, cwd), getEffectiveAllowRead(config, cwd, allowances));
|
|
4850
4868
|
return {
|
|
4851
4869
|
network: {
|
|
4852
4870
|
allowNetwork: config.network.allowNetwork,
|
|
@@ -4856,18 +4874,17 @@ function createLandstripIntegration(options = {}) {
|
|
|
4856
4874
|
...proxyPort !== null ? { httpProxyPort: proxyPort } : {}
|
|
4857
4875
|
},
|
|
4858
4876
|
filesystem: {
|
|
4859
|
-
|
|
4860
|
-
allowRead: getEffectiveAllowRead(config, cwd, allowances),
|
|
4877
|
+
...readPolicy,
|
|
4861
4878
|
allowWrite: getEffectiveAllowWrite(config, allowances),
|
|
4862
4879
|
denyWrite: config.filesystem.denyWrite
|
|
4863
4880
|
},
|
|
4864
4881
|
windows: config.windows
|
|
4865
4882
|
};
|
|
4866
4883
|
}
|
|
4867
|
-
function writePolicyFile(cwd, proxyPort, allowances) {
|
|
4884
|
+
function writePolicyFile(cwd, proxyPort, audience, allowances) {
|
|
4868
4885
|
const dir = mkdtempSync2(join7(tmpdir2(), "pi-landstrip-"));
|
|
4869
4886
|
const path = join7(dir, "policy.json");
|
|
4870
|
-
writeFileSync4(path, JSON.stringify(buildLandstripPolicy(cwd, proxyPort, allowances), null, 2) + `
|
|
4887
|
+
writeFileSync4(path, JSON.stringify(buildLandstripPolicy(cwd, proxyPort, audience, allowances), null, 2) + `
|
|
4871
4888
|
`, "utf-8");
|
|
4872
4889
|
return { dir, path };
|
|
4873
4890
|
}
|
|
@@ -5078,7 +5095,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
5078
5095
|
if (trap.operation === "write" && matchesPattern(path, config.filesystem.denyWrite, cwd)) {
|
|
5079
5096
|
return { action: "deny", reason: "hard-deny" };
|
|
5080
5097
|
}
|
|
5081
|
-
const allowed = trap.operation === "read" ? readAllowed(path, getEffectiveAllowRead(config, cwd, allowances), config
|
|
5098
|
+
const allowed = trap.operation === "read" ? readAllowed(path, getEffectiveAllowRead(config, cwd, allowances), getEffectiveDenyRead(config, cwd), cwd) : !shouldPromptForWrite(path, getEffectiveAllowWrite(config, allowances), cwd);
|
|
5082
5099
|
return allowed ? { action: "allow" } : undefined;
|
|
5083
5100
|
};
|
|
5084
5101
|
const existing = current();
|
|
@@ -5213,7 +5230,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
5213
5230
|
try {
|
|
5214
5231
|
if (options2.signal?.aborted)
|
|
5215
5232
|
throw new Error("Task cancelled");
|
|
5216
|
-
policy = writePolicyFile(options2.cwd, proxy?.port ?? null, processAllowances);
|
|
5233
|
+
policy = writePolicyFile(options2.cwd, proxy?.port ?? null, "worker", processAllowances);
|
|
5217
5234
|
if (process.platform !== "win32") {
|
|
5218
5235
|
[trapSocket, childEnd] = await createSocketPair();
|
|
5219
5236
|
if (options2.signal?.aborted)
|
|
@@ -5347,7 +5364,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
5347
5364
|
readPaths: [...allowances.readPaths, ...invocation.readPaths ?? []]
|
|
5348
5365
|
};
|
|
5349
5366
|
processAllowances = withWindowsProcessReadAccess(processAllowances, invocation.executable, cwd);
|
|
5350
|
-
policy = writePolicyFile(cwd, proxy?.port ?? null, processAllowances);
|
|
5367
|
+
policy = writePolicyFile(cwd, proxy?.port ?? null, "shell", processAllowances);
|
|
5351
5368
|
landstripArgs = [
|
|
5352
5369
|
"run",
|
|
5353
5370
|
"-p",
|
|
@@ -5565,6 +5582,15 @@ function createLandstripIntegration(options = {}) {
|
|
|
5565
5582
|
shellPath: SettingsManager.create(ctx.cwd).getShellPath()
|
|
5566
5583
|
});
|
|
5567
5584
|
const run = () => sandboxedBash.execute(id, params, signal, onUpdate, ctx);
|
|
5585
|
+
const blockedReadPathForRetry = (nativeOutput) => {
|
|
5586
|
+
const config = loadConfig(ctx.cwd);
|
|
5587
|
+
if (!enforcesShellReadPolicy(config.shell.readAccess))
|
|
5588
|
+
return null;
|
|
5589
|
+
const trappedPath = extractTrapBlockedPath(landstripErrorOutput, ctx.cwd, "read");
|
|
5590
|
+
if (trappedPath)
|
|
5591
|
+
return trappedPath;
|
|
5592
|
+
return extractRetryableNativeReadDeniedPath(nativeOutput, ctx.cwd, getEffectiveAllowRead(config, ctx.cwd, allowances), getEffectiveDenyRead(config, ctx.cwd));
|
|
5593
|
+
};
|
|
5568
5594
|
const retryWithAccess = async (operation, blockedPath) => {
|
|
5569
5595
|
if (!ctx.hasUI)
|
|
5570
5596
|
return null;
|
|
@@ -5574,7 +5600,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
5574
5600
|
if (operation === "write" && matchesPattern(blockedPath, config2.filesystem.denyWrite, ctx.cwd)) {
|
|
5575
5601
|
return false;
|
|
5576
5602
|
}
|
|
5577
|
-
const needsPrompt = operation === "read" ? !
|
|
5603
|
+
const needsPrompt = operation === "read" ? !readAllowed(blockedPath, getEffectiveAllowRead(config2, ctx.cwd, allowances), getEffectiveDenyRead(config2, ctx.cwd), ctx.cwd) : shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config2, allowances), ctx.cwd);
|
|
5578
5604
|
return needsPrompt ? undefined : true;
|
|
5579
5605
|
};
|
|
5580
5606
|
const access = await permissionPrompts.resolve(current, async () => {
|
|
@@ -5633,7 +5659,7 @@ ${errorText}`;
|
|
|
5633
5659
|
if (retryResult2)
|
|
5634
5660
|
return retryResult2;
|
|
5635
5661
|
}
|
|
5636
|
-
const blockedReadPath2 =
|
|
5662
|
+
const blockedReadPath2 = blockedReadPathForRetry(fallbackOutput);
|
|
5637
5663
|
if (blockedReadPath2) {
|
|
5638
5664
|
const retryResult2 = await retryWithAccess("read", blockedReadPath2);
|
|
5639
5665
|
if (retryResult2)
|
|
@@ -5658,7 +5684,7 @@ ${message}
|
|
|
5658
5684
|
if (retryResult2)
|
|
5659
5685
|
return retryResult2;
|
|
5660
5686
|
}
|
|
5661
|
-
const blockedReadPath =
|
|
5687
|
+
const blockedReadPath = blockedReadPathForRetry(stderrOutput);
|
|
5662
5688
|
if (!blockedReadPath)
|
|
5663
5689
|
return result;
|
|
5664
5690
|
const retryResult = await retryWithAccess("read", blockedReadPath);
|
|
@@ -5871,6 +5897,8 @@ ${message}
|
|
|
5871
5897
|
return { color: "success", label: "Active" };
|
|
5872
5898
|
};
|
|
5873
5899
|
const networkMode = config.network.allowNetwork ? "Unrestricted" : windowsImplementation === "appContainer" && !config.windows.allowLoopback ? "Blocked" : "Proxied";
|
|
5900
|
+
const shellReadMode = process.platform === "win32" ? "Policy (required)" : config.shell.readAccess === "host" ? "Host-aligned" : "Policy";
|
|
5901
|
+
const readRuleScope = enforcesShellReadPolicy(config.shell.readAccess) ? "Workers and shell" : "Workers only";
|
|
5874
5902
|
return {
|
|
5875
5903
|
render(width) {
|
|
5876
5904
|
const innerWidth = Math.max(1, width - 4);
|
|
@@ -5888,12 +5916,12 @@ ${message}
|
|
|
5888
5916
|
]);
|
|
5889
5917
|
const content = [tabs, ""];
|
|
5890
5918
|
if (tab === "Overview") {
|
|
5891
|
-
content.push(`${theme.fg(status.color, "●")} ${text(status.label)}`, item("Configured", config.enabled ? text("Enabled") : theme.fg("warning", "Disabled")), item("Runtime", sandboxEnabled && sandboxReady ? theme.fg("success", "Running") : muted("Not running")), "", accent("Protection"), item("Network", text(networkMode)), item("
|
|
5919
|
+
content.push(`${theme.fg(status.color, "●")} ${text(status.label)}`, item("Configured", config.enabled ? text("Enabled") : theme.fg("warning", "Disabled")), item("Runtime", sandboxEnabled && sandboxReady ? theme.fg("success", "Running") : muted("Not running")), "", accent("Protection"), item("Network", text(networkMode)), item("Shell reads", text(shellReadMode)), item("Read rules", text(`${config.filesystem.denyRead.length + config.filesystem.allowRead.length} · ${readRuleScope}`)), item("Write rules", text(String(config.filesystem.allowWrite.length + config.filesystem.denyWrite.length))), item("Session grants", text(String(sessionAllowedDomains.length + sessionAllowedReadPaths.length + sessionAllowedWritePaths.length))), "", accent("Configuration"), item("Change scope", text(target.scope === "project" ? "Project" : "Global")), item("Change file", text(truncateToWidth3(target.path, Math.max(10, innerWidth - 20)))), item("Project file", text(truncateToWidth3(projectPath, Math.max(10, innerWidth - 20)))), item("Global file", text(truncateToWidth3(globalPath, Math.max(10, innerWidth - 20)))), item("Landstrip binary", text(truncateToWidth3(landstripDisplayPath(), Math.max(10, innerWidth - 20)))));
|
|
5892
5920
|
if (windowsImplementation !== undefined) {
|
|
5893
5921
|
content.push(item("Windows mode", text(windowsImplementation === "restrictedUser" ? "Restricted user" : `AppContainer (${config.windows.appContainerMode})`)));
|
|
5894
5922
|
}
|
|
5895
5923
|
} else {
|
|
5896
|
-
content.push(accent(`Network · ${networkMode}`), item("Allowed domains", listValue(config.network.allowedDomains)), item("Denied domains", listValue(config.network.deniedDomains)), item("Unix sockets", config.network.allowAllUnixSockets ? text("all") : listValue(config.network.allowUnixSockets)), "", accent("Filesystem"), item("Denied reads", listValue(config.filesystem.denyRead)), item("Allowed reads", listValue(config.filesystem.allowRead)), item("Allowed writes", listValue(config.filesystem.allowWrite)), item("Denied writes", listValue(config.filesystem.denyWrite)), "", accent("Session grants"), item("Domains", listValue(sessionAllowedDomains)), item("Read paths", listValue(sessionAllowedReadPaths)), item("Write paths", listValue(sessionAllowedWritePaths)));
|
|
5924
|
+
content.push(accent(`Network · ${networkMode}`), item("Allowed domains", listValue(config.network.allowedDomains)), item("Denied domains", listValue(config.network.deniedDomains)), item("Unix sockets", config.network.allowAllUnixSockets ? text("all") : listValue(config.network.allowUnixSockets)), "", accent("Filesystem"), item("Shell reads", text(shellReadMode)), item("Read rules for", text(readRuleScope)), item("Denied reads", listValue(config.filesystem.denyRead)), item("Allowed reads", listValue(config.filesystem.allowRead)), item("Allowed writes", listValue(config.filesystem.allowWrite)), item("Denied writes", listValue(config.filesystem.denyWrite)), "", accent("Session grants"), item("Domains", listValue(sessionAllowedDomains)), item("Read paths", listValue(sessionAllowedReadPaths)), item("Write paths", listValue(sessionAllowedWritePaths)));
|
|
5897
5925
|
}
|
|
5898
5926
|
content.push("", footer);
|
|
5899
5927
|
return [
|
|
@@ -5983,14 +6011,17 @@ export {
|
|
|
5983
6011
|
writeEnvFile,
|
|
5984
6012
|
shouldPromptForWrite,
|
|
5985
6013
|
sessionScopeFor,
|
|
6014
|
+
resolveProcessReadPolicy,
|
|
5986
6015
|
readAllowed,
|
|
5987
6016
|
parseTrapLine,
|
|
5988
6017
|
matchesPattern,
|
|
5989
6018
|
isQueryTrap,
|
|
5990
6019
|
isPublicProxyAddress,
|
|
5991
6020
|
formatLandstripTraps,
|
|
6021
|
+
extractRetryableNativeReadDeniedPath,
|
|
5992
6022
|
extractNativeWriteDeniedPath,
|
|
5993
6023
|
extractNativeDeniedPath,
|
|
6024
|
+
enforcesShellReadPolicy,
|
|
5994
6025
|
domainMatchesAny,
|
|
5995
6026
|
pi_landstrip_default as default,
|
|
5996
6027
|
createWindowsDenyRead,
|
package/dist/sandbox.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.20",
|
|
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": "^0.18.
|
|
42
|
+
"@landstrip/landstrip": "^0.18.20",
|
|
43
43
|
"jsonc-parser": "^3.3.1",
|
|
44
44
|
"minimatch": "^10.2.5",
|
|
45
45
|
"yaml": "^2.9.0"
|