pi-landstrip 0.3.2 → 0.4.0
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/index.ts +79 -47
- package/package.json +2 -2
- package/sandbox.json +1 -0
package/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ interface SandboxFilesystemConfig {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
interface SandboxNetworkConfig {
|
|
50
|
+
allowNetwork: boolean;
|
|
50
51
|
allowLocalBinding: boolean;
|
|
51
52
|
allowAllUnixSockets: boolean;
|
|
52
53
|
allowUnixSockets: string[];
|
|
@@ -62,10 +63,11 @@ interface SandboxConfig {
|
|
|
62
63
|
|
|
63
64
|
interface LandstripPolicy {
|
|
64
65
|
network: {
|
|
66
|
+
allowNetwork: boolean;
|
|
65
67
|
allowLocalBinding: boolean;
|
|
66
68
|
allowAllUnixSockets: boolean;
|
|
67
69
|
allowUnixSockets: string[];
|
|
68
|
-
httpProxyPort
|
|
70
|
+
httpProxyPort?: number;
|
|
69
71
|
};
|
|
70
72
|
filesystem: SandboxFilesystemConfig;
|
|
71
73
|
}
|
|
@@ -78,12 +80,13 @@ interface LandstripErrorResponse {
|
|
|
78
80
|
message: string;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
const LANDSTRIP_VERSION = [0,
|
|
83
|
+
const LANDSTRIP_VERSION = [0, 10, 1] as const;
|
|
82
84
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
83
85
|
|
|
84
86
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
85
87
|
enabled: true,
|
|
86
88
|
network: {
|
|
89
|
+
allowNetwork: false,
|
|
87
90
|
allowLocalBinding: false,
|
|
88
91
|
allowAllUnixSockets: false,
|
|
89
92
|
allowUnixSockets: [],
|
|
@@ -345,25 +348,38 @@ function extractBlockedWritePath(output: string, cwd: string): string | null {
|
|
|
345
348
|
function parseLandstripErrors(output: string): LandstripErrorResponse[] {
|
|
346
349
|
const errors: LandstripErrorResponse[] = [];
|
|
347
350
|
|
|
348
|
-
for (const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
+
for (const block of output.trim().split(/\n\n+/)) {
|
|
352
|
+
const fields: Record<string, string> = {};
|
|
353
|
+
|
|
354
|
+
for (const line of block.split('\n')) {
|
|
355
|
+
const colonIndex = line.indexOf(':');
|
|
356
|
+
if (colonIndex === -1) continue;
|
|
357
|
+
const key = line.slice(0, colonIndex).trim();
|
|
358
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
359
|
+
if (key.length > 0 && value.length > 0) fields[key] = value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
fields.category &&
|
|
364
|
+
['policy', 'tool', 'platform', 'system'].includes(fields.category) &&
|
|
365
|
+
fields.message
|
|
366
|
+
) {
|
|
367
|
+
const error: LandstripErrorResponse = {
|
|
368
|
+
category: fields.category as LandstripErrorResponse['category'],
|
|
369
|
+
message: fields.message,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if (fields.file) error.file = fields.file;
|
|
373
|
+
if (fields.program) error.program = fields.program;
|
|
351
374
|
|
|
352
375
|
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
typeof parsed.category === 'string' &&
|
|
356
|
-
['policy', 'tool', 'platform', 'system'].includes(parsed.category) &&
|
|
357
|
-
(parsed.type === undefined ||
|
|
358
|
-
(typeof parsed.type === 'string' &&
|
|
359
|
-
['filesystem', 'network', 'platform', 'launch', 'encoding'].includes(parsed.type))) &&
|
|
360
|
-
typeof parsed.message === 'string' &&
|
|
361
|
-
parsed.message.length > 0
|
|
376
|
+
fields.type &&
|
|
377
|
+
['filesystem', 'network', 'platform', 'launch', 'encoding'].includes(fields.type)
|
|
362
378
|
) {
|
|
363
|
-
|
|
379
|
+
error.type = fields.type as LandstripErrorResponse['type'];
|
|
364
380
|
}
|
|
365
|
-
|
|
366
|
-
|
|
381
|
+
|
|
382
|
+
errors.push(error);
|
|
367
383
|
}
|
|
368
384
|
}
|
|
369
385
|
|
|
@@ -694,15 +710,16 @@ export function createLandstripIntegration(
|
|
|
694
710
|
return true;
|
|
695
711
|
}
|
|
696
712
|
|
|
697
|
-
function buildLandstripPolicy(cwd: string, proxyPort: number): LandstripPolicy {
|
|
713
|
+
function buildLandstripPolicy(cwd: string, proxyPort: number | null): LandstripPolicy {
|
|
698
714
|
const config = loadConfig(cwd);
|
|
699
715
|
|
|
700
716
|
return {
|
|
701
717
|
network: {
|
|
718
|
+
allowNetwork: config.network.allowNetwork,
|
|
702
719
|
allowLocalBinding: config.network.allowLocalBinding,
|
|
703
720
|
allowAllUnixSockets: config.network.allowAllUnixSockets,
|
|
704
721
|
allowUnixSockets: config.network.allowUnixSockets,
|
|
705
|
-
httpProxyPort: proxyPort,
|
|
722
|
+
...(proxyPort !== null ? { httpProxyPort: proxyPort } : {}),
|
|
706
723
|
},
|
|
707
724
|
filesystem: {
|
|
708
725
|
denyRead: config.filesystem.denyRead,
|
|
@@ -713,7 +730,7 @@ export function createLandstripIntegration(
|
|
|
713
730
|
};
|
|
714
731
|
}
|
|
715
732
|
|
|
716
|
-
function writePolicyFile(cwd: string, proxyPort: number): { dir: string; path: string } {
|
|
733
|
+
function writePolicyFile(cwd: string, proxyPort: number | null): { dir: string; path: string } {
|
|
717
734
|
const dir = mkdtempSync(join(tmpdir(), 'pi-landstrip-'));
|
|
718
735
|
const path = join(dir, 'policy.json');
|
|
719
736
|
writeFileSync(
|
|
@@ -858,8 +875,11 @@ export function createLandstripIntegration(
|
|
|
858
875
|
if (!existsSync(cwd)) throw new Error(`Working directory does not exist: ${cwd}`);
|
|
859
876
|
|
|
860
877
|
const { shell, args } = getShellConfig(SettingsManager.create(cwd).getShellPath());
|
|
861
|
-
const
|
|
862
|
-
const
|
|
878
|
+
const config = loadConfig(cwd);
|
|
879
|
+
const allowNetwork = config.network.allowNetwork;
|
|
880
|
+
const proxy = allowNetwork ? null : await startProxy(ctx, cwd);
|
|
881
|
+
const proxyPort = proxy ? proxy.port : null;
|
|
882
|
+
const policy = writePolicyFile(cwd, proxyPort);
|
|
863
883
|
const landstripArgs = ['-p', policy.path, shell, ...args, command];
|
|
864
884
|
|
|
865
885
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -872,13 +892,13 @@ export function createLandstripIntegration(
|
|
|
872
892
|
cleaned = true;
|
|
873
893
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
874
894
|
signal?.removeEventListener('abort', onAbort);
|
|
875
|
-
void proxy
|
|
895
|
+
void proxy?.stop();
|
|
876
896
|
rmSync(policy.dir, { recursive: true, force: true });
|
|
877
897
|
};
|
|
878
898
|
|
|
879
899
|
const child = spawn(binaryPath(), landstripArgs, {
|
|
880
900
|
cwd,
|
|
881
|
-
env: proxyEnv(env, proxy
|
|
901
|
+
env: allowNetwork ? { ...process.env, ...env } : proxyEnv(env, proxy!.port),
|
|
882
902
|
detached: true,
|
|
883
903
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
884
904
|
});
|
|
@@ -1005,6 +1025,10 @@ export function createLandstripIntegration(
|
|
|
1005
1025
|
}
|
|
1006
1026
|
|
|
1007
1027
|
function warnIfAllDomainsAllowed(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
1028
|
+
if (config.network.allowNetwork) {
|
|
1029
|
+
ctx.ui.notify('Network sandbox is disabled because network.allowNetwork is true.', 'warning');
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1008
1032
|
if (!allowsAllDomains(config.network.allowedDomains)) return;
|
|
1009
1033
|
ctx.ui.notify(
|
|
1010
1034
|
'Network sandbox allows all domains because network.allowedDomains contains "*".',
|
|
@@ -1013,9 +1037,11 @@ export function createLandstripIntegration(
|
|
|
1013
1037
|
}
|
|
1014
1038
|
|
|
1015
1039
|
function enableStatus(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
1016
|
-
const networkLabel =
|
|
1017
|
-
? '
|
|
1018
|
-
:
|
|
1040
|
+
const networkLabel = config.network.allowNetwork
|
|
1041
|
+
? 'unrestricted'
|
|
1042
|
+
: allowsAllDomains(config.network.allowedDomains)
|
|
1043
|
+
? 'all domains'
|
|
1044
|
+
: `${config.network.allowedDomains.length} domains`;
|
|
1019
1045
|
ctx.ui.setStatus(
|
|
1020
1046
|
'sandbox',
|
|
1021
1047
|
ctx.ui.theme.fg(
|
|
@@ -1049,7 +1075,7 @@ export function createLandstripIntegration(
|
|
|
1049
1075
|
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1050
1076
|
sandboxEnabled = false;
|
|
1051
1077
|
sandboxReady = false;
|
|
1052
|
-
ctx.ui.notify(`landstrip 0.
|
|
1078
|
+
ctx.ui.notify(`landstrip 0.10.1 or newer is required; found: ${version}`, 'error');
|
|
1053
1079
|
return false;
|
|
1054
1080
|
}
|
|
1055
1081
|
|
|
@@ -1093,18 +1119,21 @@ export function createLandstripIntegration(
|
|
|
1093
1119
|
|
|
1094
1120
|
pi.on('user_bash', async (event, ctx) => {
|
|
1095
1121
|
if (!sandboxEnabled || !sandboxReady) return;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1122
|
+
const config = loadConfig(ctx.cwd);
|
|
1123
|
+
if (!config.enabled) return;
|
|
1124
|
+
|
|
1125
|
+
if (!config.network.allowNetwork) {
|
|
1126
|
+
const blockedDomain = await preflightCommandDomains(event.command, ctx);
|
|
1127
|
+
if (blockedDomain) {
|
|
1128
|
+
return {
|
|
1129
|
+
result: {
|
|
1130
|
+
output: `Blocked: "${blockedDomain}" is not allowed by the sandbox. Use /sandbox to review your config.`,
|
|
1131
|
+
exitCode: 1,
|
|
1132
|
+
cancelled: false,
|
|
1133
|
+
truncated: false,
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1108
1137
|
}
|
|
1109
1138
|
|
|
1110
1139
|
return { operations: createLandstripBashOps(ctx) };
|
|
@@ -1119,12 +1148,14 @@ export function createLandstripIntegration(
|
|
|
1119
1148
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1120
1149
|
|
|
1121
1150
|
if (sandboxReady && isToolCallEventType('bash', event)) {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1151
|
+
if (!config.network.allowNetwork) {
|
|
1152
|
+
const blockedDomain = await preflightCommandDomains(event.input.command, ctx);
|
|
1153
|
+
if (blockedDomain) {
|
|
1154
|
+
return {
|
|
1155
|
+
block: true,
|
|
1156
|
+
reason: `Network access to "${blockedDomain}" is blocked by the sandbox.`,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1128
1159
|
}
|
|
1129
1160
|
}
|
|
1130
1161
|
|
|
@@ -1231,7 +1262,8 @@ export function createLandstripIntegration(
|
|
|
1231
1262
|
` Global config: ${globalPath}`,
|
|
1232
1263
|
` landstrip: ${binaryPath()}`,
|
|
1233
1264
|
'',
|
|
1234
|
-
|
|
1265
|
+
`Network (bash ${config.network.allowNetwork ? 'unrestricted' : 'through HTTP proxy'}):`,
|
|
1266
|
+
` Allow network: ${config.network.allowNetwork}`,
|
|
1235
1267
|
` Allowed domains: ${config.network.allowedDomains.join(', ') || '(none)'}`,
|
|
1236
1268
|
` Denied domains: ${config.network.deniedDomains.join(', ') || '(none)'}`,
|
|
1237
1269
|
...(sessionAllowedDomains.length > 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Landlock-based sandboxing for pi with interactive permission prompts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
34
|
-
"@jarkkojs/landstrip": "^0.
|
|
34
|
+
"@jarkkojs/landstrip": "^0.10.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|