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 +48 -16
- package/index.ts +21 -37
- package/package.json +1 -1
- package/sandbox.json +8 -1
package/README.md
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
# pi-landstrip
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
##
|
|
10
|
+
## Installing the extension
|
|
9
11
|
|
|
10
|
-
|
|
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
|
-
|
|
21
|
+
### Manual install
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
Add the extension to `~/.pi/agent/settings.json` (global) or
|
|
24
|
+
`.pi/settings.json` (project):
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"packages": ["npm:pi-landstrip"]
|
|
29
|
+
}
|
|
30
|
+
```
|
|
23
31
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1161
|
+
client.connect(addr.port, '127.0.0.1');
|
|
1178
1162
|
});
|
|
1179
1163
|
});
|
|
1180
1164
|
}
|
package/package.json
CHANGED
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": [
|
|
15
|
+
"denyWrite": [
|
|
16
|
+
"**/.env",
|
|
17
|
+
"**/.env.*",
|
|
18
|
+
"**/*.pem",
|
|
19
|
+
"**/*.key",
|
|
20
|
+
".pi/sandbox.json",
|
|
21
|
+
"~/.pi/agent/sandbox.json"
|
|
22
|
+
]
|
|
16
23
|
}
|
|
17
24
|
}
|