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.
Files changed (3) hide show
  1. package/index.ts +79 -47
  2. package/package.json +2 -2
  3. 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: number;
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, 9, 5] as const;
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 line of output.split('\n')) {
349
- try {
350
- const parsed = JSON.parse(line);
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
- typeof parsed === 'object' &&
354
- parsed !== null &&
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
- errors.push(parsed as LandstripErrorResponse);
379
+ error.type = fields.type as LandstripErrorResponse['type'];
364
380
  }
365
- } catch {
366
- // ignore non-JSON lines
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 proxy = await startProxy(ctx, cwd);
862
- const policy = writePolicyFile(cwd, proxy.port);
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.stop();
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.port),
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 = allowsAllDomains(config.network.allowedDomains)
1017
- ? 'all domains'
1018
- : `${config.network.allowedDomains.length} domains`;
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.9.5 or newer is required; found: ${version}`, 'error');
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
- if (!loadConfig(ctx.cwd).enabled) return;
1097
-
1098
- const blockedDomain = await preflightCommandDomains(event.command, ctx);
1099
- if (blockedDomain) {
1100
- return {
1101
- result: {
1102
- output: `Blocked: "${blockedDomain}" is not allowed by the sandbox. Use /sandbox to review your config.`,
1103
- exitCode: 1,
1104
- cancelled: false,
1105
- truncated: false,
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
- const blockedDomain = await preflightCommandDomains(event.input.command, ctx);
1123
- if (blockedDomain) {
1124
- return {
1125
- block: true,
1126
- reason: `Network access to "${blockedDomain}" is blocked by the sandbox.`,
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
- 'Network (bash through HTTP proxy):',
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.2",
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.9.5"
34
+ "@jarkkojs/landstrip": "^0.10.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.78.0",
package/sandbox.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "enabled": true,
3
3
  "network": {
4
+ "allowNetwork": false,
4
5
  "allowLocalBinding": true,
5
6
  "allowAllUnixSockets": false,
6
7
  "allowUnixSockets": [],