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 +5 -11
- package/index.ts +75 -32
- package/package.json +3 -2
- package/sandbox.json +33 -2
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
|
-
|
|
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(
|
|
476
|
-
const result = spawnSync(
|
|
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(
|
|
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(
|
|
999
|
+
const version = landstripVersion();
|
|
960
1000
|
if (!version) {
|
|
961
1001
|
sandboxEnabled = false;
|
|
962
1002
|
sandboxReady = false;
|
|
963
|
-
ctx.ui.notify(
|
|
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.
|
|
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: ${
|
|
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.
|
|
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": [
|
|
22
|
-
|
|
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
|
}
|