pi-landstrip 0.2.0 → 0.3.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/README.md CHANGED
@@ -3,23 +3,17 @@
3
3
  Landlock-based sandboxing for [pi](https://pi.dev/) using
4
4
  [`landstrip`](https://github.com/jarkkojs/landstrip).
5
5
 
6
- ## Prerequisites
7
-
8
- Install `landstrip` and make sure it is on the `PATH` used to launch pi:
9
-
10
- ```bash
11
- cargo install landstrip
12
- ```
13
-
14
- `landstrip` currently targets Linux. On other platforms this extension loads
15
- but leaves sandboxing disabled.
16
-
17
6
  ## Install
18
7
 
19
8
  ```bash
20
9
  pi install npm:pi-landstrip
21
10
  ```
22
11
 
12
+ This installs `pi-landstrip` and its `@jarkkojs/landstrip` dependency, which
13
+ includes platform-specific native binaries for Linux, macOS, and Windows.
14
+
15
+ On unsupported platforms the extension loads but leaves sandboxing disabled.
16
+
23
17
  ## Configure
24
18
 
25
19
  Create `.pi/sandbox.json` in a project or `~/.pi/agent/sandbox.json` globally.
package/index.ts CHANGED
@@ -10,6 +10,8 @@ import type {
10
10
  ExtensionContext,
11
11
  } from '@earendil-works/pi-coding-agent';
12
12
 
13
+ import { binaryPath } from '@jarkkojs/landstrip';
14
+
13
15
  import { spawn, spawnSync } from 'node:child_process';
14
16
  import {
15
17
  existsSync,
@@ -50,16 +52,10 @@ interface SandboxNetworkConfig {
50
52
  deniedDomains: string[];
51
53
  }
52
54
 
53
- interface LandstripConfig {
54
- command: string;
55
- debug: boolean;
56
- }
57
-
58
55
  interface SandboxConfig {
59
56
  enabled: boolean;
60
57
  network: SandboxNetworkConfig;
61
58
  filesystem: SandboxFilesystemConfig;
62
- landstrip: LandstripConfig;
63
59
  }
64
60
 
65
61
  interface LandstripPolicy {
@@ -72,7 +68,16 @@ interface LandstripPolicy {
72
68
  filesystem: SandboxFilesystemConfig;
73
69
  }
74
70
 
75
- const LANDSTRIP_VERSION = [0, 8, 3] as const;
71
+ interface LandstripErrorResponse {
72
+ category: 'policy' | 'tool' | 'platform' | 'system';
73
+ file?: string;
74
+ program?: string;
75
+ target?: 'filesystem' | 'network' | 'platform';
76
+ kind?: 'launch' | 'encoding';
77
+ message: string;
78
+ }
79
+
80
+ const LANDSTRIP_VERSION = [0, 9, 2] as const;
76
81
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
77
82
 
78
83
  const DEFAULT_CONFIG: SandboxConfig = {
@@ -100,14 +105,10 @@ const DEFAULT_CONFIG: SandboxConfig = {
100
105
  },
101
106
  filesystem: {
102
107
  denyRead: ['/Users', '/home'],
103
- allowRead: ['.', '~/.config', '~/.local', '~/.cargo'],
108
+ allowRead: ['.', '~/.config', '~/.gitconfig', '~/.local', '~/.cargo'],
104
109
  allowWrite: ['.', '/tmp'],
105
110
  denyWrite: ['.env', '.env.*', '*.pem', '*.key'],
106
111
  },
107
- landstrip: {
108
- command: 'landstrip',
109
- debug: false,
110
- },
111
112
  };
112
113
 
113
114
  type PermissionChoice = 'abort' | 'session' | 'project' | 'global';
@@ -176,10 +177,6 @@ function deepMerge(base: SandboxConfig, overrides: Partial<SandboxConfig>): Sand
176
177
  ...base.filesystem,
177
178
  ...overrides.filesystem,
178
179
  },
179
- landstrip: {
180
- ...base.landstrip,
181
- ...overrides.landstrip,
182
- },
183
180
  };
184
181
  }
185
182
 
@@ -344,6 +341,52 @@ function extractBlockedWritePath(output: string, cwd: string): string | null {
344
341
  return match ? normalizeBlockedPath(match[1], cwd) : null;
345
342
  }
346
343
 
344
+ function parseLandstripErrors(output: string): LandstripErrorResponse[] {
345
+ const errors: LandstripErrorResponse[] = [];
346
+
347
+ for (const line of output.split('\n')) {
348
+ try {
349
+ const parsed = JSON.parse(line);
350
+
351
+ if (
352
+ typeof parsed === 'object' &&
353
+ parsed !== null &&
354
+ typeof parsed.category === 'string' &&
355
+ ['policy', 'tool', 'platform', 'system'].includes(parsed.category) &&
356
+ typeof parsed.message === 'string' &&
357
+ parsed.message.length > 0
358
+ ) {
359
+ errors.push(parsed as LandstripErrorResponse);
360
+ }
361
+ } catch {
362
+ // ignore non-JSON lines
363
+ }
364
+ }
365
+
366
+ return errors;
367
+ }
368
+
369
+ function formatLandstripErrors(errors: LandstripErrorResponse[]): string {
370
+ return errors
371
+ .map((err) => {
372
+ const parts: string[] = [`landstrip: ${err.category}`];
373
+
374
+ if (err.target) {
375
+ parts.push(`(${err.target})`);
376
+ }
377
+ if (err.program) {
378
+ parts.push(` ${err.program}`);
379
+ }
380
+ if (err.kind) {
381
+ parts.push(`:${err.kind}`);
382
+ }
383
+ parts.push(`: ${err.message}`);
384
+
385
+ return parts.join('');
386
+ })
387
+ .join('\n');
388
+ }
389
+
347
390
  async function showPermissionPrompt(
348
391
  ctx: ExtensionContext,
349
392
  title: string,
@@ -472,8 +515,8 @@ function promptWriteBlock(ctx: ExtensionContext, filePath: string): Promise<Perm
472
515
  );
473
516
  }
474
517
 
475
- function landstripVersion(command: string): string | null {
476
- const result = spawnSync(command, ['--version'], { encoding: 'utf-8' });
518
+ function landstripVersion(): string | null {
519
+ const result = spawnSync(binaryPath(), ['--version'], { encoding: 'utf-8' });
477
520
  if (result.status !== 0) return null;
478
521
  return result.stdout.trim();
479
522
  }
@@ -790,18 +833,10 @@ export default function (pi: ExtensionAPI) {
790
833
  async exec(command, cwd, { onData, signal, timeout, env }) {
791
834
  if (!existsSync(cwd)) throw new Error(`Working directory does not exist: ${cwd}`);
792
835
 
793
- const config = loadConfig(cwd);
794
836
  const { shell, args } = getShellConfig(userShellPath);
795
837
  const proxy = await startProxy(ctx, cwd);
796
838
  const policy = writePolicyFile(cwd, proxy.port);
797
- const landstripArgs = [
798
- ...(config.landstrip.debug ? ['--debug'] : []),
799
- '-p',
800
- policy.path,
801
- shell,
802
- ...args,
803
- command,
804
- ];
839
+ const landstripArgs = ['-p', policy.path, shell, ...args, command];
805
840
 
806
841
  return new Promise((resolvePromise, reject) => {
807
842
  let timeoutHandle: NodeJS.Timeout | undefined;
@@ -817,7 +852,7 @@ export default function (pi: ExtensionAPI) {
817
852
  rmSync(policy.dir, { recursive: true, force: true });
818
853
  };
819
854
 
820
- const child = spawn(config.landstrip.command, landstripArgs, {
855
+ const child = spawn(binaryPath(), landstripArgs, {
821
856
  cwd,
822
857
  env: proxyEnv(env, proxy.port),
823
858
  detached: true,
@@ -886,6 +921,11 @@ export default function (pi: ExtensionAPI) {
886
921
  .filter((content) => content.type === 'text')
887
922
  .map((content) => content.text)
888
923
  .join('\n');
924
+ const landstripErrors = parseLandstripErrors(outputText);
925
+ if (landstripErrors.length > 0) {
926
+ const message = formatLandstripErrors(landstripErrors);
927
+ result.content.unshift({ type: 'text', text: `\n${message}\n` });
928
+ }
889
929
  const blockedPath = extractBlockedWritePath(outputText, ctx.cwd);
890
930
 
891
931
  if (!blockedPath || !ctx.hasUI) return result;
@@ -956,18 +996,21 @@ export default function (pi: ExtensionAPI) {
956
996
  return false;
957
997
  }
958
998
 
959
- const version = landstripVersion(config.landstrip.command);
999
+ const version = landstripVersion();
960
1000
  if (!version) {
961
1001
  sandboxEnabled = false;
962
1002
  sandboxReady = false;
963
- ctx.ui.notify(`landstrip was not found. Install it with: cargo install landstrip`, 'error');
1003
+ ctx.ui.notify(
1004
+ `landstrip was not found. Reinstall with: npm install @jarkkojs/landstrip`,
1005
+ 'error',
1006
+ );
964
1007
  return false;
965
1008
  }
966
1009
 
967
1010
  if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
968
1011
  sandboxEnabled = false;
969
1012
  sandboxReady = false;
970
- ctx.ui.notify(`landstrip 0.8.3 or newer is required; found: ${version}`, 'error');
1013
+ ctx.ui.notify(`landstrip 0.9.2 or newer is required; found: ${version}`, 'error');
971
1014
  return false;
972
1015
  }
973
1016
 
@@ -1127,7 +1170,7 @@ export default function (pi: ExtensionAPI) {
1127
1170
  'Sandbox Configuration',
1128
1171
  ` Project config: ${projectPath}`,
1129
1172
  ` Global config: ${globalPath}`,
1130
- ` landstrip: ${config.landstrip.command}`,
1173
+ ` landstrip: ${binaryPath()}`,
1131
1174
  '',
1132
1175
  'Network (bash through HTTP proxy):',
1133
1176
  ` Allowed domains: ${config.network.allowedDomains.join(', ') || '(none)'}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -29,7 +29,8 @@
29
29
  "ci:check": "npm run check"
30
30
  },
31
31
  "dependencies": {
32
- "@earendil-works/pi-tui": "^0.78.0"
32
+ "@earendil-works/pi-tui": "^0.78.0",
33
+ "@jarkkojs/landstrip": "^0.9.2"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@earendil-works/pi-coding-agent": "^0.78.0",
package/sandbox.json CHANGED
@@ -7,9 +7,15 @@
7
7
  "allowedDomains": [
8
8
  "github.com",
9
9
  "*.github.com",
10
+ "api.github.com",
10
11
  "raw.githubusercontent.com",
12
+ "objects.githubusercontent.com",
13
+ "codeload.github.com",
11
14
  "registry.npmjs.org",
15
+ "npmjs.org",
12
16
  "*.npmjs.org",
17
+ "nodejs.org",
18
+ "*.nodejs.org",
13
19
  "crates.io",
14
20
  "*.crates.io",
15
21
  "static.crates.io"
@@ -18,8 +24,33 @@
18
24
  },
19
25
  "filesystem": {
20
26
  "denyRead": ["/home"],
21
- "allowRead": [".", "~/.config", "~/.local", "~/.cargo"],
22
- "allowWrite": [".", "/tmp", "~/.cargo", "~/.rustup"],
27
+ "allowRead": [
28
+ ".",
29
+ "/tmp",
30
+ "/var/tmp",
31
+ "/dev/null",
32
+ "~/.config",
33
+ "~/.gitconfig",
34
+ "~/.local",
35
+ "~/.cargo",
36
+ "~/.rustup",
37
+ "~/.npm",
38
+ "~/.cache",
39
+ "~/.bun",
40
+ "~/.node-gyp"
41
+ ],
42
+ "allowWrite": [
43
+ ".",
44
+ "/tmp",
45
+ "/var/tmp",
46
+ "/dev/null",
47
+ "~/.cargo",
48
+ "~/.rustup",
49
+ "~/.npm",
50
+ "~/.cache",
51
+ "~/.bun",
52
+ "~/.node-gyp"
53
+ ],
23
54
  "denyWrite": [".env", ".env.*", "*.pem", "*.key"]
24
55
  }
25
56
  }