pi-landstrip 0.18.14 → 0.18.16
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 +18 -6
- package/api.ts +105 -44
- package/dist/index.ts +163 -46
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -168,18 +168,30 @@ Remove it with `npx @landstrip/landstrip windows uninstall`.
|
|
|
168
168
|
|
|
169
169
|
## Plugin API
|
|
170
170
|
|
|
171
|
-
Other Pi extensions can discover the runtime with `useLandstrip` from
|
|
172
|
-
`pi-landstrip/api`. The
|
|
171
|
+
Other Pi extensions can discover the v2 runtime with `useLandstrip` from
|
|
172
|
+
`pi-landstrip/api`. The runtime provides:
|
|
173
173
|
|
|
174
174
|
- `getContext()` for sandbox and task context.
|
|
175
|
-
- `
|
|
175
|
+
- `registerShellProvider()` for replacing POSIX shell invocation preparation.
|
|
176
176
|
- `prepareProcess()` for one policy-aware process launch.
|
|
177
177
|
- `registerWorkerExtension()` for trusted worker extensions.
|
|
178
178
|
- `on()` for sandbox and subagent lifecycle events.
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
A shell provider receives the command, working directory, composed command
|
|
181
|
+
environment, and abort signal. It returns an executable, arguments, minimal
|
|
182
|
+
launcher environment, required read paths, and optional cleanup. Landstrip adds
|
|
183
|
+
platform variables required by its launcher, such as Windows `ProgramData`.
|
|
184
|
+
Providers should pass the composed environment through a private temporary file,
|
|
185
|
+
include that file in `readPaths`, and remove it in `dispose()`; do not copy secrets
|
|
186
|
+
into `launcherEnv`. Landstrip remains responsible for starting the process, applying
|
|
187
|
+
policy, proxying network access, handling traps, and prompting for additional
|
|
188
|
+
access. Only one external provider can be active; disposing it restores the
|
|
189
|
+
built-in POSIX provider.
|
|
190
|
+
|
|
191
|
+
Use `provideLandstripShell()` to register a provider independently of extension
|
|
192
|
+
load order. Shell providers and worker extensions are trusted code. Workers also
|
|
193
|
+
receive an informational `LANDSTRIP_CONTEXT` environment value; it is not proof
|
|
194
|
+
of authorization.
|
|
183
195
|
|
|
184
196
|
## Limits
|
|
185
197
|
|
package/api.ts
CHANGED
|
@@ -2,26 +2,20 @@
|
|
|
2
2
|
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
3
|
|
|
4
4
|
import type { ChildProcess, SpawnOptions } from 'node:child_process';
|
|
5
|
+
import type { Readable, Writable } from 'node:stream';
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
createBashToolDefinition,
|
|
8
|
-
type ExtensionAPI,
|
|
9
|
-
type ExtensionContext,
|
|
10
|
-
} from '@earendil-works/pi-coding-agent';
|
|
11
|
-
|
|
12
|
-
import type { RpcChildProcess } from './rpc-process.ts';
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
13
8
|
|
|
14
9
|
export const LANDSTRIP_CONTEXT_ENV = 'LANDSTRIP_CONTEXT';
|
|
15
|
-
export const LANDSTRIP_RUNTIME_VERSION =
|
|
10
|
+
export const LANDSTRIP_RUNTIME_VERSION = 2;
|
|
16
11
|
|
|
17
|
-
const RUNTIME_REGISTER_EVENT = 'landstrip:runtime:register:
|
|
18
|
-
const RUNTIME_DISCOVER_EVENT = 'landstrip:runtime:discover:
|
|
12
|
+
const RUNTIME_REGISTER_EVENT = 'landstrip:runtime:register:v2';
|
|
13
|
+
const RUNTIME_DISCOVER_EVENT = 'landstrip:runtime:discover:v2';
|
|
19
14
|
|
|
20
15
|
export type LandstripSandboxState = 'enabled' | 'disabled' | 'unavailable';
|
|
21
|
-
export type LandstripBashTool = ReturnType<typeof createBashToolDefinition>;
|
|
22
16
|
|
|
23
|
-
export interface
|
|
24
|
-
readonly version:
|
|
17
|
+
export interface LandstripContextV2 {
|
|
18
|
+
readonly version: 2;
|
|
25
19
|
readonly host: 'pi';
|
|
26
20
|
readonly role: 'primary' | 'subagent';
|
|
27
21
|
readonly sandbox: LandstripSandboxState;
|
|
@@ -38,6 +32,30 @@ export interface LandstripWorkerExtension {
|
|
|
38
32
|
readonly entry: string;
|
|
39
33
|
}
|
|
40
34
|
|
|
35
|
+
export interface LandstripShellPrepareOptions {
|
|
36
|
+
readonly command: string;
|
|
37
|
+
readonly cwd: string;
|
|
38
|
+
/** Composed command environment; transport it without copying secrets into launcherEnv. */
|
|
39
|
+
readonly env: NodeJS.ProcessEnv;
|
|
40
|
+
readonly signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LandstripShellInvocation {
|
|
44
|
+
readonly executable: string;
|
|
45
|
+
readonly args: readonly string[];
|
|
46
|
+
/** Minimal environment for the Landstrip launcher and shell bootstrap process. */
|
|
47
|
+
readonly launcherEnv: NodeJS.ProcessEnv;
|
|
48
|
+
readonly readPaths?: readonly string[];
|
|
49
|
+
dispose?(): void | Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LandstripShellProvider {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
prepare(
|
|
55
|
+
options: LandstripShellPrepareOptions,
|
|
56
|
+
): LandstripShellInvocation | Promise<LandstripShellInvocation>;
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
export interface LandstripProcessOptions {
|
|
42
60
|
readonly command: string;
|
|
43
61
|
readonly args: readonly string[];
|
|
@@ -58,25 +76,42 @@ export interface LandstripPreparedProcess {
|
|
|
58
76
|
dispose(): Promise<void>;
|
|
59
77
|
}
|
|
60
78
|
|
|
79
|
+
export interface LandstripRpcChildProcess {
|
|
80
|
+
readonly stdin: Writable;
|
|
81
|
+
readonly stdout: Readable;
|
|
82
|
+
readonly stderr: Readable;
|
|
83
|
+
readonly exitCode: number | null;
|
|
84
|
+
readonly signalCode: NodeJS.Signals | null;
|
|
85
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
86
|
+
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
87
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
88
|
+
once(event: 'spawn', listener: () => void): this;
|
|
89
|
+
once(event: 'error', listener: (error: Error) => void): this;
|
|
90
|
+
once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
91
|
+
off(event: 'spawn', listener: () => void): this;
|
|
92
|
+
off(event: 'error', listener: (error: Error) => void): this;
|
|
93
|
+
off(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
export type LandstripSpawn = (
|
|
62
97
|
command: string,
|
|
63
98
|
args: readonly string[],
|
|
64
99
|
options: SpawnOptions,
|
|
65
|
-
) => ChildProcess |
|
|
100
|
+
) => ChildProcess | LandstripRpcChildProcess;
|
|
66
101
|
|
|
67
102
|
export type LandstripEvent =
|
|
68
|
-
| { readonly type: 'sandbox.changed'; readonly context:
|
|
69
|
-
| { readonly type: 'subagent.start'; readonly context:
|
|
103
|
+
| { readonly type: 'sandbox.changed'; readonly context: LandstripContextV2 }
|
|
104
|
+
| { readonly type: 'subagent.start'; readonly context: LandstripContextV2 }
|
|
70
105
|
| {
|
|
71
106
|
readonly type: 'subagent.end';
|
|
72
|
-
readonly context:
|
|
107
|
+
readonly context: LandstripContextV2;
|
|
73
108
|
readonly status: 'completed' | 'cancelled' | 'error';
|
|
74
109
|
};
|
|
75
110
|
|
|
76
|
-
export interface
|
|
77
|
-
readonly version:
|
|
78
|
-
getContext(ctx?: ExtensionContext):
|
|
79
|
-
|
|
111
|
+
export interface PiLandstripRuntimeV2 {
|
|
112
|
+
readonly version: 2;
|
|
113
|
+
getContext(ctx?: ExtensionContext): LandstripContextV2;
|
|
114
|
+
registerShellProvider(provider: LandstripShellProvider): () => void;
|
|
80
115
|
prepareProcess(options: LandstripProcessOptions): Promise<LandstripPreparedProcess>;
|
|
81
116
|
registerWorkerExtension(extension: LandstripWorkerExtension): () => void;
|
|
82
117
|
getWorkerExtensions(): readonly LandstripWorkerExtension[];
|
|
@@ -87,13 +122,13 @@ export interface PiLandstripRuntimeV1 {
|
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
interface RuntimeRegistration {
|
|
90
|
-
readonly version:
|
|
91
|
-
readonly runtime:
|
|
125
|
+
readonly version: 2;
|
|
126
|
+
readonly runtime: PiLandstripRuntimeV2;
|
|
92
127
|
}
|
|
93
128
|
|
|
94
129
|
interface RuntimeDiscovery {
|
|
95
|
-
readonly version:
|
|
96
|
-
register(runtime:
|
|
130
|
+
readonly version: 2;
|
|
131
|
+
register(runtime: PiLandstripRuntimeV2): void;
|
|
97
132
|
}
|
|
98
133
|
|
|
99
134
|
interface EventBusLike {
|
|
@@ -109,12 +144,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
109
144
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
110
145
|
}
|
|
111
146
|
|
|
112
|
-
function isRuntime(value: unknown): value is
|
|
147
|
+
function isRuntime(value: unknown): value is PiLandstripRuntimeV2 {
|
|
113
148
|
return (
|
|
114
149
|
isRecord(value) &&
|
|
115
150
|
value.version === LANDSTRIP_RUNTIME_VERSION &&
|
|
116
151
|
typeof value.getContext === 'function' &&
|
|
117
|
-
typeof value.
|
|
152
|
+
typeof value.registerShellProvider === 'function' &&
|
|
118
153
|
typeof value.prepareProcess === 'function' &&
|
|
119
154
|
typeof value.registerWorkerExtension === 'function' &&
|
|
120
155
|
typeof value.getWorkerExtensions === 'function' &&
|
|
@@ -124,13 +159,13 @@ function isRuntime(value: unknown): value is PiLandstripRuntimeV1 {
|
|
|
124
159
|
|
|
125
160
|
export function useLandstrip(
|
|
126
161
|
pi: ExtensionAPI,
|
|
127
|
-
callback: (runtime:
|
|
162
|
+
callback: (runtime: PiLandstripRuntimeV2) => void,
|
|
128
163
|
): () => void {
|
|
129
164
|
const events = eventBus(pi);
|
|
130
165
|
if (!events) return () => undefined;
|
|
131
166
|
|
|
132
|
-
let current:
|
|
133
|
-
const register = (runtime:
|
|
167
|
+
let current: PiLandstripRuntimeV2 | undefined;
|
|
168
|
+
const register = (runtime: PiLandstripRuntimeV2): void => {
|
|
134
169
|
if (runtime === current) return;
|
|
135
170
|
current = runtime;
|
|
136
171
|
callback(runtime);
|
|
@@ -139,16 +174,37 @@ export function useLandstrip(
|
|
|
139
174
|
if (!isRecord(value) || value.version !== LANDSTRIP_RUNTIME_VERSION) return;
|
|
140
175
|
if (isRuntime(value.runtime)) register(value.runtime);
|
|
141
176
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
try {
|
|
178
|
+
events.emit(RUNTIME_DISCOVER_EVENT, {
|
|
179
|
+
version: LANDSTRIP_RUNTIME_VERSION,
|
|
180
|
+
register,
|
|
181
|
+
} satisfies RuntimeDiscovery);
|
|
182
|
+
return unsubscribe;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
unsubscribe();
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function provideLandstripShell(
|
|
190
|
+
pi: ExtensionAPI,
|
|
191
|
+
provider: LandstripShellProvider,
|
|
192
|
+
): () => void {
|
|
193
|
+
let unregisterProvider: (() => void) | undefined;
|
|
194
|
+
const stopDiscovery = useLandstrip(pi, (runtime) => {
|
|
195
|
+
const unregisterNext = runtime.registerShellProvider(provider);
|
|
196
|
+
unregisterProvider?.();
|
|
197
|
+
unregisterProvider = unregisterNext;
|
|
198
|
+
});
|
|
199
|
+
return () => {
|
|
200
|
+
stopDiscovery();
|
|
201
|
+
unregisterProvider?.();
|
|
202
|
+
};
|
|
147
203
|
}
|
|
148
204
|
|
|
149
205
|
export function publishLandstripRuntime(
|
|
150
206
|
pi: ExtensionAPI,
|
|
151
|
-
runtime:
|
|
207
|
+
runtime: PiLandstripRuntimeV2,
|
|
152
208
|
): () => void {
|
|
153
209
|
const events = eventBus(pi);
|
|
154
210
|
if (!events) return () => undefined;
|
|
@@ -157,20 +213,25 @@ export function publishLandstripRuntime(
|
|
|
157
213
|
if (!isRecord(value) || value.version !== LANDSTRIP_RUNTIME_VERSION) return;
|
|
158
214
|
if (typeof value.register === 'function') value.register(runtime);
|
|
159
215
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
216
|
+
try {
|
|
217
|
+
events.emit(RUNTIME_REGISTER_EVENT, {
|
|
218
|
+
version: LANDSTRIP_RUNTIME_VERSION,
|
|
219
|
+
runtime,
|
|
220
|
+
} satisfies RuntimeRegistration);
|
|
221
|
+
return unsubscribe;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
unsubscribe();
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
165
226
|
}
|
|
166
227
|
|
|
167
|
-
export function encodeLandstripContext(context:
|
|
228
|
+
export function encodeLandstripContext(context: LandstripContextV2): string {
|
|
168
229
|
return Buffer.from(JSON.stringify(context)).toString('base64url');
|
|
169
230
|
}
|
|
170
231
|
|
|
171
232
|
export function contextFromEnvironment(
|
|
172
233
|
env: NodeJS.ProcessEnv = process.env,
|
|
173
|
-
):
|
|
234
|
+
): LandstripContextV2 | undefined {
|
|
174
235
|
const encoded = env[LANDSTRIP_CONTEXT_ENV];
|
|
175
236
|
if (!encoded) return undefined;
|
|
176
237
|
|
|
@@ -191,7 +252,7 @@ export function contextFromEnvironment(
|
|
|
191
252
|
) {
|
|
192
253
|
return undefined;
|
|
193
254
|
}
|
|
194
|
-
return value as unknown as
|
|
255
|
+
return value as unknown as LandstripContextV2;
|
|
195
256
|
} catch {
|
|
196
257
|
return undefined;
|
|
197
258
|
}
|
package/dist/index.ts
CHANGED
|
@@ -36,9 +36,9 @@ import {
|
|
|
36
36
|
|
|
37
37
|
// api.ts
|
|
38
38
|
var LANDSTRIP_CONTEXT_ENV = "LANDSTRIP_CONTEXT";
|
|
39
|
-
var LANDSTRIP_RUNTIME_VERSION =
|
|
40
|
-
var RUNTIME_REGISTER_EVENT = "landstrip:runtime:register:
|
|
41
|
-
var RUNTIME_DISCOVER_EVENT = "landstrip:runtime:discover:
|
|
39
|
+
var LANDSTRIP_RUNTIME_VERSION = 2;
|
|
40
|
+
var RUNTIME_REGISTER_EVENT = "landstrip:runtime:register:v2";
|
|
41
|
+
var RUNTIME_DISCOVER_EVENT = "landstrip:runtime:discover:v2";
|
|
42
42
|
function eventBus(pi) {
|
|
43
43
|
return pi.events;
|
|
44
44
|
}
|
|
@@ -57,11 +57,16 @@ function publishLandstripRuntime(pi, runtime) {
|
|
|
57
57
|
if (typeof value.register === "function")
|
|
58
58
|
value.register(runtime);
|
|
59
59
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
try {
|
|
61
|
+
events.emit(RUNTIME_REGISTER_EVENT, {
|
|
62
|
+
version: LANDSTRIP_RUNTIME_VERSION,
|
|
63
|
+
runtime
|
|
64
|
+
});
|
|
65
|
+
return unsubscribe;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
unsubscribe();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
65
70
|
}
|
|
66
71
|
function encodeLandstripContext(context) {
|
|
67
72
|
return Buffer.from(JSON.stringify(context)).toString("base64url");
|
|
@@ -3510,7 +3515,7 @@ ${catalog.diagnostics.join(`
|
|
|
3510
3515
|
}
|
|
3511
3516
|
taskContext(task, ctx) {
|
|
3512
3517
|
const context = this.integration.getContext?.(ctx) ?? {
|
|
3513
|
-
version:
|
|
3518
|
+
version: 2,
|
|
3514
3519
|
host: "pi",
|
|
3515
3520
|
role: "primary",
|
|
3516
3521
|
sandbox: "unavailable",
|
|
@@ -4275,7 +4280,7 @@ function extractNativeDeniedPath(output, cwd) {
|
|
|
4275
4280
|
}
|
|
4276
4281
|
function extractNativeWriteDeniedPath(output, cwd) {
|
|
4277
4282
|
const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
|
|
4278
|
-
let match = output.match(new RegExp(String.raw`(?:[Uu]nable to create|cannot (?:create|touch|mkdir|remove|unlink|rename)|for writing)[^'"\n]*['"]([^'"\n]+)['"][^\r\n]{0,120}${denial}`, "im"));
|
|
4283
|
+
let match = output.match(new RegExp(String.raw`(?:[Uu]nable to create|failed to create(?: directory)?|cannot (?:create|touch|mkdir|remove|unlink|rename)|for writing)[^'"\x60\r\n]*['"\x60]([^'"\x60\r\n]+)['"\x60](?:[^\r\n]*\r?\n){0,3}[^\r\n]{0,120}${denial}`, "im"));
|
|
4279
4284
|
if (match)
|
|
4280
4285
|
return normalizePathMatch(match[1], cwd);
|
|
4281
4286
|
match = output.match(new RegExp(String.raw`^[a-zA-Z0-9_-]+: cannot create(?: directory)? '?([^'\n]+?)'?(?: for writing)?: ${denial}$`, "im"));
|
|
@@ -4488,6 +4493,51 @@ function writeEnvFile(env, proxyPort, proxyToken) {
|
|
|
4488
4493
|
`), "utf-8");
|
|
4489
4494
|
return { dir, path };
|
|
4490
4495
|
}
|
|
4496
|
+
var POSIX_SHELL_PROVIDER_ID = "posix";
|
|
4497
|
+
function createPosixShellProvider() {
|
|
4498
|
+
return {
|
|
4499
|
+
id: POSIX_SHELL_PROVIDER_ID,
|
|
4500
|
+
prepare(options) {
|
|
4501
|
+
if (options.signal?.aborted)
|
|
4502
|
+
throw new Error("aborted");
|
|
4503
|
+
const { shell, args } = getShellConfig(SettingsManager.create(options.cwd).getShellPath());
|
|
4504
|
+
const envFile = writeEnvFile(options.env, null);
|
|
4505
|
+
let disposed = false;
|
|
4506
|
+
return {
|
|
4507
|
+
executable: shell,
|
|
4508
|
+
args: [...args, `source '${envFile.path}' && ${options.command}`],
|
|
4509
|
+
launcherEnv: { PATH: process.env.PATH, HOME: process.env.HOME },
|
|
4510
|
+
readPaths: [envFile.path],
|
|
4511
|
+
dispose() {
|
|
4512
|
+
if (disposed)
|
|
4513
|
+
return;
|
|
4514
|
+
disposed = true;
|
|
4515
|
+
rmSync2(envFile.dir, { recursive: true, force: true });
|
|
4516
|
+
}
|
|
4517
|
+
};
|
|
4518
|
+
}
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
function shellEnvironment(env, cwd, proxyPort, proxyToken) {
|
|
4522
|
+
const result = { ...process.env, ...env, PWD: resolve3(cwd) };
|
|
4523
|
+
if (proxyPort === null)
|
|
4524
|
+
return result;
|
|
4525
|
+
const credentials = proxyToken === undefined ? "" : `landstrip:${proxyToken}@`;
|
|
4526
|
+
const url = `http://${credentials}127.0.0.1:${proxyPort}`;
|
|
4527
|
+
for (const name of PROXY_ENVIRONMENT_VARIABLES)
|
|
4528
|
+
result[name] = url;
|
|
4529
|
+
result.NO_PROXY = "";
|
|
4530
|
+
result.no_proxy = "";
|
|
4531
|
+
return result;
|
|
4532
|
+
}
|
|
4533
|
+
function createLandstripLauncherEnvironment(providerEnv, hostEnv = process.env, platform = process.platform) {
|
|
4534
|
+
if (platform !== "win32" || hostEnv.ProgramData === undefined)
|
|
4535
|
+
return providerEnv;
|
|
4536
|
+
return { ...providerEnv, ProgramData: hostEnv.ProgramData };
|
|
4537
|
+
}
|
|
4538
|
+
function createWindowsDenyRead(denyRead, volumeRoot) {
|
|
4539
|
+
return [...new Set([...denyRead.filter((path) => !path.startsWith("/")), volumeRoot])];
|
|
4540
|
+
}
|
|
4491
4541
|
function parseProxyPort(value, defaultPort) {
|
|
4492
4542
|
const rawPort = value ?? String(defaultPort);
|
|
4493
4543
|
if (!/^\d+$/.test(rawPort))
|
|
@@ -4576,6 +4626,8 @@ function createLandstripIntegration(options = {}) {
|
|
|
4576
4626
|
let unsandboxedWorkerWarningShown = false;
|
|
4577
4627
|
let activeContext;
|
|
4578
4628
|
let unpublishRuntime;
|
|
4629
|
+
const defaultShellProvider = createPosixShellProvider();
|
|
4630
|
+
let externalShellProvider;
|
|
4579
4631
|
const eventHandlers = new Set;
|
|
4580
4632
|
const workerExtensions = new Map;
|
|
4581
4633
|
const sessionAllowedDomains = [];
|
|
@@ -4617,6 +4669,29 @@ function createLandstripIntegration(options = {}) {
|
|
|
4617
4669
|
eventHandlers.add(listener);
|
|
4618
4670
|
return () => eventHandlers.delete(listener);
|
|
4619
4671
|
}
|
|
4672
|
+
function registerShellProvider(provider) {
|
|
4673
|
+
const id = provider.id.trim();
|
|
4674
|
+
if (!id)
|
|
4675
|
+
throw new Error("Shell provider id must not be empty");
|
|
4676
|
+
if (id === POSIX_SHELL_PROVIDER_ID) {
|
|
4677
|
+
throw new Error(`Shell provider id "${POSIX_SHELL_PROVIDER_ID}" is reserved`);
|
|
4678
|
+
}
|
|
4679
|
+
if (externalShellProvider) {
|
|
4680
|
+
throw new Error(`Shell provider "${externalShellProvider.id}" is already registered; cannot register "${id}"`);
|
|
4681
|
+
}
|
|
4682
|
+
externalShellProvider = provider;
|
|
4683
|
+
let disposed = false;
|
|
4684
|
+
return () => {
|
|
4685
|
+
if (disposed)
|
|
4686
|
+
return;
|
|
4687
|
+
disposed = true;
|
|
4688
|
+
if (externalShellProvider === provider)
|
|
4689
|
+
externalShellProvider = undefined;
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
function activeShellProvider() {
|
|
4693
|
+
return externalShellProvider ?? defaultShellProvider;
|
|
4694
|
+
}
|
|
4620
4695
|
function registerWorkerExtension(extension) {
|
|
4621
4696
|
const id = extension.id.trim();
|
|
4622
4697
|
if (!id)
|
|
@@ -4677,7 +4752,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
4677
4752
|
if (process.platform !== "win32")
|
|
4678
4753
|
return config.filesystem.denyRead;
|
|
4679
4754
|
const volumeRoot = parse(resolve3(cwd)).root;
|
|
4680
|
-
return
|
|
4755
|
+
return createWindowsDenyRead(config.filesystem.denyRead, volumeRoot);
|
|
4681
4756
|
}
|
|
4682
4757
|
function hasBaselineWindowsApplicationAccess(filePath) {
|
|
4683
4758
|
const normalizedPath = normalizePathSeparators(resolve3(filePath)).toLowerCase();
|
|
@@ -4786,13 +4861,13 @@ function createLandstripIntegration(options = {}) {
|
|
|
4786
4861
|
`, "utf-8");
|
|
4787
4862
|
return { dir, path };
|
|
4788
4863
|
}
|
|
4789
|
-
function startProxy(ctx, allowances, signal, authorization, portRange) {
|
|
4864
|
+
function startProxy(ctx, cwd, allowances, signal, authorization, portRange) {
|
|
4790
4865
|
const sockets = new Set;
|
|
4791
4866
|
async function domainAllowed(domain) {
|
|
4792
|
-
const config = loadConfig(
|
|
4867
|
+
const config = loadConfig(cwd);
|
|
4793
4868
|
if (domainMatchesAny(domain, config.network.deniedDomains))
|
|
4794
4869
|
return false;
|
|
4795
|
-
return ensureDomainAllowed(ctx, domain,
|
|
4870
|
+
return ensureDomainAllowed(ctx, domain, cwd, allowances, signal);
|
|
4796
4871
|
}
|
|
4797
4872
|
function trackUpstream(upstream, client, settled) {
|
|
4798
4873
|
sockets.add(upstream);
|
|
@@ -5120,7 +5195,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
5120
5195
|
const config = loadConfig(options2.cwd);
|
|
5121
5196
|
const proxyToken = randomBytes(32).toString("base64url");
|
|
5122
5197
|
const proxyAuthorization = `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString("base64")}`;
|
|
5123
|
-
const proxy = shouldStartProxy(config) ? await startProxy(options2.ctx, processAllowances, options2.signal, proxyAuthorization, restrictedUserProxyPortRange()) : null;
|
|
5198
|
+
const proxy = shouldStartProxy(config) ? await startProxy(options2.ctx, options2.cwd, processAllowances, options2.signal, proxyAuthorization, restrictedUserProxyPortRange()) : null;
|
|
5124
5199
|
let policy;
|
|
5125
5200
|
let trapSocket;
|
|
5126
5201
|
let childEnd;
|
|
@@ -5222,32 +5297,55 @@ function createLandstripIntegration(options = {}) {
|
|
|
5222
5297
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
5223
5298
|
if (!existsSync5(cwd))
|
|
5224
5299
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
5225
|
-
const
|
|
5300
|
+
const provider = activeShellProvider();
|
|
5226
5301
|
const config = loadConfig(cwd);
|
|
5227
5302
|
const proxyToken = randomBytes(32).toString("base64url");
|
|
5228
5303
|
const proxyAuthorization = `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString("base64")}`;
|
|
5229
|
-
const proxy = shouldStartProxy(config) ? await startProxy(ctx, allowances, signal, proxyAuthorization, restrictedUserProxyPortRange()) : null;
|
|
5304
|
+
const proxy = shouldStartProxy(config) ? await startProxy(ctx, cwd, allowances, signal, proxyAuthorization, restrictedUserProxyPortRange()) : null;
|
|
5230
5305
|
let policy;
|
|
5231
|
-
let
|
|
5306
|
+
let invocation;
|
|
5232
5307
|
let processAllowances = allowances;
|
|
5233
5308
|
let teardownPromise;
|
|
5234
5309
|
const teardownResources = () => {
|
|
5235
|
-
teardownPromise ??= (
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5310
|
+
teardownPromise ??= Promise.all([
|
|
5311
|
+
Promise.resolve().then(() => invocation?.dispose?.()),
|
|
5312
|
+
Promise.resolve().then(() => {
|
|
5313
|
+
if (policy)
|
|
5314
|
+
rmSync2(policy.dir, { recursive: true, force: true });
|
|
5315
|
+
}),
|
|
5316
|
+
Promise.resolve().then(() => proxy?.stop())
|
|
5317
|
+
]).then(() => {
|
|
5318
|
+
return;
|
|
5319
|
+
});
|
|
5242
5320
|
return teardownPromise;
|
|
5243
5321
|
};
|
|
5244
5322
|
let landstripArgs;
|
|
5245
5323
|
try {
|
|
5246
|
-
|
|
5247
|
-
|
|
5324
|
+
invocation = await provider.prepare({
|
|
5325
|
+
command,
|
|
5326
|
+
cwd,
|
|
5327
|
+
env: shellEnvironment(env, cwd, proxy?.port ?? null, proxy ? proxyToken : undefined),
|
|
5328
|
+
signal
|
|
5329
|
+
});
|
|
5330
|
+
if (!invocation.executable.trim()) {
|
|
5331
|
+
throw new Error(`Shell provider "${provider.id}" returned an empty executable`);
|
|
5332
|
+
}
|
|
5333
|
+
if (signal?.aborted)
|
|
5334
|
+
throw new Error("aborted");
|
|
5335
|
+
processAllowances = {
|
|
5336
|
+
...allowances,
|
|
5337
|
+
readPaths: [...allowances.readPaths, ...invocation.readPaths ?? []]
|
|
5338
|
+
};
|
|
5339
|
+
processAllowances = withWindowsProcessReadAccess(processAllowances, invocation.executable, cwd);
|
|
5248
5340
|
policy = writePolicyFile(cwd, proxy?.port ?? null, processAllowances);
|
|
5249
|
-
|
|
5250
|
-
|
|
5341
|
+
landstripArgs = [
|
|
5342
|
+
"run",
|
|
5343
|
+
"-p",
|
|
5344
|
+
policy.path,
|
|
5345
|
+
"--",
|
|
5346
|
+
invocation.executable,
|
|
5347
|
+
...invocation.args
|
|
5348
|
+
];
|
|
5251
5349
|
if (process.platform !== "win32")
|
|
5252
5350
|
landstripArgs.splice(1, 0, "--trap-fd", "3");
|
|
5253
5351
|
} catch (error) {
|
|
@@ -5255,14 +5353,19 @@ function createLandstripIntegration(options = {}) {
|
|
|
5255
5353
|
throw error;
|
|
5256
5354
|
}
|
|
5257
5355
|
return new Promise((resolvePromise, reject) => {
|
|
5356
|
+
let pendingSocketPair;
|
|
5258
5357
|
(async () => {
|
|
5259
5358
|
let timeoutHandle;
|
|
5260
5359
|
let timedOut = false;
|
|
5261
5360
|
let cleaned = false;
|
|
5262
5361
|
let trapSocket;
|
|
5263
5362
|
let childEnd;
|
|
5264
|
-
if (process.platform !== "win32")
|
|
5265
|
-
|
|
5363
|
+
if (process.platform !== "win32") {
|
|
5364
|
+
pendingSocketPair = await createSocketPair();
|
|
5365
|
+
[trapSocket, childEnd] = pendingSocketPair;
|
|
5366
|
+
}
|
|
5367
|
+
if (signal?.aborted)
|
|
5368
|
+
throw new Error("aborted");
|
|
5266
5369
|
const cleanup = async () => {
|
|
5267
5370
|
if (cleaned)
|
|
5268
5371
|
return;
|
|
@@ -5276,12 +5379,13 @@ function createLandstripIntegration(options = {}) {
|
|
|
5276
5379
|
const stdio = childEnd ? ["ignore", "pipe", "pipe", childEnd] : ["ignore", "pipe", "pipe"];
|
|
5277
5380
|
const child = spawn2(binaryPath(), landstripArgs, {
|
|
5278
5381
|
cwd,
|
|
5279
|
-
env:
|
|
5382
|
+
env: createLandstripLauncherEnvironment(invocation.launcherEnv),
|
|
5280
5383
|
detached: true,
|
|
5281
5384
|
stdio
|
|
5282
5385
|
});
|
|
5283
5386
|
patchProcessGroupKill(child);
|
|
5284
5387
|
childEnd?.destroy();
|
|
5388
|
+
pendingSocketPair = undefined;
|
|
5285
5389
|
function killChild() {
|
|
5286
5390
|
child.kill("SIGKILL");
|
|
5287
5391
|
}
|
|
@@ -5295,6 +5399,8 @@ function createLandstripIntegration(options = {}) {
|
|
|
5295
5399
|
}, timeout * 1000);
|
|
5296
5400
|
}
|
|
5297
5401
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
5402
|
+
if (signal?.aborted)
|
|
5403
|
+
onAbort();
|
|
5298
5404
|
let stderrAcc = "";
|
|
5299
5405
|
let errorFdAcc = "";
|
|
5300
5406
|
let execSettled = false;
|
|
@@ -5419,9 +5525,9 @@ function createLandstripIntegration(options = {}) {
|
|
|
5419
5525
|
}
|
|
5420
5526
|
});
|
|
5421
5527
|
child.once("close", (code) => finalizeExec(code));
|
|
5422
|
-
})().catch(
|
|
5423
|
-
|
|
5424
|
-
reject(error);
|
|
5528
|
+
})().catch((error) => {
|
|
5529
|
+
pendingSocketPair?.forEach((socket) => socket.destroy());
|
|
5530
|
+
teardownResources().then(() => reject(error), (cleanupError) => reject(new AggregateError([error, cleanupError], "Shell setup and cleanup failed")));
|
|
5425
5531
|
});
|
|
5426
5532
|
});
|
|
5427
5533
|
}
|
|
@@ -5662,19 +5768,22 @@ ${message}
|
|
|
5662
5768
|
return enableSandbox(ctx);
|
|
5663
5769
|
return true;
|
|
5664
5770
|
}
|
|
5665
|
-
function createBashTool(cwd
|
|
5771
|
+
function createBashTool(cwd) {
|
|
5666
5772
|
const localBash = createPlainBashTool(cwd);
|
|
5667
5773
|
return {
|
|
5668
5774
|
...localBash,
|
|
5669
5775
|
label: "bash (landstrip)",
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
if (!
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5776
|
+
description: "Execute commands with the active Landstrip shell provider.",
|
|
5777
|
+
async execute(id, params, signal, onUpdate, ctx) {
|
|
5778
|
+
if (!ctx)
|
|
5779
|
+
throw new Error("Sandbox context is unavailable");
|
|
5780
|
+
if (!ensureSandboxState(ctx)) {
|
|
5781
|
+
if (sandboxState === "unavailable") {
|
|
5782
|
+
throw new Error("Sandbox is unavailable; refusing command");
|
|
5783
|
+
}
|
|
5784
|
+
return localBash.execute(id, params, signal, onUpdate, ctx);
|
|
5676
5785
|
}
|
|
5677
|
-
return runBashWithOptionalRetry(id, params, signal, onUpdate,
|
|
5786
|
+
return runBashWithOptionalRetry(id, params, signal, onUpdate, ctx);
|
|
5678
5787
|
}
|
|
5679
5788
|
};
|
|
5680
5789
|
}
|
|
@@ -5690,8 +5799,12 @@ ${message}
|
|
|
5690
5799
|
if (shouldRegisterBashTool)
|
|
5691
5800
|
pi.registerTool(createBashTool(localCwd));
|
|
5692
5801
|
pi.on("user_bash", async (_event, ctx) => {
|
|
5693
|
-
if (!ensureSandboxState(ctx))
|
|
5802
|
+
if (!ensureSandboxState(ctx)) {
|
|
5803
|
+
if (sandboxState === "unavailable") {
|
|
5804
|
+
throw new Error("Sandbox is unavailable; refusing command");
|
|
5805
|
+
}
|
|
5694
5806
|
return;
|
|
5807
|
+
}
|
|
5695
5808
|
return { operations: createLandstripBashOps(ctx, { promptOnBlock: true }) };
|
|
5696
5809
|
});
|
|
5697
5810
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -5844,7 +5957,7 @@ ${target.path}`);
|
|
|
5844
5957
|
const integration = {
|
|
5845
5958
|
version: LANDSTRIP_RUNTIME_VERSION,
|
|
5846
5959
|
getContext,
|
|
5847
|
-
|
|
5960
|
+
registerShellProvider,
|
|
5848
5961
|
prepareProcess,
|
|
5849
5962
|
prepareRpcWorker,
|
|
5850
5963
|
registerWorkerExtension,
|
|
@@ -5870,6 +5983,10 @@ export {
|
|
|
5870
5983
|
extractNativeDeniedPath,
|
|
5871
5984
|
domainMatchesAny,
|
|
5872
5985
|
pi_landstrip_default as default,
|
|
5986
|
+
createWindowsDenyRead,
|
|
5987
|
+
createPosixShellProvider,
|
|
5988
|
+
createLandstripLauncherEnvironment,
|
|
5873
5989
|
createLandstripIntegration,
|
|
5874
|
-
controlResponseLine
|
|
5990
|
+
controlResponseLine,
|
|
5991
|
+
POSIX_SHELL_PROVIDER_ID
|
|
5875
5992
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.16",
|
|
4
4
|
"description": "Sandboxed Bash and process-backed agents for Pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"ci:test": "npm run test"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@landstrip/landstrip": "^0.18.
|
|
42
|
+
"@landstrip/landstrip": "^0.18.16",
|
|
43
43
|
"jsonc-parser": "^3.3.1",
|
|
44
44
|
"minimatch": "^10.2.5",
|
|
45
45
|
"yaml": "^2.9.0"
|