pi-landstrip 0.19.13 → 0.19.15
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 +51 -71
- package/api.ts +152 -27
- package/dist/index.ts +48 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,52 +9,38 @@ Requires Pi ≥0.82.0 and Node.js ≥22.19.0.
|
|
|
9
9
|
pi install npm:pi-landstrip
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`--no-sandbox` or `enabled: false` explicitly permits unsandboxed execution.
|
|
12
|
+
Unusable sandboxing fails closed; `--no-sandbox` or `enabled: false` permits
|
|
13
|
+
unsandboxed execution.
|
|
15
14
|
|
|
16
15
|
## Permissions and configuration
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`toolFilesystemPolicy: "sandbox"` applies filesystem policy checks, not OS isolation.
|
|
17
|
+
With sandboxing enabled, Bash (`bash`, `!`, `!!`) and workers are OS-isolated.
|
|
18
|
+
Primary file tools remain in trusted Pi; `toolFilesystemPolicy: "sandbox"`
|
|
19
|
+
adds policy checks, not isolation. Tool approval never bypasses hard denials.
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
the global or project file. Store only overrides:
|
|
21
|
+
Configuration: bundled [`sandbox.json`](sandbox.json) → `~/.pi/agent/sandbox.json`
|
|
22
|
+
→ trusted-project `.pi/sandbox.json`. Arrays combine; later scalars win.
|
|
23
|
+
Persistent approvals update global/project overrides. For policy-only shell reads:
|
|
27
24
|
|
|
28
25
|
```json
|
|
29
|
-
{
|
|
30
|
-
"shell": { "readAccess": "policy" },
|
|
31
|
-
"filesystem": { "allowWrite": ["."] },
|
|
32
|
-
"network": { "allowNetwork": false }
|
|
33
|
-
}
|
|
26
|
+
{ "shell": { "readAccess": "policy" } }
|
|
34
27
|
```
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- `filesystem.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
auth are unsupported on Windows.
|
|
52
|
-
- `filesystem.denyWriteAlways` cannot be overridden by equal/nested write grants
|
|
53
|
-
or interactive prompts; write attempts are silently rejected. Non-empty
|
|
54
|
-
`denyWriteAlways` is unsupported on Windows.
|
|
55
|
-
- External skill, prompt, and theme directories may need explicit discovery
|
|
56
|
-
access. Prefer resource file paths: resolved-file grants do not cover enclosing
|
|
57
|
-
directories.
|
|
29
|
+
Shell reads default to `"host"`; workers always use policy reads. See
|
|
30
|
+
[landstrip(1)](../landstrip/man/man1/landstrip.1) for native policy, traps,
|
|
31
|
+
and platform limits, including Windows read grants and hard-denial restrictions.
|
|
32
|
+
|
|
33
|
+
Worker boundaries:
|
|
34
|
+
|
|
35
|
+
- Bootstrap grants override bundled `denyRead`, never global/trusted-project
|
|
36
|
+
denies. Denied startup resources prevent launch. Legacy `/home` or `/Users`
|
|
37
|
+
denies are explicit overrides; remove them only if unintended.
|
|
38
|
+
- Hard read denials also apply to shell host reads. Private workers protect
|
|
39
|
+
parent auth-file/lock roots with `denyReadAlways` and `denyWrite`;
|
|
40
|
+
private worker authentication is unsupported on Windows.
|
|
41
|
+
- `filesystem.denyWriteAlways` rejects write attempts without prompting.
|
|
42
|
+
- External skills/prompts/themes may need directory discovery grants;
|
|
43
|
+
resolved-file grants do not include parent directories.
|
|
58
44
|
|
|
59
45
|
## Agents and tasks
|
|
60
46
|
|
|
@@ -69,49 +55,43 @@ Configure `~/.pi/agent/landstrip.json`, trusted-project `.pi/landstrip.json`, or
|
|
|
69
55
|
}
|
|
70
56
|
```
|
|
71
57
|
|
|
72
|
-
`maxSubagents
|
|
73
|
-
|
|
58
|
+
`maxSubagents`: 0–16. Agent definitions can override model, prompt, mode, options,
|
|
59
|
+
and permissions.
|
|
74
60
|
|
|
75
|
-
`/landstrip`
|
|
76
|
-
|
|
61
|
+
Use `/landstrip` for the management pane and `/landstrip help` for commands.
|
|
62
|
+
Task IDs accept unique prefixes; bare IDs open logs. `/landstrip tasks kill <id>`
|
|
63
|
+
terminates a task. `Ctrl+Shift+A` cycles primary agents while idle.
|
|
64
|
+
The `task` tool accepts `background: true` for immediate return and `task_id`
|
|
65
|
+
to continue a saved task. Press `Enter` in running/queued logs to steer workers.
|
|
77
66
|
|
|
78
|
-
|
|
79
|
-
| --------------------------------- | ------------------------------- |
|
|
80
|
-
| `/landstrip status` | Inspect sandbox, agents, tasks. |
|
|
81
|
-
| `/landstrip settings` | Set concurrency/file policy. |
|
|
82
|
-
| `/landstrip sandbox [on\|off]` | Inspect/toggle OS isolation. |
|
|
83
|
-
| `/landstrip agents [@name]` | List/select primary agents. |
|
|
84
|
-
| `/landstrip subagents` | List process subagents. |
|
|
85
|
-
| `/landstrip tasks [list]` | List task sessions. |
|
|
86
|
-
| `/landstrip tasks kill <task-id>` | Terminate a task. |
|
|
87
|
-
| `/landstrip logs [task-id]` | Open task logs. |
|
|
67
|
+
### Worker environment and authentication
|
|
88
68
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`background: true` for immediate return and `task_id` to continue a saved task.
|
|
92
|
-
Press `Enter` in running/queued task logs to steer the worker.
|
|
69
|
+
Workers start `--offline`; install resources and refresh model catalogs in the
|
|
70
|
+
parent first. Provider endpoints still need network permission.
|
|
93
71
|
|
|
94
|
-
|
|
72
|
+
Only runtime paths, locale, certificate paths, and supported Pi config are
|
|
73
|
+
inherited—not parent API keys, tool tokens, proxy URLs, or arbitrary variables.
|
|
74
|
+
The parent refreshes selected-provider auth and sends it, along with selected
|
|
75
|
+
Bedrock/Vertex/Azure settings, over a private Unix pipe, not arguments or
|
|
76
|
+
environment. Configure tool logins separately; authenticated workers cannot
|
|
77
|
+
launch on Windows.
|
|
95
78
|
|
|
96
|
-
|
|
97
|
-
|
|
79
|
+
Ambient AWS profiles need an explicit region for non-ARN Bedrock models.
|
|
80
|
+
Custom credential-chain environment paths are unsupported; use scoped keys or
|
|
81
|
+
a Bedrock bearer token. Provider credential files need filesystem grants.
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
configuration—not parent API keys, tool tokens, proxy URLs, or arbitrary variables.
|
|
101
|
-
The parent refreshes selected-provider auth and sends it through a private Unix
|
|
102
|
-
pipe, never arguments or worker environment. Environment-based tool logins need
|
|
103
|
-
separate configuration; authenticated workers fail before spawning on Windows.
|
|
83
|
+
## Extension API
|
|
104
84
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
85
|
+
[`pi-landstrip/api`](api.ts) exports `createLandstrip`. `prepareProcess()` returns
|
|
86
|
+
`undefined` without a runtime. Dispose prepared processes separately from the
|
|
87
|
+
discovery handle; generic `spawn()` returns `ChildProcess`, RPC workers use
|
|
88
|
+
piped streams.
|
|
109
89
|
|
|
110
90
|
## Development
|
|
111
91
|
|
|
112
|
-
From this package: `npm ci
|
|
113
|
-
|
|
92
|
+
From this package: `npm ci && npm run all` (Bun required).
|
|
93
|
+
`npm run ci:fmt` checks formatting without edits.
|
|
114
94
|
|
|
115
95
|
## License
|
|
116
96
|
|
|
117
|
-
Apache-2.0
|
|
97
|
+
[Apache-2.0](LICENSE).
|
package/api.ts
CHANGED
|
@@ -105,6 +105,11 @@ export interface LandstripPreparedProcess {
|
|
|
105
105
|
dispose(): Promise<void>;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export interface Landstrip {
|
|
109
|
+
prepareProcess(options: LandstripProcessOptions): Promise<LandstripPreparedProcess | undefined>;
|
|
110
|
+
dispose(): void;
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
export interface LandstripRpcChildProcess {
|
|
109
114
|
readonly stdin: Writable;
|
|
110
115
|
readonly stdout: Readable;
|
|
@@ -126,7 +131,7 @@ export type LandstripSpawn = (
|
|
|
126
131
|
command: string,
|
|
127
132
|
args: readonly string[],
|
|
128
133
|
options: SpawnOptions,
|
|
129
|
-
) => ChildProcess
|
|
134
|
+
) => ChildProcess;
|
|
130
135
|
|
|
131
136
|
export type LandstripEvent =
|
|
132
137
|
| { readonly type: 'sandbox.changed'; readonly context: LandstripContextV2 }
|
|
@@ -173,46 +178,165 @@ function eventBus(pi: ExtensionAPI): EventBusLike | undefined {
|
|
|
173
178
|
function isRuntime(value: unknown): value is PiLandstripRuntimeV2 {
|
|
174
179
|
return (
|
|
175
180
|
isRecord(value) &&
|
|
176
|
-
value
|
|
177
|
-
typeof value
|
|
178
|
-
typeof value
|
|
179
|
-
typeof value
|
|
180
|
-
typeof value
|
|
181
|
-
typeof value
|
|
182
|
-
typeof value
|
|
183
|
-
typeof value
|
|
181
|
+
value['version'] === LANDSTRIP_RUNTIME_VERSION &&
|
|
182
|
+
typeof value['getContext'] === 'function' &&
|
|
183
|
+
typeof value['registerShellProvider'] === 'function' &&
|
|
184
|
+
typeof value['registerPermissionAskProvider'] === 'function' &&
|
|
185
|
+
typeof value['prepareProcess'] === 'function' &&
|
|
186
|
+
typeof value['registerWorkerExtension'] === 'function' &&
|
|
187
|
+
typeof value['getWorkerExtensions'] === 'function' &&
|
|
188
|
+
typeof value['on'] === 'function'
|
|
184
189
|
);
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
|
|
192
|
+
function isDisposableProcess(value: unknown): value is Pick<LandstripPreparedProcess, 'dispose'> {
|
|
193
|
+
return isRecord(value) && typeof value['dispose'] === 'function';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isPreparedProcess(value: unknown): value is LandstripPreparedProcess {
|
|
197
|
+
return (
|
|
198
|
+
isRecord(value) &&
|
|
199
|
+
typeof value['command'] === 'string' &&
|
|
200
|
+
value['command'].trim().length > 0 &&
|
|
201
|
+
Array.isArray(value['args']) &&
|
|
202
|
+
[...value['args']].every((arg: unknown) => typeof arg === 'string') &&
|
|
203
|
+
typeof value['cwd'] === 'string' &&
|
|
204
|
+
value['cwd'].length > 0 &&
|
|
205
|
+
isRecord(value['env']) &&
|
|
206
|
+
Object.values(value['env']).every(
|
|
207
|
+
(entry) => entry === undefined || typeof entry === 'string',
|
|
208
|
+
) &&
|
|
209
|
+
typeof value['spawn'] === 'function' &&
|
|
210
|
+
isDisposableProcess(value)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function discoverLandstrip(
|
|
188
215
|
pi: ExtensionAPI,
|
|
189
216
|
callback: (runtime: PiLandstripRuntimeV2) => void,
|
|
217
|
+
onInvalid?: (error: Error) => void,
|
|
190
218
|
): () => void {
|
|
191
219
|
const events = eventBus(pi);
|
|
192
220
|
if (!events) return () => undefined;
|
|
193
221
|
|
|
194
222
|
let current: PiLandstripRuntimeV2 | undefined;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
let disposed = false;
|
|
224
|
+
const register = (value: unknown): void => {
|
|
225
|
+
if (disposed) return;
|
|
226
|
+
if (!isRuntime(value)) {
|
|
227
|
+
onInvalid?.(new Error('Invalid Landstrip runtime v2 declaration'));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (value === current) return;
|
|
231
|
+
current = value;
|
|
232
|
+
callback(value);
|
|
199
233
|
};
|
|
200
234
|
const unsubscribe = events.on(RUNTIME_REGISTER_EVENT, (value) => {
|
|
201
|
-
if (
|
|
202
|
-
if (
|
|
235
|
+
if (disposed) return;
|
|
236
|
+
if (!isRecord(value) || value['version'] !== LANDSTRIP_RUNTIME_VERSION) {
|
|
237
|
+
onInvalid?.(new Error('Invalid Landstrip runtime v2 registration'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
register(value['runtime']);
|
|
203
241
|
});
|
|
242
|
+
const dispose = (): void => {
|
|
243
|
+
if (disposed) return;
|
|
244
|
+
disposed = true;
|
|
245
|
+
unsubscribe();
|
|
246
|
+
};
|
|
204
247
|
try {
|
|
205
248
|
events.emit(RUNTIME_DISCOVER_EVENT, {
|
|
206
249
|
version: LANDSTRIP_RUNTIME_VERSION,
|
|
207
250
|
register,
|
|
208
251
|
} satisfies RuntimeDiscovery);
|
|
209
|
-
return
|
|
252
|
+
return dispose;
|
|
210
253
|
} catch (error) {
|
|
211
|
-
|
|
254
|
+
dispose();
|
|
212
255
|
throw error;
|
|
213
256
|
}
|
|
214
257
|
}
|
|
215
258
|
|
|
259
|
+
export function useLandstrip(
|
|
260
|
+
pi: ExtensionAPI,
|
|
261
|
+
callback: (runtime: PiLandstripRuntimeV2) => void,
|
|
262
|
+
): () => void {
|
|
263
|
+
return discoverLandstrip(pi, callback);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function createLandstrip(pi: ExtensionAPI): Landstrip {
|
|
267
|
+
let runtime: PiLandstripRuntimeV2 | undefined;
|
|
268
|
+
let runtimeError: Error | undefined;
|
|
269
|
+
let disposed = false;
|
|
270
|
+
const stopDiscovery = discoverLandstrip(
|
|
271
|
+
pi,
|
|
272
|
+
(value) => {
|
|
273
|
+
runtime = value;
|
|
274
|
+
},
|
|
275
|
+
(error) => {
|
|
276
|
+
runtimeError ??= error;
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
if (runtimeError) {
|
|
280
|
+
stopDiscovery();
|
|
281
|
+
throw runtimeError;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
async prepareProcess(options) {
|
|
286
|
+
if (disposed) throw new Error('Landstrip handle has been disposed');
|
|
287
|
+
if (runtimeError) throw runtimeError;
|
|
288
|
+
options.signal?.throwIfAborted();
|
|
289
|
+
if (!runtime) return undefined;
|
|
290
|
+
|
|
291
|
+
const prepared: unknown = await runtime.prepareProcess(options);
|
|
292
|
+
if (!isDisposableProcess(prepared)) {
|
|
293
|
+
throw new Error('Landstrip returned a process without a disposal method');
|
|
294
|
+
}
|
|
295
|
+
let disposePromise: Promise<void> | undefined;
|
|
296
|
+
const dispose = (): Promise<void> => {
|
|
297
|
+
disposePromise ??= Promise.resolve().then(() => prepared.dispose());
|
|
298
|
+
return disposePromise;
|
|
299
|
+
};
|
|
300
|
+
try {
|
|
301
|
+
options.signal?.throwIfAborted();
|
|
302
|
+
if (disposed) throw new Error('Landstrip handle has been disposed');
|
|
303
|
+
if (runtimeError) throw runtimeError;
|
|
304
|
+
if (!isPreparedProcess(prepared)) {
|
|
305
|
+
throw new Error('Landstrip returned an invalid prepared process');
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
try {
|
|
309
|
+
await dispose();
|
|
310
|
+
} catch (cleanupError) {
|
|
311
|
+
throw new AggregateError(
|
|
312
|
+
[error, cleanupError],
|
|
313
|
+
'Landstrip process preparation cleanup failed',
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
command: prepared.command,
|
|
321
|
+
args: prepared.args,
|
|
322
|
+
cwd: prepared.cwd,
|
|
323
|
+
env: prepared.env,
|
|
324
|
+
spawn(command, args, spawnOptions) {
|
|
325
|
+
if (disposePromise) throw new Error('Prepared Landstrip process has been disposed');
|
|
326
|
+
return prepared.spawn(command, args, spawnOptions);
|
|
327
|
+
},
|
|
328
|
+
dispose,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
dispose() {
|
|
332
|
+
if (disposed) return;
|
|
333
|
+
disposed = true;
|
|
334
|
+
stopDiscovery();
|
|
335
|
+
runtime = undefined;
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
216
340
|
function provideExtensionProvider(
|
|
217
341
|
pi: ExtensionAPI,
|
|
218
342
|
register: (runtime: PiLandstripRuntimeV2) => () => void,
|
|
@@ -251,8 +375,8 @@ export function publishLandstripRuntime(
|
|
|
251
375
|
if (!events) return () => undefined;
|
|
252
376
|
|
|
253
377
|
const unsubscribe = events.on(RUNTIME_DISCOVER_EVENT, (value) => {
|
|
254
|
-
if (!isRecord(value) || value
|
|
255
|
-
if (typeof value
|
|
378
|
+
if (!isRecord(value) || value['version'] !== LANDSTRIP_RUNTIME_VERSION) return;
|
|
379
|
+
if (typeof value['register'] === 'function') value['register'](runtime);
|
|
256
380
|
});
|
|
257
381
|
try {
|
|
258
382
|
events.emit(RUNTIME_REGISTER_EVENT, {
|
|
@@ -280,13 +404,14 @@ export function contextFromEnvironment(
|
|
|
280
404
|
const value: unknown = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
281
405
|
if (
|
|
282
406
|
!isRecord(value) ||
|
|
283
|
-
value
|
|
284
|
-
value
|
|
285
|
-
value
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
(value
|
|
407
|
+
value['version'] !== LANDSTRIP_RUNTIME_VERSION ||
|
|
408
|
+
value['host'] !== 'pi' ||
|
|
409
|
+
value['role'] !== 'subagent' ||
|
|
410
|
+
typeof value['sandbox'] !== 'string' ||
|
|
411
|
+
!['enabled', 'disabled', 'unavailable'].includes(value['sandbox']) ||
|
|
412
|
+
typeof value['cwd'] !== 'string' ||
|
|
413
|
+
!Number.isInteger(value['depth']) ||
|
|
414
|
+
(value['depth'] as number) < 0 ||
|
|
290
415
|
['sessionId', 'taskId', 'parentTaskId', 'agent'].some(
|
|
291
416
|
(key) => value[key] !== undefined && typeof value[key] !== 'string',
|
|
292
417
|
)
|
package/dist/index.ts
CHANGED
|
@@ -80,10 +80,10 @@ function publishLandstripRuntime(pi, runtime) {
|
|
|
80
80
|
return;
|
|
81
81
|
};
|
|
82
82
|
const unsubscribe = events.on(RUNTIME_DISCOVER_EVENT, (value) => {
|
|
83
|
-
if (!isRecord(value) || value
|
|
83
|
+
if (!isRecord(value) || value["version"] !== LANDSTRIP_RUNTIME_VERSION)
|
|
84
84
|
return;
|
|
85
|
-
if (typeof value
|
|
86
|
-
value
|
|
85
|
+
if (typeof value["register"] === "function")
|
|
86
|
+
value["register"](runtime);
|
|
87
87
|
});
|
|
88
88
|
try {
|
|
89
89
|
events.emit(RUNTIME_REGISTER_EVENT, {
|
|
@@ -6029,7 +6029,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
|
|
|
6029
6029
|
if (!provider)
|
|
6030
6030
|
return { decision: "abstain" };
|
|
6031
6031
|
const result = await provider.decide(request);
|
|
6032
|
-
if (typeof result !== "object" || result === null || !("decision" in result) || !["allow", "deny", "abstain"].includes(
|
|
6032
|
+
if (typeof result !== "object" || result === null || !("decision" in result) || typeof result.decision !== "string" || !["allow", "deny", "abstain"].includes(result.decision) || "reason" in result && result.reason !== undefined && typeof result.reason !== "string") {
|
|
6033
6033
|
throw new Error(`Permission ask provider "${provider.id}" returned an invalid decision`);
|
|
6034
6034
|
}
|
|
6035
6035
|
return result;
|
|
@@ -6390,7 +6390,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6390
6390
|
}
|
|
6391
6391
|
});
|
|
6392
6392
|
}
|
|
6393
|
-
function
|
|
6393
|
+
function prepareUnsandboxedProcess(options) {
|
|
6394
6394
|
let spawned = false;
|
|
6395
6395
|
let disposed = false;
|
|
6396
6396
|
let disposePromise;
|
|
@@ -6426,7 +6426,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6426
6426
|
}
|
|
6427
6427
|
};
|
|
6428
6428
|
}
|
|
6429
|
-
async function
|
|
6429
|
+
async function prepareManagedProcess(options) {
|
|
6430
6430
|
if (options.signal?.aborted)
|
|
6431
6431
|
throw new Error("Task cancelled");
|
|
6432
6432
|
if (!ensureSandboxState(options.ctx)) {
|
|
@@ -6437,7 +6437,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6437
6437
|
warnUnsandboxed(options.ctx, "Subagent processes are running without Landstrip sandboxing", "warning");
|
|
6438
6438
|
unsandboxedWorkerWarningShown = true;
|
|
6439
6439
|
}
|
|
6440
|
-
return
|
|
6440
|
+
return prepareUnsandboxedProcess(options);
|
|
6441
6441
|
}
|
|
6442
6442
|
const allowances = {
|
|
6443
6443
|
permissionPromptLabel: options.permissionPromptLabel,
|
|
@@ -6475,7 +6475,28 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6475
6475
|
let policy;
|
|
6476
6476
|
let trapSocket;
|
|
6477
6477
|
let childEnd;
|
|
6478
|
+
let preparedChild;
|
|
6478
6479
|
let disposed = false;
|
|
6480
|
+
const cleanup = async () => {
|
|
6481
|
+
const errors = [];
|
|
6482
|
+
for (const release of [
|
|
6483
|
+
() => stopPreparedChild(preparedChild),
|
|
6484
|
+
() => trapSocket?.destroy(),
|
|
6485
|
+
() => childEnd?.destroy(),
|
|
6486
|
+
() => {
|
|
6487
|
+
if (policy)
|
|
6488
|
+
rmSync2(policy.dir, { recursive: true, force: true });
|
|
6489
|
+
},
|
|
6490
|
+
() => proxy?.stop()
|
|
6491
|
+
]) {
|
|
6492
|
+
try {
|
|
6493
|
+
await release();
|
|
6494
|
+
} catch (error) {
|
|
6495
|
+
errors.push(error);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
return errors;
|
|
6499
|
+
};
|
|
6479
6500
|
try {
|
|
6480
6501
|
if (options.signal?.aborted)
|
|
6481
6502
|
throw new Error("Task cancelled");
|
|
@@ -6487,20 +6508,17 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6487
6508
|
attachWorkerTrap(trapSocket, options.ctx, options.cwd, processAllowances, options.signal);
|
|
6488
6509
|
}
|
|
6489
6510
|
} catch (error) {
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
await proxy?.stop();
|
|
6511
|
+
const cleanupErrors = await cleanup();
|
|
6512
|
+
if (cleanupErrors.length > 0) {
|
|
6513
|
+
throw new AggregateError([error, ...cleanupErrors], "Landstrip process preparation cleanup failed");
|
|
6514
|
+
}
|
|
6495
6515
|
throw error;
|
|
6496
6516
|
}
|
|
6497
6517
|
const workerPolicy = policy;
|
|
6498
|
-
const workerTrapSocket = trapSocket;
|
|
6499
6518
|
const workerChildEnd = childEnd;
|
|
6500
6519
|
const workerEnv = createProxyEnvironment(proxy?.port ?? null, proxyToken, options.env ?? {});
|
|
6501
6520
|
let spawned = false;
|
|
6502
6521
|
let disposePromise;
|
|
6503
|
-
let preparedChild;
|
|
6504
6522
|
const spawnWorker = (_command, _args, spawnOptions) => {
|
|
6505
6523
|
if (disposed)
|
|
6506
6524
|
throw new Error("Prepared process has been disposed");
|
|
@@ -6535,9 +6553,9 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6535
6553
|
detached: true,
|
|
6536
6554
|
stdio
|
|
6537
6555
|
});
|
|
6538
|
-
workerChildEnd?.destroy();
|
|
6539
|
-
patchProcessGroupKill(child);
|
|
6540
6556
|
preparedChild = child;
|
|
6557
|
+
patchProcessGroupKill(child);
|
|
6558
|
+
workerChildEnd?.destroy();
|
|
6541
6559
|
return child;
|
|
6542
6560
|
};
|
|
6543
6561
|
return {
|
|
@@ -6549,21 +6567,29 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6549
6567
|
dispose() {
|
|
6550
6568
|
disposePromise ??= (async () => {
|
|
6551
6569
|
disposed = true;
|
|
6552
|
-
await
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
await proxy?.stop();
|
|
6570
|
+
const cleanupErrors = await cleanup();
|
|
6571
|
+
if (cleanupErrors.length > 0) {
|
|
6572
|
+
throw new AggregateError(cleanupErrors, "Landstrip process disposal cleanup failed");
|
|
6573
|
+
}
|
|
6557
6574
|
})();
|
|
6558
6575
|
return disposePromise;
|
|
6559
6576
|
}
|
|
6560
6577
|
};
|
|
6561
6578
|
}
|
|
6579
|
+
async function prepareRpcWorker(options) {
|
|
6580
|
+
const prepared = await prepareManagedProcess(options);
|
|
6581
|
+
return {
|
|
6582
|
+
...prepared,
|
|
6583
|
+
spawn(command, args, spawnOptions) {
|
|
6584
|
+
return prepared.spawn(command, args, spawnOptions);
|
|
6585
|
+
}
|
|
6586
|
+
};
|
|
6587
|
+
}
|
|
6562
6588
|
async function prepareProcess(options) {
|
|
6563
6589
|
if (process.platform === "win32" && (getNoSandboxFlag() || !loadConfig(options.cwd).enabled)) {
|
|
6564
6590
|
throw new Error("Generic process preparation requires sandboxing on Windows");
|
|
6565
6591
|
}
|
|
6566
|
-
return
|
|
6592
|
+
return prepareManagedProcess({
|
|
6567
6593
|
...options,
|
|
6568
6594
|
env: options.env ?? process.env,
|
|
6569
6595
|
readPaths: options.readPaths ?? [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.15",
|
|
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-api": "^0.19.
|
|
42
|
+
"@landstrip/landstrip-api": "^0.19.15",
|
|
43
43
|
"jsonc-parser": "^3.3.1",
|
|
44
44
|
"minimatch": "^10.2.5",
|
|
45
45
|
"undici": "^8.9.0",
|