pi-landstrip 0.16.7 → 0.16.9

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
@@ -1,29 +1,44 @@
1
1
  # pi-landstrip
2
2
 
3
- ![pi-landstrip screenshot](screenshot.png)
3
+ `pi-landstrip` is an extension for [pi](https://pi.dev/) providing a sandbox
4
+ defined with a policy compatible with Anthropic's JSON format. It uses
5
+ [`landstrip`](https://github.com/landstrip/landstrip) to implement the sandbox.
4
6
 
5
- Landlock-based sandboxing for [pi](https://pi.dev/) using
6
- [`landstrip`](https://github.com/landstrip/landstrip).
7
+ `pi-landstrip` has a default policy [sandbox.json](./sandbox.json), and allows
8
+ the define either or both global or project specific policies.
7
9
 
8
- ## Install
10
+ ## Installing the extension
9
11
 
10
- ```bash
12
+ ### Automatic install
13
+
14
+ ```
11
15
  pi install npm:pi-landstrip
12
16
  ```
13
17
 
14
18
  This installs `pi-landstrip` and its `@landstrip/landstrip` dependency, which
15
19
  includes platform-specific native binaries for Linux, macOS, and Windows.
16
20
 
17
- On unsupported platforms the extension loads but leaves sandboxing disabled.
21
+ ### Manual install
18
22
 
19
- ## Configure
23
+ Add the extension to `~/.pi/agent/settings.json` (global) or
24
+ `.pi/settings.json` (project):
20
25
 
21
- Create `.pi/sandbox.json` in a project or `~/.pi/agent/sandbox.json` globally.
22
- Project config takes precedence.
26
+ ```json
27
+ {
28
+ "packages": ["npm:pi-landstrip"]
29
+ }
30
+ ```
23
31
 
24
- See [`sandbox.json`](./sandbox.json) for a starter config.
32
+ Alternatively, drop the extension under `~/.pi/agent/extensions/` (global) or
33
+ `.pi/extensions/` (project). See the pi
34
+ [extensions](https://pi.dev/docs/latest/extensions) documentation for details.
25
35
 
26
- Use sandbox config to toggle sandboxing:
36
+ On unsupported platforms the extension loads but leaves sandboxing disabled.
37
+
38
+ ## Disable
39
+
40
+ Use the `--no-sandbox` flag, or set `enabled` to `false` in the sandbox
41
+ config:
27
42
 
28
43
  ```json
29
44
  {
@@ -31,17 +46,34 @@ Use sandbox config to toggle sandboxing:
31
46
  }
32
47
  ```
33
48
 
34
- Project config overrides global config.
35
- The `/sandbox` UI updates the project config when present, otherwise the global config.
49
+ Project config overrides global config. The `/sandbox` UI updates the project
50
+ config when present, otherwise the global config.
51
+
52
+ ## Behavior
53
+
54
+ When pi asks for a sandboxed permission, the extension emits a host
55
+ notification. After that the extension opens a dialog with the choices to allow
56
+ once, allow for the session, persist for the project, persist globally, or
57
+ reject. The dialog shows the exact path or domain being approved.
58
+
59
+ Project approvals are written to `.pi/sandbox.json`; global approvals are
60
+ written to `~/.pi/agent/sandbox.json`.
36
61
 
37
- ## Usage
62
+ When pi runs a bash command, the extension wraps it in `landstrip`. This
63
+ applies to both the AI `bash` tool calls and manually typed shell-mode commands
64
+ (`!` and `!!`). Network traffic is routed through an allowlist proxy when
65
+ network access is off, and read/write tool access is blocked outside the
66
+ configured filesystem allowlists. The default policy is strict: network access
67
+ is off unless domains are allowed, reads are limited to the project,
68
+ `~/.gitconfig`, and `/dev/null`, and writes are limited to the project and
69
+ `/dev/null`.
38
70
 
39
- Use `/sandbox` inside Pi to show the active config and toggle sandboxing.
71
+ Use `/sandbox` inside pi to show the active config and toggle sandboxing.
40
72
 
41
73
  ## License
42
74
 
43
75
  `pi-landstrip` is licensed under `MIT`. See [LICENSE](LICENSE) for more
44
76
  information.
45
77
 
46
- The bundled `@landstrip/landstrip` package is licensed under
78
+ The bundled `@landstrip/landstrip` package is licensed separately as
47
79
  `Apache-2.0 AND LGPL-2.1-or-later`.
package/index.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  // Copyright (C) Jarkko Sakkinen 2026
3
3
 
4
4
  import { spawn, spawnSync } from 'node:child_process';
5
- import { randomBytes } from 'node:crypto';
6
5
  import {
7
6
  existsSync,
8
7
  mkdirSync,
@@ -20,6 +19,7 @@ import {
20
19
  Socket as NetSocket,
21
20
  } from 'node:net';
22
21
  import { homedir, tmpdir } from 'node:os';
22
+ import { fileURLToPath } from 'node:url';
23
23
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
24
24
  import { URL } from 'node:url';
25
25
 
@@ -109,24 +109,7 @@ const LANDSTRIP_VERSION = [0, 16, 4] as const;
109
109
  const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
110
110
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
111
111
 
112
- const DEFAULT_CONFIG: SandboxConfig = {
113
- enabled: true,
114
- network: {
115
- allowNetwork: false,
116
- allowLocalBinding: false,
117
- allowAllUnixSockets: false,
118
- allowUnixSockets: [],
119
- allowedDomains: [],
120
- deniedDomains: [],
121
- },
122
- filesystem: {
123
- denyRead: ['/Users', '/home'],
124
- allowRead: ['.', '~/.gitconfig', '~/.config/git/config', '/dev/null'],
125
- allowWrite: ['.', '/dev/null'],
126
- denyWrite: ['**/.env', '**/.env.*', '**/*.pem', '**/*.key'],
127
- },
128
- };
129
-
112
+ const packageDir = dirname(fileURLToPath(import.meta.url));
130
113
  type PermissionChoice = 'abort' | 'session' | 'project' | 'global';
131
114
  type NotificationLevel = Parameters<ExtensionContext['ui']['notify']>[1];
132
115
 
@@ -161,26 +144,32 @@ function loadConfig(cwd: string): SandboxConfig {
161
144
  const projectConfigPath = join(cwd, '.pi', 'sandbox.json');
162
145
  const globalConfigPath = join(getAgentDir(), 'sandbox.json');
163
146
 
164
- let globalConfig: SandboxConfigFile = {};
165
- let projectConfig: SandboxConfigFile = {};
147
+ if (!existsSync(globalConfigPath)) {
148
+ const templatePath = join(packageDir, 'sandbox.json');
149
+ mkdirSync(dirname(globalConfigPath), { recursive: true });
150
+ writeFileSync(globalConfigPath, readFileSync(templatePath, 'utf-8'), 'utf-8');
151
+ }
166
152
 
167
- if (existsSync(globalConfigPath)) {
168
- try {
169
- globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf-8'));
170
- } catch (error) {
171
- console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
172
- }
153
+ let globalConfig: SandboxConfig = JSON.parse(
154
+ readFileSync(join(packageDir, 'sandbox.json'), 'utf-8'),
155
+ );
156
+ try {
157
+ const override = JSON.parse(readFileSync(globalConfigPath, 'utf-8'));
158
+ globalConfig = deepMerge(globalConfig, override);
159
+ } catch (error) {
160
+ console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
173
161
  }
174
162
 
175
163
  if (existsSync(projectConfigPath)) {
176
164
  try {
177
- projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf-8'));
165
+ const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf-8'));
166
+ return deepMerge(globalConfig, projectConfig);
178
167
  } catch (error) {
179
168
  console.error(`Warning: Could not parse ${projectConfigPath}: ${error}`);
180
169
  }
181
170
  }
182
171
 
183
- return deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig);
172
+ return globalConfig;
184
173
  }
185
174
 
186
175
  function mergeArray(base: string[], override?: string[]): string[] {
@@ -1154,27 +1143,22 @@ export function createLandstripIntegration(
1154
1143
 
1155
1144
  function createSocketPair(): Promise<[NetSocket, NetSocket]> {
1156
1145
  return new Promise((resolve, reject) => {
1157
- const sockPath = join(tmpdir(), `.landstrip-sock-${randomBytes(8).toString('hex')}`);
1158
1146
  const server = createServer();
1159
1147
  server.on('error', reject);
1160
1148
  let client: NetSocket | null = null;
1161
1149
  server.on('connection', (serverEnd) => {
1162
1150
  server.removeListener('error', reject);
1163
1151
  server.close();
1164
- try {
1165
- rmSync(sockPath, { force: true });
1166
- } catch {
1167
- /* ok */
1168
- }
1169
1152
  if (client) {
1170
1153
  client.removeListener('error', reject);
1171
1154
  resolve([client, serverEnd]);
1172
1155
  }
1173
1156
  });
1174
- server.listen(sockPath, () => {
1157
+ server.listen(0, '127.0.0.1', () => {
1158
+ const addr = server.address() as AddressInfo;
1175
1159
  client = new NetSocket();
1176
1160
  client.on('error', reject);
1177
- client.connect(sockPath);
1161
+ client.connect(addr.port, '127.0.0.1');
1178
1162
  });
1179
1163
  });
1180
1164
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.16.7",
3
+ "version": "0.16.9",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",
package/sandbox.json CHANGED
@@ -12,6 +12,13 @@
12
12
  "denyRead": ["/Users", "/home"],
13
13
  "allowRead": [".", "~/.gitconfig", "~/.config/git/config", "/dev/null"],
14
14
  "allowWrite": [".", "/dev/null"],
15
- "denyWrite": ["**/.env", "**/.env.*", "**/*.pem", "**/*.key"]
15
+ "denyWrite": [
16
+ "**/.env",
17
+ "**/.env.*",
18
+ "**/*.pem",
19
+ "**/*.key",
20
+ ".pi/sandbox.json",
21
+ "~/.pi/agent/sandbox.json"
22
+ ]
16
23
  }
17
24
  }