pi-landstrip 0.17.34 → 0.17.35
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 +88 -20
- package/dist/index.ts +338 -121
- package/dist/sandbox.json +5 -0
- package/dist/subagents.json +17 -0
- package/package.json +3 -3
- package/sandbox.json +5 -0
- package/subagents.json +17 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The extension includes a shared [sandbox policy](./sandbox.json) and separate
|
|
|
16
16
|
[agent configuration](./subagents.json). Global and trusted-project files can
|
|
17
17
|
override both.
|
|
18
18
|
|
|
19
|
-
Process-backed subagents require Pi >= 0.80.6
|
|
19
|
+
Process-backed subagents require Pi >= 0.80.6, and Node.js >= 22.19.0.
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
@@ -46,6 +46,16 @@ Alternatively, place the extension under `~/.pi/agent/extensions/` (global) or
|
|
|
46
46
|
|
|
47
47
|
On unsupported platforms the extension loads but leaves sandboxing disabled.
|
|
48
48
|
|
|
49
|
+
On Windows, Pi's Git Bash/MSYS shell cannot run under LPAC. The packaged policy
|
|
50
|
+
therefore selects standard AppContainer explicitly. This is weaker than LPAC because
|
|
51
|
+
resources granted to `ALL APPLICATION PACKAGES` remain visible; `/sandbox` and the
|
|
52
|
+
status line report the active backend and mode. There is no silent
|
|
53
|
+
LPAC-to-standard fallback.
|
|
54
|
+
|
|
55
|
+
An optional restricted-user backend supports Git Bash without AppContainer. It
|
|
56
|
+
requires one-time elevated host setup and persistent local accounts and WFP rules;
|
|
57
|
+
see [Windows restricted-user setup](#windows-restricted-user-setup).
|
|
58
|
+
|
|
49
59
|
## Disabling
|
|
50
60
|
|
|
51
61
|
Use the `--no-sandbox` flag, or set `enabled` to `false` in `sandbox.json`:
|
|
@@ -61,9 +71,10 @@ processes, but without the outer Landstrip OS sandbox. The extension warns once
|
|
|
61
71
|
per session. Agent tool permissions still apply, but they are not an OS isolation
|
|
62
72
|
boundary.
|
|
63
73
|
|
|
64
|
-
Trusted project config overrides global config
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
Trusted project config overrides global config except for the Windows backend,
|
|
75
|
+
which can be selected only in the trusted global file. `/sandbox` updates a trusted
|
|
76
|
+
project sandbox file when present, otherwise the global file. Pi versions without a
|
|
77
|
+
project-trust API use only global configuration.
|
|
67
78
|
|
|
68
79
|
## Behavior
|
|
69
80
|
|
|
@@ -75,11 +86,12 @@ being approved.
|
|
|
75
86
|
Project approvals are written to `.pi/sandbox.json`; global approvals are
|
|
76
87
|
written to `~/.pi/agent/sandbox.json`.
|
|
77
88
|
|
|
78
|
-
The main agent remains a normal Pi process. `pi-landstrip` replaces Bash
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
callbacks remain trusted code outside the
|
|
89
|
+
The main agent remains a normal Pi process. `pi-landstrip` replaces Bash execution,
|
|
90
|
+
including AI `bash` calls and manually typed shell commands (`!` and `!!`), with a
|
|
91
|
+
Landstrip-wrapped implementation. Network traffic uses an allowlist proxy when
|
|
92
|
+
direct access is disabled and the platform policy permits loopback; otherwise it is
|
|
93
|
+
blocked. Pi's filesystem tools and plugin callbacks remain trusted code outside the
|
|
94
|
+
Landstrip sandbox.
|
|
83
95
|
|
|
84
96
|
By default, Bash and subagent processes have no direct network access. Reads are
|
|
85
97
|
limited to the project, Git configuration, and `/dev/null`; writes are limited
|
|
@@ -132,9 +144,11 @@ The tool accepts the OpenCode task fields:
|
|
|
132
144
|
Foreground tasks return the child result directly. Background tasks return a
|
|
133
145
|
queued result and deliver completion automatically. Task rows show lifecycle
|
|
134
146
|
state, current activity, tool-call count, elapsed time, and expandable output.
|
|
135
|
-
Use `/subagents`
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
Use `/subagents` to open an inspector with **Agents** and **Sessions** tabs. The
|
|
148
|
+
Agents tab lists every available subagent type and its `built-in`, `global`, or
|
|
149
|
+
`local` source. The Sessions tab inspects child transcripts; task rows can open a
|
|
150
|
+
specific one with `/subagents <id>`. Completed and failed task metadata remains
|
|
151
|
+
available after reload, and persisted sessions can be continued with `task_id`.
|
|
138
152
|
|
|
139
153
|
Session switching or shutdown stops live workers. After an unclean restart,
|
|
140
154
|
unfinished work is marked interrupted; completed but undelivered background
|
|
@@ -162,11 +176,19 @@ Unsupported Pi versions still fail task startup.
|
|
|
162
176
|
`sandbox.json`, then restart or continue the task in a fresh worker to apply
|
|
163
177
|
additional filesystem access. Main-agent Bash can retry a command, but there
|
|
164
178
|
is no live worker-policy update.
|
|
165
|
-
- **Windows
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
- **Windows AppContainer**: the packaged policy selects standard AppContainer for
|
|
180
|
+
Pi's Git Bash/MSYS runtime. With `windows.allowLoopback: false` (the default), the
|
|
181
|
+
extension does not start an unreachable proxy and the worker has no network.
|
|
182
|
+
Setting `windows.allowLoopback` to `true` enables the domain proxy, but also gives
|
|
183
|
+
the AppContainer access to every local loopback service, not only the proxy port.
|
|
184
|
+
Setting `network.allowNetwork` to `true` instead gives the entire worker
|
|
185
|
+
unrestricted network access; domain lists are then not a boundary. Both opt-ins
|
|
186
|
+
are explicit security downgrades and produce warnings.
|
|
187
|
+
- **Windows restricted user**: WFP blocks restricted accounts except for the
|
|
188
|
+
installed proxy port range, so domain-filtered proxying works without a broad
|
|
189
|
+
loopback exemption. `network.allowNetwork: true` leases a separately provisioned
|
|
190
|
+
unrestricted account. Filesystem grants are journaled and revoked after every
|
|
191
|
+
process tree exits.
|
|
170
192
|
|
|
171
193
|
### Credentials
|
|
172
194
|
|
|
@@ -182,13 +204,59 @@ first.
|
|
|
182
204
|
|
|
183
205
|
Subagent configuration is read from `~/.pi/agent/subagents.json` and, for trusted
|
|
184
206
|
projects, `.pi/subagents.json`. Project values override global values; both are
|
|
185
|
-
merged over the packaged [defaults](./subagents.json).
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
merged over the packaged [defaults](./subagents.json). The packaged subagent types
|
|
208
|
+
are `explore`, `general`, and the OpenCode-compatible `scout` reconnaissance agent.
|
|
209
|
+
|
|
210
|
+
Sandbox policy remains in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi's
|
|
211
|
+
`settings.json` contains only normal Pi settings, such as the `pi-landstrip` package
|
|
212
|
+
entry.
|
|
188
213
|
|
|
189
214
|
`subagents.json` accepts only top-level `maxSubagents` and `subagents`; sandbox
|
|
190
215
|
fields belong in `sandbox.json`.
|
|
191
216
|
|
|
217
|
+
The Windows sandbox fields are:
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"windows": {
|
|
222
|
+
"backend": "appContainer",
|
|
223
|
+
"appContainerMode": "standard",
|
|
224
|
+
"allowLoopback": false
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`backend` is `"appContainer"` or `"restrictedUser"`. It is a trusted host setting:
|
|
230
|
+
only `~/.pi/agent/sandbox.json` can change it, and project configuration cannot
|
|
231
|
+
override it. `appContainerMode` is `"lpac"` or `"standard"`; core Landstrip defaults
|
|
232
|
+
to LPAC, while the Pi package defaults to standard mode for Git Bash compatibility.
|
|
233
|
+
`allowLoopback` is independently opt-in and applies only to AppContainer.
|
|
234
|
+
|
|
235
|
+
### Windows restricted-user setup
|
|
236
|
+
|
|
237
|
+
Provision the backend once before selecting it in global configuration:
|
|
238
|
+
|
|
239
|
+
```powershell
|
|
240
|
+
npx @landstrip/landstrip windows setup
|
|
241
|
+
npx @landstrip/landstrip windows status
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Setup requests UAC elevation and defaults to eight restricted-network accounts, two
|
|
245
|
+
unrestricted-network accounts, and proxy ports 60080-60111. Run
|
|
246
|
+
`npx @landstrip/landstrip windows setup --help` for pool and port options. Then set
|
|
247
|
+
`"backend": "restrictedUser"` in `~/.pi/agent/sandbox.json` and restart Pi.
|
|
248
|
+
|
|
249
|
+
The extension checks installation health and reads the configured proxy range before
|
|
250
|
+
launching a restricted worker. Restricted-user mode does not support
|
|
251
|
+
`windows.allowLoopback` or `network.allowLocalBinding`. Remove the persistent
|
|
252
|
+
accounts, WFP rules, runner, and recovered per-run ACL grants with:
|
|
253
|
+
|
|
254
|
+
```powershell
|
|
255
|
+
npx @landstrip/landstrip windows uninstall
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A subagent configuration example follows:
|
|
259
|
+
|
|
192
260
|
```json
|
|
193
261
|
{
|
|
194
262
|
"maxSubagents": 2,
|
package/dist/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
Socket as NetSocket
|
|
20
20
|
} from "node:net";
|
|
21
21
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
22
|
-
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "node:path";
|
|
22
|
+
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join5, parse, resolve as resolve2 } from "node:path";
|
|
23
23
|
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
24
24
|
import { fileURLToPath as fileURLToPath3, URL } from "node:url";
|
|
25
25
|
import {
|
|
@@ -195,6 +195,12 @@ function mergeValue(base, override) {
|
|
|
195
195
|
}
|
|
196
196
|
return override;
|
|
197
197
|
}
|
|
198
|
+
function recordAgentSources(sources, config, source) {
|
|
199
|
+
if (!isRecord2(config.subagents) || !isRecord2(config.subagents.agent))
|
|
200
|
+
return;
|
|
201
|
+
for (const name of Object.keys(config.subagents.agent))
|
|
202
|
+
sources.set(name, source);
|
|
203
|
+
}
|
|
198
204
|
function getPiConfigPaths(cwd, fileName, agentDir = getAgentDir()) {
|
|
199
205
|
return {
|
|
200
206
|
globalPath: join2(agentDir, fileName),
|
|
@@ -217,17 +223,23 @@ async function setMaxSubagentsConfigForScope(cwd, maxSubagents, scope, agentDir
|
|
|
217
223
|
}
|
|
218
224
|
function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir()) {
|
|
219
225
|
const { globalPath, projectPath } = getPiConfigPaths(cwd, "subagents.json", agentDir);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
226
|
+
const builtInConfig = readConfig(join2(packageDir, "subagents.json"));
|
|
227
|
+
const globalConfig = readConfig(globalPath);
|
|
228
|
+
const projectConfig = includeProject ? readConfig(projectPath) : undefined;
|
|
229
|
+
const agentSources = new Map;
|
|
230
|
+
recordAgentSources(agentSources, builtInConfig, "built-in");
|
|
231
|
+
recordAgentSources(agentSources, globalConfig, "global");
|
|
232
|
+
if (projectConfig)
|
|
233
|
+
recordAgentSources(agentSources, projectConfig, "local");
|
|
234
|
+
let config = mergeValue(builtInConfig, globalConfig);
|
|
235
|
+
if (projectConfig)
|
|
236
|
+
config = mergeValue(config, projectConfig);
|
|
225
237
|
if (!Number.isInteger(config.maxSubagents) || (config.maxSubagents ?? -1) < 0 || (config.maxSubagents ?? MAX_SUBAGENTS + 1) > MAX_SUBAGENTS) {
|
|
226
238
|
throw new Error(`maxSubagents must be an integer from 0 to ${MAX_SUBAGENTS}`);
|
|
227
239
|
}
|
|
228
240
|
if (!isRecord2(config.subagents))
|
|
229
241
|
throw new Error("subagents must be an object");
|
|
230
|
-
return config;
|
|
242
|
+
return { ...config, agentSources };
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
// agents.ts
|
|
@@ -295,7 +307,7 @@ function legacyConfigWarnings(piAgentDir) {
|
|
|
295
307
|
return [];
|
|
296
308
|
}
|
|
297
309
|
}
|
|
298
|
-
function normalizeAgent(name, raw) {
|
|
310
|
+
function normalizeAgent(name, raw, source) {
|
|
299
311
|
for (const field of ["description", "prompt", "model", "variant"]) {
|
|
300
312
|
if (raw[field] !== undefined && typeof raw[field] !== "string") {
|
|
301
313
|
throw new Error(`agent ${name} ${field} must be a string`);
|
|
@@ -333,6 +345,7 @@ function normalizeAgent(name, raw) {
|
|
|
333
345
|
}
|
|
334
346
|
return {
|
|
335
347
|
name,
|
|
348
|
+
source,
|
|
336
349
|
description: typeof raw.description === "string" ? raw.description : undefined,
|
|
337
350
|
prompt: typeof raw.prompt === "string" ? raw.prompt : "",
|
|
338
351
|
mode: raw.mode ?? "all",
|
|
@@ -352,10 +365,12 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
|
|
|
352
365
|
const diagnostics = [];
|
|
353
366
|
let maxSubagents = 0;
|
|
354
367
|
let subagents = {};
|
|
368
|
+
let agentSources = new Map;
|
|
355
369
|
try {
|
|
356
370
|
const config = loadLandstripConfig(cwd, includeProject, piAgentDir);
|
|
357
371
|
maxSubagents = config.maxSubagents;
|
|
358
372
|
subagents = config.subagents;
|
|
373
|
+
agentSources = new Map(config.agentSources);
|
|
359
374
|
} catch (error) {
|
|
360
375
|
diagnostics.push(formatError(error));
|
|
361
376
|
}
|
|
@@ -370,7 +385,7 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
|
|
|
370
385
|
continue;
|
|
371
386
|
}
|
|
372
387
|
try {
|
|
373
|
-
const agent = normalizeAgent(name, value);
|
|
388
|
+
const agent = normalizeAgent(name, value, agentSources.get(name) ?? "built-in");
|
|
374
389
|
if (agent)
|
|
375
390
|
normalized.set(name, agent);
|
|
376
391
|
} catch (error) {
|
|
@@ -866,12 +881,11 @@ var INSPECTOR_BODY_LINES = 16;
|
|
|
866
881
|
var SUPPORTED_PI_MAJOR = 0;
|
|
867
882
|
var MIN_SUPPORTED_PI_MINOR = 80;
|
|
868
883
|
var MIN_SUPPORTED_PI_PATCH = 6;
|
|
869
|
-
var MAX_SUPPORTED_PI_MINOR = 81;
|
|
870
884
|
var cachedPiPackage;
|
|
871
885
|
var piPackageResolved = false;
|
|
872
886
|
function isSupportedPiVersion(version) {
|
|
873
887
|
const [major, minor = -1, patch = -1] = version;
|
|
874
|
-
return version.length === 3 && Number.isInteger(major) && Number.isInteger(minor) && Number.isInteger(patch) && major === SUPPORTED_PI_MAJOR && minor
|
|
888
|
+
return version.length === 3 && Number.isInteger(major) && Number.isInteger(minor) && Number.isInteger(patch) && major === SUPPORTED_PI_MAJOR && (minor > MIN_SUPPORTED_PI_MINOR || minor === MIN_SUPPORTED_PI_MINOR && patch >= MIN_SUPPORTED_PI_PATCH);
|
|
875
889
|
}
|
|
876
890
|
function resolvePiPackage() {
|
|
877
891
|
if (piPackageResolved)
|
|
@@ -1483,14 +1497,18 @@ ${this.primaryAgent.prompt}` };
|
|
|
1483
1497
|
ctx.ui.notify("Subagent inspection is available in TUI mode", "warning");
|
|
1484
1498
|
return;
|
|
1485
1499
|
}
|
|
1500
|
+
const catalog = this.getAgentCatalog(ctx);
|
|
1501
|
+
for (const warning of catalog.warnings)
|
|
1502
|
+
ctx.ui.notify(warning, "warning");
|
|
1503
|
+
for (const diagnostic of catalog.diagnostics)
|
|
1504
|
+
ctx.ui.notify(diagnostic, "warning");
|
|
1505
|
+
const agents = availableSubagents(catalog).sort((a, b) => a.name.localeCompare(b.name));
|
|
1486
1506
|
const tasks = [...this.tasks.values()];
|
|
1487
|
-
if (tasks.length === 0) {
|
|
1488
|
-
ctx.ui.notify("No subagent sessions in this session", "info");
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
1507
|
const requested = args.trim();
|
|
1492
|
-
let
|
|
1493
|
-
|
|
1508
|
+
let tab = requested ? "sessions" : "agents";
|
|
1509
|
+
let selectedAgent = 0;
|
|
1510
|
+
let selectedTask = requested ? tasks.findIndex((task) => task.id === requested || task.id.startsWith(requested)) : 0;
|
|
1511
|
+
if (selectedTask < 0) {
|
|
1494
1512
|
ctx.ui.notify(`Unknown subagent task: ${requested}`, "error");
|
|
1495
1513
|
return;
|
|
1496
1514
|
}
|
|
@@ -1499,27 +1517,54 @@ ${this.primaryAgent.prompt}` };
|
|
|
1499
1517
|
await ctx.ui.custom((tui, theme, _keybindings, done) => ({
|
|
1500
1518
|
render: (width) => {
|
|
1501
1519
|
const contentWidth = Math.max(1, width - 4);
|
|
1502
|
-
const box = (
|
|
1503
|
-
boxTop(theme, width,
|
|
1520
|
+
const box = (lines2) => [
|
|
1521
|
+
boxTop(theme, width, "Subagents"),
|
|
1504
1522
|
...lines2.map((line) => boxRow(theme, width, line)),
|
|
1505
1523
|
boxBottom(theme, width)
|
|
1506
1524
|
];
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1525
|
+
const tabLabel = (label, active) => active ? theme.fg("accent", theme.bold(`[${label}]`)) : theme.fg("muted", label);
|
|
1526
|
+
const tabs = `${tabLabel("Agents", tab === "agents")} ${tabLabel(`Sessions ${tasks.length}`, tab === "sessions")}`;
|
|
1527
|
+
if (tab === "agents") {
|
|
1528
|
+
const start = Math.max(0, Math.min(selectedAgent - 5, agents.length - 11));
|
|
1529
|
+
const shown2 = agents.slice(start, start + 11);
|
|
1530
|
+
const nameWidth = Math.min(24, Math.max(5, ...agents.map((agent) => agent.name.length + 1)));
|
|
1531
|
+
const lines2 = [
|
|
1532
|
+
tabs,
|
|
1533
|
+
"",
|
|
1534
|
+
` ${"AGENT".padEnd(nameWidth)} ${"SOURCE".padEnd(8)} DESCRIPTION`
|
|
1535
|
+
];
|
|
1536
|
+
for (const [offset, agent] of shown2.entries()) {
|
|
1512
1537
|
const index = start + offset;
|
|
1513
|
-
const cursor = index ===
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1538
|
+
const cursor = index === selectedAgent ? theme.fg("accent", "›") : " ";
|
|
1539
|
+
const name = `@${agent.name}`.padEnd(nameWidth);
|
|
1540
|
+
const description = `${agent.description ?? "No description"}${agent.hidden ? " [hidden]" : ""}`;
|
|
1541
|
+
lines2.push(`${cursor} ${theme.fg("accent", name)} ${theme.fg("dim", agent.source.padEnd(8))} ${theme.fg("text", description)}`);
|
|
1542
|
+
}
|
|
1543
|
+
if (agents.length === 0)
|
|
1544
|
+
lines2.push(" No subagent types are configured.");
|
|
1545
|
+
lines2.push("", theme.fg("dim", "↑↓ select tab/→ sessions esc close"));
|
|
1546
|
+
return box(lines2);
|
|
1547
|
+
}
|
|
1548
|
+
if (!detail) {
|
|
1549
|
+
const lines2 = [tabs, ""];
|
|
1550
|
+
if (tasks.length === 0) {
|
|
1551
|
+
lines2.push("No subagent sessions in this session.");
|
|
1552
|
+
} else {
|
|
1553
|
+
const start = Math.max(0, Math.min(selectedTask - 5, tasks.length - 11));
|
|
1554
|
+
const shown2 = tasks.slice(start, start + 11);
|
|
1555
|
+
for (const [offset, task2] of shown2.entries()) {
|
|
1556
|
+
const index = start + offset;
|
|
1557
|
+
const cursor = index === selectedTask ? theme.fg("accent", "›") : " ";
|
|
1558
|
+
const indent = " ".repeat(Math.max(0, task2.depth - 1));
|
|
1559
|
+
lines2.push(`${cursor} ${indent}${taskState(theme, task2)} ${theme.fg("accent", `@${task2.agent}`)} ${theme.fg("text", task2.description)} ${theme.fg("dim", task2.id.slice(0, 8))}`);
|
|
1560
|
+
}
|
|
1516
1561
|
}
|
|
1517
|
-
lines2.push("", theme.fg("dim", "↑↓ select enter inspect esc close"));
|
|
1518
|
-
return box(
|
|
1562
|
+
lines2.push("", theme.fg("dim", "↑↓ select enter inspect tab/← agents esc close"));
|
|
1563
|
+
return box(lines2);
|
|
1519
1564
|
}
|
|
1520
|
-
const task = tasks[
|
|
1565
|
+
const task = tasks[selectedTask];
|
|
1521
1566
|
if (!task)
|
|
1522
|
-
return [];
|
|
1567
|
+
return box([tabs, "", "No subagent sessions in this session."]);
|
|
1523
1568
|
const siblings = tasks.filter((candidate) => candidate.parentTaskId === task.parentTaskId);
|
|
1524
1569
|
const siblingIndex = siblings.findIndex((candidate) => candidate.id === task.id);
|
|
1525
1570
|
const transcript = taskTranscript(task).flatMap((line) => line.split(`
|
|
@@ -1535,28 +1580,53 @@ ${this.primaryAgent.prompt}` };
|
|
|
1535
1580
|
duration
|
|
1536
1581
|
].filter(Boolean);
|
|
1537
1582
|
const lines = [
|
|
1583
|
+
tabs,
|
|
1584
|
+
"",
|
|
1538
1585
|
`${theme.fg("accent", theme.bold(`@${task.agent}`))} ${theme.fg("text", task.description)}`,
|
|
1539
1586
|
`${taskState(theme, task)} ${theme.fg("dim", metrics.join(" · "))}`,
|
|
1540
1587
|
"",
|
|
1541
1588
|
...shown.map((line) => theme.fg("toolOutput", line))
|
|
1542
1589
|
];
|
|
1543
|
-
while (lines.length < INSPECTOR_BODY_LINES +
|
|
1590
|
+
while (lines.length < INSPECTOR_BODY_LINES + 5)
|
|
1544
1591
|
lines.push("");
|
|
1545
1592
|
if (transcript.length > INSPECTOR_BODY_LINES) {
|
|
1546
1593
|
lines.push(theme.fg("dim", `${scroll + 1}-${scroll + shown.length} of ${transcript.length}`));
|
|
1547
1594
|
}
|
|
1548
|
-
lines.push(theme.fg("dim", "Parent p/backspace Prev ← Next → ↑↓ scroll
|
|
1549
|
-
return box(
|
|
1595
|
+
lines.push(theme.fg("dim", "Parent p/backspace Prev ← Next → ↑↓ scroll tab agents esc back"));
|
|
1596
|
+
return box(lines.map((line) => truncateToWidth2(line, contentWidth)));
|
|
1550
1597
|
},
|
|
1551
1598
|
handleInput: (data) => {
|
|
1599
|
+
if (matchesKey(data, "tab")) {
|
|
1600
|
+
tab = tab === "agents" ? "sessions" : "agents";
|
|
1601
|
+
detail = false;
|
|
1602
|
+
scroll = 0;
|
|
1603
|
+
tui.requestRender();
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (tab === "agents") {
|
|
1607
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
|
|
1608
|
+
done();
|
|
1609
|
+
else if (matchesKey(data, "up"))
|
|
1610
|
+
selectedAgent = Math.max(0, selectedAgent - 1);
|
|
1611
|
+
else if (matchesKey(data, "down") && agents.length > 0) {
|
|
1612
|
+
selectedAgent = Math.min(agents.length - 1, selectedAgent + 1);
|
|
1613
|
+
} else if (matchesKey(data, "right")) {
|
|
1614
|
+
tab = "sessions";
|
|
1615
|
+
} else
|
|
1616
|
+
return;
|
|
1617
|
+
tui.requestRender();
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1552
1620
|
if (!detail) {
|
|
1553
1621
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
|
|
1554
1622
|
done();
|
|
1623
|
+
else if (matchesKey(data, "left"))
|
|
1624
|
+
tab = "agents";
|
|
1555
1625
|
else if (matchesKey(data, "up"))
|
|
1556
|
-
|
|
1557
|
-
else if (matchesKey(data, "down"))
|
|
1558
|
-
|
|
1559
|
-
else if (matchesKey(data, "return")) {
|
|
1626
|
+
selectedTask = Math.max(0, selectedTask - 1);
|
|
1627
|
+
else if (matchesKey(data, "down") && tasks.length > 0) {
|
|
1628
|
+
selectedTask = Math.min(tasks.length - 1, selectedTask + 1);
|
|
1629
|
+
} else if (matchesKey(data, "return") && tasks.length > 0) {
|
|
1560
1630
|
detail = true;
|
|
1561
1631
|
scroll = 0;
|
|
1562
1632
|
} else
|
|
@@ -1564,7 +1634,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
1564
1634
|
tui.requestRender();
|
|
1565
1635
|
return;
|
|
1566
1636
|
}
|
|
1567
|
-
const task = tasks[
|
|
1637
|
+
const task = tasks[selectedTask];
|
|
1568
1638
|
if (!task)
|
|
1569
1639
|
return;
|
|
1570
1640
|
if (matchesKey(data, "escape")) {
|
|
@@ -1579,7 +1649,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
1579
1649
|
} else if (data === "p" || matchesKey(data, "backspace")) {
|
|
1580
1650
|
const parentIndex = task.parentTaskId ? tasks.findIndex((candidate) => candidate.id === task.parentTaskId) : -1;
|
|
1581
1651
|
if (parentIndex >= 0)
|
|
1582
|
-
|
|
1652
|
+
selectedTask = parentIndex;
|
|
1583
1653
|
else
|
|
1584
1654
|
detail = false;
|
|
1585
1655
|
scroll = 0;
|
|
@@ -1589,7 +1659,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
1589
1659
|
const offset = matchesKey(data, "left") ? -1 : 1;
|
|
1590
1660
|
const sibling = siblings[siblingIndex + offset];
|
|
1591
1661
|
if (sibling)
|
|
1592
|
-
|
|
1662
|
+
selectedTask = tasks.indexOf(sibling);
|
|
1593
1663
|
scroll = 0;
|
|
1594
1664
|
} else if (data !== "r")
|
|
1595
1665
|
return;
|
|
@@ -2188,7 +2258,7 @@ ${catalog.diagnostics.join(`
|
|
|
2188
2258
|
throw new Error("Unable to resolve the running Pi package; process-backed subagents are unavailable");
|
|
2189
2259
|
}
|
|
2190
2260
|
if (!isSupportedPiVersion(pkg.version)) {
|
|
2191
|
-
throw new Error(`Process-backed subagents require Pi >=0.80.6
|
|
2261
|
+
throw new Error(`Process-backed subagents require Pi >=0.80.6; found ${pkg.version.join(".")}`);
|
|
2192
2262
|
}
|
|
2193
2263
|
}
|
|
2194
2264
|
async handleWorkerRequest(task, rules, ctx, request) {
|
|
@@ -2575,7 +2645,9 @@ function loadSandboxConfig(cwd, includeProject) {
|
|
|
2575
2645
|
if (includeProject && existsSync4(projectConfigPath)) {
|
|
2576
2646
|
try {
|
|
2577
2647
|
const projectConfig = JSON.parse(readFileSync4(projectConfigPath, "utf-8"));
|
|
2578
|
-
|
|
2648
|
+
const merged = deepMerge(globalConfig, projectConfig);
|
|
2649
|
+
merged.windows.backend = globalConfig.windows.backend;
|
|
2650
|
+
return merged;
|
|
2579
2651
|
} catch (error) {
|
|
2580
2652
|
console.error(`Warning: Could not parse ${projectConfigPath}: ${error}`);
|
|
2581
2653
|
}
|
|
@@ -2590,6 +2662,7 @@ function mergeArray(base, override) {
|
|
|
2590
2662
|
function deepMerge(base, overrides) {
|
|
2591
2663
|
const network = overrides.network;
|
|
2592
2664
|
const filesystem = overrides.filesystem;
|
|
2665
|
+
const windows = overrides.windows;
|
|
2593
2666
|
return {
|
|
2594
2667
|
enabled: overrides.enabled ?? base.enabled,
|
|
2595
2668
|
network: {
|
|
@@ -2605,9 +2678,43 @@ function deepMerge(base, overrides) {
|
|
|
2605
2678
|
allowRead: mergeArray(base.filesystem.allowRead, filesystem?.allowRead),
|
|
2606
2679
|
allowWrite: mergeArray(base.filesystem.allowWrite, filesystem?.allowWrite),
|
|
2607
2680
|
denyWrite: mergeArray(base.filesystem.denyWrite, filesystem?.denyWrite)
|
|
2681
|
+
},
|
|
2682
|
+
windows: {
|
|
2683
|
+
backend: windows?.backend ?? base.windows.backend,
|
|
2684
|
+
appContainerMode: windows?.appContainerMode ?? base.windows.appContainerMode,
|
|
2685
|
+
allowLoopback: windows?.allowLoopback ?? base.windows.allowLoopback
|
|
2608
2686
|
}
|
|
2609
2687
|
};
|
|
2610
2688
|
}
|
|
2689
|
+
function windowsBackendArgs(config) {
|
|
2690
|
+
if (process.platform !== "win32")
|
|
2691
|
+
return [];
|
|
2692
|
+
const backend = config.windows.backend === "restrictedUser" ? "restricted-user" : "app-container";
|
|
2693
|
+
return ["--windows-backend", backend];
|
|
2694
|
+
}
|
|
2695
|
+
function shouldStartProxy(config) {
|
|
2696
|
+
if (config.network.allowNetwork)
|
|
2697
|
+
return false;
|
|
2698
|
+
return process.platform !== "win32" || config.windows.backend === "restrictedUser" || config.windows.allowLoopback;
|
|
2699
|
+
}
|
|
2700
|
+
function restrictedUserProxyPortRange(config) {
|
|
2701
|
+
if (process.platform !== "win32" || config.windows.backend !== "restrictedUser")
|
|
2702
|
+
return;
|
|
2703
|
+
const result = spawnSync(binaryPath(), ["windows", "status"], {
|
|
2704
|
+
encoding: "utf-8",
|
|
2705
|
+
windowsHide: true
|
|
2706
|
+
});
|
|
2707
|
+
if (result.error)
|
|
2708
|
+
throw result.error;
|
|
2709
|
+
if (result.status !== 0) {
|
|
2710
|
+
throw new Error(result.stderr.trim() || "Restricted-user backend status check failed");
|
|
2711
|
+
}
|
|
2712
|
+
const status = JSON.parse(result.stdout);
|
|
2713
|
+
if (status.installed !== true || status.healthy !== true || !Number.isInteger(status.proxyPortLow) || !Number.isInteger(status.proxyPortHigh) || status.proxyPortLow < 1 || status.proxyPortHigh < status.proxyPortLow) {
|
|
2714
|
+
throw new Error('The Windows restricted-user backend is not installed or healthy. Run "landstrip windows setup".');
|
|
2715
|
+
}
|
|
2716
|
+
return { low: status.proxyPortLow, high: status.proxyPortHigh };
|
|
2717
|
+
}
|
|
2611
2718
|
function getConfigPaths(cwd) {
|
|
2612
2719
|
return getPiConfigPaths(cwd, "sandbox.json");
|
|
2613
2720
|
}
|
|
@@ -2717,10 +2824,13 @@ function canonicalizePath(filePath, cwd) {
|
|
|
2717
2824
|
}
|
|
2718
2825
|
}
|
|
2719
2826
|
}
|
|
2827
|
+
function normalizePathSeparators(path) {
|
|
2828
|
+
return process.platform === "win32" ? path.replaceAll("\\", "/") : path;
|
|
2829
|
+
}
|
|
2720
2830
|
function matchesPattern(filePath, patterns, cwd) {
|
|
2721
|
-
const abs = canonicalizePath(filePath, cwd);
|
|
2831
|
+
const abs = normalizePathSeparators(canonicalizePath(filePath, cwd));
|
|
2722
2832
|
return patterns.some((pattern) => {
|
|
2723
|
-
const absPattern = pattern.includes("*") ? expandPath(pattern, cwd) : canonicalizePath(pattern, cwd);
|
|
2833
|
+
const absPattern = normalizePathSeparators(pattern.includes("*") ? expandPath(pattern, cwd) : canonicalizePath(pattern, cwd));
|
|
2724
2834
|
if (pattern.includes("*")) {
|
|
2725
2835
|
const escaped = absPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\/|\*\*|\*/g, (token) => token === "**/" ? "(?:.*/)?" : token === "**" ? ".*" : "[^/]*");
|
|
2726
2836
|
return new RegExp(`^${escaped}$`).test(abs);
|
|
@@ -2730,16 +2840,20 @@ function matchesPattern(filePath, patterns, cwd) {
|
|
|
2730
2840
|
});
|
|
2731
2841
|
}
|
|
2732
2842
|
function normalizeBlockedPath(path, cwd) {
|
|
2733
|
-
|
|
2843
|
+
const nativePath = process.platform === "win32" && /^\/[a-zA-Z](?:\/|$)/.test(path) ? `${path[1]}:${path.slice(2)}` : path;
|
|
2844
|
+
return canonicalizePath(isAbsolute2(nativePath) ? nativePath : join5(cwd, nativePath), cwd);
|
|
2734
2845
|
}
|
|
2735
2846
|
function pathUnderDirectory(filePath, dir) {
|
|
2736
|
-
|
|
2847
|
+
const normalizedFilePath = normalizePathSeparators(filePath);
|
|
2848
|
+
const normalizedDir = normalizePathSeparators(dir);
|
|
2849
|
+
if (normalizedFilePath === normalizedDir)
|
|
2737
2850
|
return true;
|
|
2738
|
-
const sep2 =
|
|
2739
|
-
return
|
|
2851
|
+
const sep2 = normalizedDir.endsWith("/") ? "" : "/";
|
|
2852
|
+
return normalizedFilePath.startsWith(normalizedDir + sep2);
|
|
2740
2853
|
}
|
|
2741
2854
|
function sessionScopeFor(filePath, baseDirectory) {
|
|
2742
2855
|
const dir = dirname3(filePath);
|
|
2856
|
+
const normalizedDir = normalizePathSeparators(dir);
|
|
2743
2857
|
const home = homedir3();
|
|
2744
2858
|
const boundaries = new Set;
|
|
2745
2859
|
if (home)
|
|
@@ -2750,12 +2864,13 @@ function sessionScopeFor(filePath, baseDirectory) {
|
|
|
2750
2864
|
boundaries.add(realHome);
|
|
2751
2865
|
} catch {}
|
|
2752
2866
|
for (const boundary of boundaries) {
|
|
2867
|
+
const normalizedBoundary = normalizePathSeparators(boundary);
|
|
2753
2868
|
if (pathUnderDirectory(dir, boundary)) {
|
|
2754
|
-
const rest =
|
|
2869
|
+
const rest = normalizedDir.slice(normalizedBoundary.length).replace(/^\/+/, "");
|
|
2755
2870
|
const first = rest.split("/")[0];
|
|
2756
2871
|
if (!first)
|
|
2757
2872
|
return filePath;
|
|
2758
|
-
return
|
|
2873
|
+
return join5(boundary, first);
|
|
2759
2874
|
}
|
|
2760
2875
|
}
|
|
2761
2876
|
if (pathUnderDirectory(dir, baseDirectory))
|
|
@@ -2781,7 +2896,7 @@ function readAllowed(path, allowRead, denyRead, cwd) {
|
|
|
2781
2896
|
}
|
|
2782
2897
|
function isPathLike(value) {
|
|
2783
2898
|
const trimmed = value.trim();
|
|
2784
|
-
return trimmed === "~" || trimmed.startsWith("/") || trimmed.startsWith("~/") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith(".") || trimmed.includes("/");
|
|
2899
|
+
return trimmed === "~" || trimmed.startsWith("/") || trimmed.startsWith("\\\\") || /^[a-zA-Z]:[\\/]/.test(trimmed) || trimmed.startsWith("~/") || trimmed.startsWith("~\\") || trimmed.startsWith("./") || trimmed.startsWith(".\\") || trimmed.startsWith("../") || trimmed.startsWith("..\\") || trimmed.startsWith(".") || trimmed.includes("/") || trimmed.includes("\\");
|
|
2785
2900
|
}
|
|
2786
2901
|
function normalizePathMatch(value, cwd) {
|
|
2787
2902
|
return isPathLike(value) ? normalizeBlockedPath(value, cwd) : null;
|
|
@@ -2803,28 +2918,27 @@ function extractBlockedPath(trapOutput, cwd) {
|
|
|
2803
2918
|
return null;
|
|
2804
2919
|
}
|
|
2805
2920
|
function extractNativeDeniedPath(output, cwd) {
|
|
2806
|
-
|
|
2921
|
+
const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
|
|
2922
|
+
let match = output.match(new RegExp(String.raw`['"]([^'"\n]+)['"][^\r\n]{0,120}${denial}`, "i"));
|
|
2807
2923
|
if (match)
|
|
2808
2924
|
return normalizePathMatch(match[1], cwd);
|
|
2809
|
-
match = output.match(
|
|
2925
|
+
match = output.match(new RegExp(String.raw`(?:^|:\s+)((?:[a-zA-Z]:[\\/]|\\\\|/|\.{1,2}[\\/])[^\r\n]*?):\s+${denial}$`, "im"));
|
|
2810
2926
|
if (match)
|
|
2811
2927
|
return normalizePathMatch(match[1], cwd);
|
|
2812
|
-
match = output.match(
|
|
2928
|
+
match = output.match(new RegExp(String.raw`^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: ${denial}$`, "im"));
|
|
2813
2929
|
if (match)
|
|
2814
2930
|
return normalizePathMatch(match[1], cwd);
|
|
2815
|
-
match = output.match(/^[a-zA-Z0-9_-]+: (\/[^:\n]+): (?:Operation not permitted|Permission denied)$/m);
|
|
2816
|
-
if (match)
|
|
2817
|
-
return normalizeBlockedPath(match[1], cwd);
|
|
2818
2931
|
return null;
|
|
2819
2932
|
}
|
|
2820
2933
|
function extractNativeWriteDeniedPath(output, cwd) {
|
|
2821
|
-
|
|
2934
|
+
const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
|
|
2935
|
+
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"));
|
|
2822
2936
|
if (match)
|
|
2823
2937
|
return normalizePathMatch(match[1], cwd);
|
|
2824
|
-
match = output.match(
|
|
2938
|
+
match = output.match(new RegExp(String.raw`^[a-zA-Z0-9_-]+: cannot create(?: directory)? '?([^'\n]+?)'?(?: for writing)?: ${denial}$`, "im"));
|
|
2825
2939
|
if (match)
|
|
2826
2940
|
return normalizePathMatch(match[1], cwd);
|
|
2827
|
-
match = output.match(
|
|
2941
|
+
match = output.match(new RegExp(String.raw`^[a-zA-Z0-9_-]+: couldn't open temporary file ((?:[a-zA-Z]:[\\/]|\\\\|/)[^\r\n]*?): ${denial}$`, "im"));
|
|
2828
2942
|
if (match)
|
|
2829
2943
|
return normalizePathMatch(match[1], cwd);
|
|
2830
2944
|
return null;
|
|
@@ -3010,7 +3124,7 @@ function landstripDisplayPath() {
|
|
|
3010
3124
|
return "unavailable";
|
|
3011
3125
|
}
|
|
3012
3126
|
}
|
|
3013
|
-
function writeEnvFile(env, proxyPort) {
|
|
3127
|
+
function writeEnvFile(env, proxyPort, proxyToken) {
|
|
3014
3128
|
const lines = [];
|
|
3015
3129
|
for (const [key, value] of Object.entries(env)) {
|
|
3016
3130
|
if (value === undefined)
|
|
@@ -3021,7 +3135,8 @@ function writeEnvFile(env, proxyPort) {
|
|
|
3021
3135
|
lines.push(`export ${key}='${escaped}'`);
|
|
3022
3136
|
}
|
|
3023
3137
|
if (proxyPort !== null) {
|
|
3024
|
-
const
|
|
3138
|
+
const credentials = proxyToken === undefined ? "" : `landstrip:${proxyToken}@`;
|
|
3139
|
+
const url = `http://${credentials}127.0.0.1:${proxyPort}`;
|
|
3025
3140
|
for (const name of PROXY_ENVIRONMENT_VARIABLES) {
|
|
3026
3141
|
lines.push(`export ${name}='${url}'`);
|
|
3027
3142
|
}
|
|
@@ -3208,11 +3323,45 @@ function createLandstripIntegration(options = {}) {
|
|
|
3208
3323
|
function getEffectiveAllowedDomains(config, allowances) {
|
|
3209
3324
|
return mergeAllowances(config.network.allowedDomains, sessionAllowedDomains, allowances?.domains);
|
|
3210
3325
|
}
|
|
3211
|
-
function getEffectiveAllowRead(config, allowances) {
|
|
3212
|
-
|
|
3326
|
+
function getEffectiveAllowRead(config, cwd, allowances) {
|
|
3327
|
+
const paths = mergeAllowances(config.filesystem.allowRead, sessionAllowedReadPaths, allowances?.readPaths);
|
|
3328
|
+
if (process.platform !== "win32")
|
|
3329
|
+
return paths;
|
|
3330
|
+
return paths.filter((path) => !path.startsWith("/") && (path.includes("*") || existsSync4(expandPath(path, cwd))));
|
|
3213
3331
|
}
|
|
3214
3332
|
function getEffectiveAllowWrite(config, allowances) {
|
|
3215
|
-
|
|
3333
|
+
const paths = mergeAllowances(config.filesystem.allowWrite, sessionAllowedWritePaths, allowances?.writePaths);
|
|
3334
|
+
return process.platform === "win32" ? paths.filter((path) => !path.startsWith("/")) : paths;
|
|
3335
|
+
}
|
|
3336
|
+
function getEffectiveDenyRead(config, cwd) {
|
|
3337
|
+
if (process.platform !== "win32")
|
|
3338
|
+
return config.filesystem.denyRead;
|
|
3339
|
+
const volumeRoot = parse(resolve2(cwd)).root;
|
|
3340
|
+
return [...new Set([...config.filesystem.denyRead, volumeRoot])];
|
|
3341
|
+
}
|
|
3342
|
+
function hasBaselineWindowsApplicationAccess(filePath) {
|
|
3343
|
+
const normalizedPath = normalizePathSeparators(resolve2(filePath)).toLowerCase();
|
|
3344
|
+
const systemRoots = [
|
|
3345
|
+
process.env.ProgramFiles,
|
|
3346
|
+
process.env["ProgramFiles(x86)"],
|
|
3347
|
+
process.env.ProgramW6432,
|
|
3348
|
+
process.env.SystemRoot
|
|
3349
|
+
];
|
|
3350
|
+
return systemRoots.some((root) => {
|
|
3351
|
+
if (!root)
|
|
3352
|
+
return false;
|
|
3353
|
+
return pathUnderDirectory(normalizedPath, normalizePathSeparators(resolve2(root)).toLowerCase());
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
function withWindowsProcessReadAccess(allowances, command, cwd, additionalPaths = []) {
|
|
3357
|
+
if (process.platform !== "win32")
|
|
3358
|
+
return allowances;
|
|
3359
|
+
const executable = resolve2(cwd, command);
|
|
3360
|
+
const executablePaths = hasBaselineWindowsApplicationAccess(executable) ? [] : [executable];
|
|
3361
|
+
return {
|
|
3362
|
+
...allowances,
|
|
3363
|
+
readPaths: [...new Set([...allowances.readPaths, ...executablePaths, ...additionalPaths])]
|
|
3364
|
+
};
|
|
3216
3365
|
}
|
|
3217
3366
|
async function applyDomainChoice(choice, domain, cwd, allowances) {
|
|
3218
3367
|
const { globalPath, projectPath } = getConfigPaths(cwd);
|
|
@@ -3275,11 +3424,12 @@ function createLandstripIntegration(options = {}) {
|
|
|
3275
3424
|
...proxyPort !== null ? { httpProxyPort: proxyPort } : {}
|
|
3276
3425
|
},
|
|
3277
3426
|
filesystem: {
|
|
3278
|
-
denyRead: config
|
|
3279
|
-
allowRead: getEffectiveAllowRead(config, allowances),
|
|
3427
|
+
denyRead: getEffectiveDenyRead(config, cwd),
|
|
3428
|
+
allowRead: getEffectiveAllowRead(config, cwd, allowances),
|
|
3280
3429
|
allowWrite: getEffectiveAllowWrite(config, allowances),
|
|
3281
3430
|
denyWrite: config.filesystem.denyWrite
|
|
3282
|
-
}
|
|
3431
|
+
},
|
|
3432
|
+
windows: config.windows
|
|
3283
3433
|
};
|
|
3284
3434
|
}
|
|
3285
3435
|
function writePolicyFile(cwd, proxyPort, allowances) {
|
|
@@ -3289,7 +3439,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3289
3439
|
`, "utf-8");
|
|
3290
3440
|
return { dir, path };
|
|
3291
3441
|
}
|
|
3292
|
-
function startProxy(ctx, allowances, signal, authorization) {
|
|
3442
|
+
function startProxy(ctx, allowances, signal, authorization, portRange) {
|
|
3293
3443
|
const sockets = new Set;
|
|
3294
3444
|
async function domainAllowed(domain) {
|
|
3295
3445
|
const config = loadConfig(ctx.cwd);
|
|
@@ -3421,24 +3571,33 @@ function createLandstripIntegration(options = {}) {
|
|
|
3421
3571
|
const server = createServer(handleClient);
|
|
3422
3572
|
let stopped = false;
|
|
3423
3573
|
return new Promise((resolve3, reject) => {
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
if (stopped) {
|
|
3432
|
-
done();
|
|
3433
|
-
return;
|
|
3434
|
-
}
|
|
3435
|
-
stopped = true;
|
|
3436
|
-
for (const socket of sockets)
|
|
3437
|
-
socket.destroy();
|
|
3438
|
-
server.close(() => done());
|
|
3439
|
-
})
|
|
3574
|
+
const listen = (port) => {
|
|
3575
|
+
server.once("error", (error) => {
|
|
3576
|
+
if (error.code === "EADDRINUSE" && portRange && port < portRange.high) {
|
|
3577
|
+
listen(port + 1);
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
reject(error);
|
|
3440
3581
|
});
|
|
3441
|
-
|
|
3582
|
+
server.listen(port, "127.0.0.1", () => {
|
|
3583
|
+
server.removeAllListeners("error");
|
|
3584
|
+
const address = server.address();
|
|
3585
|
+
resolve3({
|
|
3586
|
+
port: address.port,
|
|
3587
|
+
stop: () => new Promise((done) => {
|
|
3588
|
+
if (stopped) {
|
|
3589
|
+
done();
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
stopped = true;
|
|
3593
|
+
for (const socket of sockets)
|
|
3594
|
+
socket.destroy();
|
|
3595
|
+
server.close(() => done());
|
|
3596
|
+
})
|
|
3597
|
+
});
|
|
3598
|
+
});
|
|
3599
|
+
};
|
|
3600
|
+
listen(portRange?.low ?? 0);
|
|
3442
3601
|
});
|
|
3443
3602
|
}
|
|
3444
3603
|
function createSocketPair() {
|
|
@@ -3479,7 +3638,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3479
3638
|
}
|
|
3480
3639
|
const path = normalizeBlockedPath(trap.path, cwd);
|
|
3481
3640
|
const config = loadConfig(cwd);
|
|
3482
|
-
const allowed = trap.operation === "read" ? readAllowed(path, getEffectiveAllowRead(config, allowances), config.filesystem.denyRead, cwd) : !matchesPattern(path, config.filesystem.denyWrite, cwd) && !shouldPromptForWrite(path, getEffectiveAllowWrite(config, allowances), cwd);
|
|
3641
|
+
const allowed = trap.operation === "read" ? readAllowed(path, getEffectiveAllowRead(config, cwd, allowances), config.filesystem.denyRead, cwd) : !matchesPattern(path, config.filesystem.denyWrite, cwd) && !shouldPromptForWrite(path, getEffectiveAllowWrite(config, allowances), cwd);
|
|
3483
3642
|
if (allowed)
|
|
3484
3643
|
return { action: "allow" };
|
|
3485
3644
|
if (trap.operation === "write" && matchesPattern(path, config.filesystem.denyWrite, cwd)) {
|
|
@@ -3599,10 +3758,11 @@ function createLandstripIntegration(options = {}) {
|
|
|
3599
3758
|
writePaths: [...options2.writePaths],
|
|
3600
3759
|
targets: []
|
|
3601
3760
|
};
|
|
3761
|
+
const processAllowances = withWindowsProcessReadAccess(allowances, options2.command, options2.cwd);
|
|
3602
3762
|
const config = loadConfig(options2.cwd);
|
|
3603
3763
|
const proxyToken = randomBytes(32).toString("base64url");
|
|
3604
3764
|
const proxyAuthorization = `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString("base64")}`;
|
|
3605
|
-
const proxy = config
|
|
3765
|
+
const proxy = shouldStartProxy(config) ? await startProxy(options2.ctx, processAllowances, options2.signal, proxyAuthorization, restrictedUserProxyPortRange(config)) : null;
|
|
3606
3766
|
let policy;
|
|
3607
3767
|
let trapSocket;
|
|
3608
3768
|
let childEnd;
|
|
@@ -3610,11 +3770,13 @@ function createLandstripIntegration(options = {}) {
|
|
|
3610
3770
|
try {
|
|
3611
3771
|
if (options2.signal?.aborted)
|
|
3612
3772
|
throw new Error("Task cancelled");
|
|
3613
|
-
policy = writePolicyFile(options2.cwd, proxy?.port ?? null,
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3773
|
+
policy = writePolicyFile(options2.cwd, proxy?.port ?? null, processAllowances);
|
|
3774
|
+
if (process.platform !== "win32") {
|
|
3775
|
+
[trapSocket, childEnd] = await createSocketPair();
|
|
3776
|
+
if (options2.signal?.aborted)
|
|
3777
|
+
throw new Error("Task cancelled");
|
|
3778
|
+
attachWorkerTrap(trapSocket, options2.ctx, options2.cwd, processAllowances, options2.signal);
|
|
3779
|
+
}
|
|
3618
3780
|
} catch (error) {
|
|
3619
3781
|
trapSocket?.destroy();
|
|
3620
3782
|
childEnd?.destroy();
|
|
@@ -3624,6 +3786,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3624
3786
|
throw error;
|
|
3625
3787
|
}
|
|
3626
3788
|
const workerPolicy = policy;
|
|
3789
|
+
const workerTrapSocket = trapSocket;
|
|
3627
3790
|
const workerChildEnd = childEnd;
|
|
3628
3791
|
const workerEnv = { ...options2.env };
|
|
3629
3792
|
if (proxy) {
|
|
@@ -3643,14 +3806,24 @@ function createLandstripIntegration(options = {}) {
|
|
|
3643
3806
|
if (spawned)
|
|
3644
3807
|
throw new Error("Prepared process can only be spawned once");
|
|
3645
3808
|
spawned = true;
|
|
3646
|
-
const
|
|
3809
|
+
const landstripArgs = [
|
|
3810
|
+
...windowsBackendArgs(config),
|
|
3811
|
+
"-p",
|
|
3812
|
+
workerPolicy.path,
|
|
3813
|
+
options2.command,
|
|
3814
|
+
...options2.args
|
|
3815
|
+
];
|
|
3816
|
+
const stdio = workerChildEnd ? ["pipe", "pipe", "pipe", workerChildEnd] : ["pipe", "pipe", "pipe"];
|
|
3817
|
+
if (workerChildEnd)
|
|
3818
|
+
landstripArgs.unshift("--trap-fd", "3");
|
|
3819
|
+
const child = spawn2(binaryPath(), landstripArgs, {
|
|
3647
3820
|
...spawnOptions,
|
|
3648
3821
|
cwd: options2.cwd,
|
|
3649
3822
|
env: workerEnv,
|
|
3650
3823
|
detached: true,
|
|
3651
|
-
stdio
|
|
3824
|
+
stdio
|
|
3652
3825
|
});
|
|
3653
|
-
workerChildEnd
|
|
3826
|
+
workerChildEnd?.destroy();
|
|
3654
3827
|
patchProcessGroupKill(child);
|
|
3655
3828
|
preparedChild = child;
|
|
3656
3829
|
return child;
|
|
@@ -3665,8 +3838,8 @@ function createLandstripIntegration(options = {}) {
|
|
|
3665
3838
|
disposePromise ??= (async () => {
|
|
3666
3839
|
disposed = true;
|
|
3667
3840
|
await stopPreparedChild(preparedChild);
|
|
3668
|
-
|
|
3669
|
-
workerChildEnd
|
|
3841
|
+
workerTrapSocket?.destroy();
|
|
3842
|
+
workerChildEnd?.destroy();
|
|
3670
3843
|
rmSync2(workerPolicy.dir, { recursive: true, force: true });
|
|
3671
3844
|
await proxy?.stop();
|
|
3672
3845
|
})();
|
|
@@ -3692,10 +3865,12 @@ function createLandstripIntegration(options = {}) {
|
|
|
3692
3865
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
3693
3866
|
const { shell, args } = getShellConfig(SettingsManager.create(cwd).getShellPath());
|
|
3694
3867
|
const config = loadConfig(cwd);
|
|
3695
|
-
const
|
|
3696
|
-
const
|
|
3868
|
+
const proxyToken = randomBytes(32).toString("base64url");
|
|
3869
|
+
const proxyAuthorization = `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString("base64")}`;
|
|
3870
|
+
const proxy = shouldStartProxy(config) ? await startProxy(ctx, allowances, signal, proxyAuthorization, restrictedUserProxyPortRange(config)) : null;
|
|
3697
3871
|
let policy;
|
|
3698
3872
|
let envFile;
|
|
3873
|
+
let processAllowances = allowances;
|
|
3699
3874
|
let teardownPromise;
|
|
3700
3875
|
const teardownResources = () => {
|
|
3701
3876
|
teardownPromise ??= (async () => {
|
|
@@ -3709,10 +3884,20 @@ function createLandstripIntegration(options = {}) {
|
|
|
3709
3884
|
};
|
|
3710
3885
|
let landstripArgs;
|
|
3711
3886
|
try {
|
|
3712
|
-
|
|
3713
|
-
|
|
3887
|
+
envFile = writeEnvFile({ ...process.env, ...env, PWD: resolve2(cwd) }, proxy?.port ?? null, proxy ? proxyToken : undefined);
|
|
3888
|
+
processAllowances = withWindowsProcessReadAccess(allowances, shell, cwd, [envFile.path]);
|
|
3889
|
+
policy = writePolicyFile(cwd, proxy?.port ?? null, processAllowances);
|
|
3714
3890
|
const wrappedCommand = `source '${envFile.path}' && ${command}`;
|
|
3715
|
-
landstripArgs = [
|
|
3891
|
+
landstripArgs = [
|
|
3892
|
+
...windowsBackendArgs(config),
|
|
3893
|
+
"-p",
|
|
3894
|
+
policy.path,
|
|
3895
|
+
shell,
|
|
3896
|
+
...args,
|
|
3897
|
+
wrappedCommand
|
|
3898
|
+
];
|
|
3899
|
+
if (process.platform !== "win32")
|
|
3900
|
+
landstripArgs.unshift("--trap-fd", "3");
|
|
3716
3901
|
} catch (error) {
|
|
3717
3902
|
await teardownResources();
|
|
3718
3903
|
throw error;
|
|
@@ -3722,7 +3907,10 @@ function createLandstripIntegration(options = {}) {
|
|
|
3722
3907
|
let timeoutHandle;
|
|
3723
3908
|
let timedOut = false;
|
|
3724
3909
|
let cleaned = false;
|
|
3725
|
-
|
|
3910
|
+
let trapSocket;
|
|
3911
|
+
let childEnd;
|
|
3912
|
+
if (process.platform !== "win32")
|
|
3913
|
+
[trapSocket, childEnd] = await createSocketPair();
|
|
3726
3914
|
const cleanup = async () => {
|
|
3727
3915
|
if (cleaned)
|
|
3728
3916
|
return;
|
|
@@ -3730,24 +3918,20 @@ function createLandstripIntegration(options = {}) {
|
|
|
3730
3918
|
if (timeoutHandle)
|
|
3731
3919
|
clearTimeout(timeoutHandle);
|
|
3732
3920
|
signal?.removeEventListener("abort", onAbort);
|
|
3733
|
-
trapSocket
|
|
3921
|
+
trapSocket?.destroy();
|
|
3734
3922
|
await teardownResources();
|
|
3735
3923
|
};
|
|
3924
|
+
const stdio = childEnd ? ["ignore", "pipe", "pipe", childEnd] : ["ignore", "pipe", "pipe"];
|
|
3736
3925
|
const child = spawn2(binaryPath(), landstripArgs, {
|
|
3737
3926
|
cwd,
|
|
3738
3927
|
env: { PATH: process.env.PATH, HOME: process.env.HOME },
|
|
3739
3928
|
detached: true,
|
|
3740
|
-
stdio
|
|
3929
|
+
stdio
|
|
3741
3930
|
});
|
|
3742
|
-
|
|
3931
|
+
patchProcessGroupKill(child);
|
|
3932
|
+
childEnd?.destroy();
|
|
3743
3933
|
function killChild() {
|
|
3744
|
-
|
|
3745
|
-
return;
|
|
3746
|
-
try {
|
|
3747
|
-
process.kill(-child.pid, "SIGKILL");
|
|
3748
|
-
} catch {
|
|
3749
|
-
child.kill("SIGKILL");
|
|
3750
|
-
}
|
|
3934
|
+
child.kill("SIGKILL");
|
|
3751
3935
|
}
|
|
3752
3936
|
function onAbort() {
|
|
3753
3937
|
killChild();
|
|
@@ -3824,7 +4008,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3824
4008
|
let trapBuffer = "";
|
|
3825
4009
|
let queryChain = Promise.resolve();
|
|
3826
4010
|
const respondQuery = (queryId, action) => {
|
|
3827
|
-
if (trapSocket.destroyed)
|
|
4011
|
+
if (!trapSocket || trapSocket.destroyed)
|
|
3828
4012
|
return;
|
|
3829
4013
|
trapSocket.write(controlResponseLine(queryId, action));
|
|
3830
4014
|
};
|
|
@@ -3834,7 +4018,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3834
4018
|
errorFdAcc += infoLine;
|
|
3835
4019
|
callbacks.onErrorFd?.(Buffer.from(infoLine, "utf8"));
|
|
3836
4020
|
};
|
|
3837
|
-
trapSocket
|
|
4021
|
+
trapSocket?.on("data", (data) => {
|
|
3838
4022
|
trapBuffer += trapDecoder.write(data);
|
|
3839
4023
|
let nl = trapBuffer.indexOf(`
|
|
3840
4024
|
`);
|
|
@@ -3848,7 +4032,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3848
4032
|
const trap = parseTrapLine(line);
|
|
3849
4033
|
if (trap && isQueryTrap(trap)) {
|
|
3850
4034
|
queryChain = queryChain.then(async () => {
|
|
3851
|
-
const result = await resolveTrapQuery(trap, ctx, cwd,
|
|
4035
|
+
const result = await resolveTrapQuery(trap, ctx, cwd, processAllowances, callbacks.promptOnBlock === true, signal);
|
|
3852
4036
|
if (result.reason === "unprompted" || !isFilesystemTrap(trap) && result.reason === "rejected") {
|
|
3853
4037
|
appendErrorLine(line);
|
|
3854
4038
|
}
|
|
@@ -3924,7 +4108,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3924
4108
|
${globalPath}`, "warning");
|
|
3925
4109
|
return null;
|
|
3926
4110
|
}
|
|
3927
|
-
const needsPrompt = operation === "read" ? !matchesPattern(blockedPath, getEffectiveAllowRead(config, allowances), ctx.cwd) : shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config, allowances), ctx.cwd);
|
|
4111
|
+
const needsPrompt = operation === "read" ? !matchesPattern(blockedPath, getEffectiveAllowRead(config, ctx.cwd, allowances), ctx.cwd) : shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config, allowances), ctx.cwd);
|
|
3928
4112
|
if (needsPrompt) {
|
|
3929
4113
|
const choice = operation === "read" ? await promptReadBlock(ctx, blockedPath, matchesPattern(blockedPath, config.filesystem.denyRead, ctx.cwd) ? "granting allowRead will override it" : undefined, signal) : await promptWriteBlock(ctx, blockedPath, signal);
|
|
3930
4114
|
if (choice === "abort")
|
|
@@ -4010,6 +4194,16 @@ ${message}
|
|
|
4010
4194
|
return;
|
|
4011
4195
|
notify(ctx, 'Network sandbox allows all domains because network.allowedDomains contains "*".', "warning");
|
|
4012
4196
|
}
|
|
4197
|
+
function warnIfWindowsSecurityDowngraded(ctx, config) {
|
|
4198
|
+
if (process.platform !== "win32" || config.windows.backend !== "appContainer")
|
|
4199
|
+
return;
|
|
4200
|
+
if (config.windows.appContainerMode === "standard") {
|
|
4201
|
+
notify(ctx, "Windows uses standard AppContainer mode, which is weaker than LPAC isolation.", "warning");
|
|
4202
|
+
}
|
|
4203
|
+
if (config.windows.allowLoopback) {
|
|
4204
|
+
notify(ctx, "Windows loopback access includes all local loopback services, not only the Landstrip proxy.", "warning");
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4013
4207
|
function enableStatus(ctx, config) {
|
|
4014
4208
|
if (!hasTuiStatus(ctx))
|
|
4015
4209
|
return;
|
|
@@ -4021,6 +4215,9 @@ ${message}
|
|
|
4021
4215
|
if (config.network.allowNetwork) {
|
|
4022
4216
|
networkLabel = "unrestricted";
|
|
4023
4217
|
networkColor = "warning";
|
|
4218
|
+
} else if (process.platform === "win32" && config.windows.backend === "appContainer" && !config.windows.allowLoopback) {
|
|
4219
|
+
networkLabel = "blocked";
|
|
4220
|
+
networkColor = "accent";
|
|
4024
4221
|
} else if (allowsAllDomains(config.network.allowedDomains)) {
|
|
4025
4222
|
networkLabel = "any domain";
|
|
4026
4223
|
networkColor = "warning";
|
|
@@ -4031,7 +4228,14 @@ ${message}
|
|
|
4031
4228
|
const sep2 = theme.fg("dim", "·");
|
|
4032
4229
|
const net = theme.fg(networkColor, networkLabel);
|
|
4033
4230
|
const write = theme.fg("accent", `${config.filesystem.allowWrite.length} write paths`);
|
|
4034
|
-
|
|
4231
|
+
const fields = [`${dot} ${label}`, net, write];
|
|
4232
|
+
if (process.platform === "win32") {
|
|
4233
|
+
const backend = config.windows.backend;
|
|
4234
|
+
const standard = backend === "appContainer" && config.windows.appContainerMode === "standard";
|
|
4235
|
+
const label2 = backend === "restrictedUser" ? "restricted user" : `${config.windows.appContainerMode} AppContainer`;
|
|
4236
|
+
fields.splice(1, 0, theme.fg(standard ? "warning" : "accent", label2));
|
|
4237
|
+
}
|
|
4238
|
+
setTuiStatus(ctx, "sandbox", fields.join(` ${sep2} `));
|
|
4035
4239
|
}
|
|
4036
4240
|
const headlessWarnings = new Set;
|
|
4037
4241
|
function warnUnsandboxed(ctx, message, level) {
|
|
@@ -4061,6 +4265,7 @@ ${message}
|
|
|
4061
4265
|
sandboxReady = true;
|
|
4062
4266
|
setSandboxState("enabled", ctx);
|
|
4063
4267
|
warnIfAllDomainsAllowed(ctx, config);
|
|
4268
|
+
warnIfWindowsSecurityDowngraded(ctx, config);
|
|
4064
4269
|
enableStatus(ctx, config);
|
|
4065
4270
|
return true;
|
|
4066
4271
|
}
|
|
@@ -4191,7 +4396,7 @@ ${message}
|
|
|
4191
4396
|
section("Config");
|
|
4192
4397
|
item("project", text(projectPath));
|
|
4193
4398
|
item("global", text(globalPath));
|
|
4194
|
-
const networkMode = config.network.allowNetwork ? "unrestricted" : "proxied";
|
|
4399
|
+
const networkMode = config.network.allowNetwork ? "unrestricted" : process.platform === "win32" && config.windows.backend === "appContainer" && !config.windows.allowLoopback ? "blocked" : "proxied";
|
|
4195
4400
|
section("Network", networkMode);
|
|
4196
4401
|
item("allow network", boolValue(config.network.allowNetwork));
|
|
4197
4402
|
item("allowed", listValue(config.network.allowedDomains));
|
|
@@ -4205,6 +4410,16 @@ ${message}
|
|
|
4205
4410
|
item("allow read", listValue(config.filesystem.allowRead));
|
|
4206
4411
|
item("allow write", listValue(config.filesystem.allowWrite));
|
|
4207
4412
|
item("deny write", listValue(config.filesystem.denyWrite));
|
|
4413
|
+
if (process.platform === "win32") {
|
|
4414
|
+
section("Windows");
|
|
4415
|
+
const backend = config.windows.backend;
|
|
4416
|
+
item("backend", text(backend === "restrictedUser" ? "restricted user" : "AppContainer"));
|
|
4417
|
+
if (backend === "appContainer") {
|
|
4418
|
+
const mode = config.windows.appContainerMode;
|
|
4419
|
+
item("container", mode === "standard" ? theme.fg("warning", `${mode} (weaker)`) : text(mode));
|
|
4420
|
+
item("loopback", boolValue(config.windows.allowLoopback));
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4208
4423
|
if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
|
|
4209
4424
|
section("Session grants");
|
|
4210
4425
|
if (sessionAllowedReadPaths.length > 0) {
|
|
@@ -4479,6 +4694,8 @@ export {
|
|
|
4479
4694
|
isQueryTrap,
|
|
4480
4695
|
isPublicProxyAddress,
|
|
4481
4696
|
formatLandstripTraps,
|
|
4697
|
+
extractNativeWriteDeniedPath,
|
|
4698
|
+
extractNativeDeniedPath,
|
|
4482
4699
|
domainMatchesAny,
|
|
4483
4700
|
pi_landstrip_default as default,
|
|
4484
4701
|
createLandstripIntegration,
|
package/dist/sandbox.json
CHANGED
package/dist/subagents.json
CHANGED
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
"prompt": "Complete the delegated task autonomously. Return one concise result to the parent.",
|
|
20
20
|
"permission": { "todowrite": "deny" }
|
|
21
21
|
},
|
|
22
|
+
"scout": {
|
|
23
|
+
"description": "Fast codebase reconnaissance with concise handoff findings",
|
|
24
|
+
"mode": "subagent",
|
|
25
|
+
"prompt": "Scout the codebase without modifying it. Return concise findings with exact file references, key code, architecture, and a recommended starting point.",
|
|
26
|
+
"permission": {
|
|
27
|
+
"*": "deny",
|
|
28
|
+
"read": {
|
|
29
|
+
"*": "allow",
|
|
30
|
+
"**/*.env": "ask",
|
|
31
|
+
"**/*.env.*": "ask",
|
|
32
|
+
"**/*.env.example": "allow"
|
|
33
|
+
},
|
|
34
|
+
"glob": "allow",
|
|
35
|
+
"grep": "allow",
|
|
36
|
+
"list": "allow"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
22
39
|
"explore": {
|
|
23
40
|
"description": "Fast read-focused agent for exploring codebases",
|
|
24
41
|
"mode": "subagent",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.35",
|
|
4
4
|
"description": "Sandbox-aware Pi subagents and interactive command permissions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"build": "node -e \"const fs = require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.mkdirSync('dist'); for (const file of ['sandbox.json', 'subagents.json']) fs.copyFileSync(file, 'dist/' + file)\" && bun build index.ts --outfile dist/index.ts --target=node --packages=external",
|
|
31
31
|
"prepack": "npm run build",
|
|
32
32
|
"fmt": "oxfmt *.ts package.json tsconfig.json subagents.json sandbox.json .oxfmtrc.json README.md",
|
|
33
|
-
"lint": "oxlint
|
|
33
|
+
"lint": "oxlint .",
|
|
34
34
|
"check": "tsc --noEmit",
|
|
35
35
|
"test": "npm run build && vitest run",
|
|
36
36
|
"all": "npm run fmt && npm run lint && npm run check && npm run test",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"ci:test": "npm run test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@landstrip/landstrip": "^0.17.
|
|
43
|
+
"@landstrip/landstrip": "^0.17.35",
|
|
44
44
|
"minimatch": "^10.2.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/sandbox.json
CHANGED
package/subagents.json
CHANGED
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
"prompt": "Complete the delegated task autonomously. Return one concise result to the parent.",
|
|
20
20
|
"permission": { "todowrite": "deny" }
|
|
21
21
|
},
|
|
22
|
+
"scout": {
|
|
23
|
+
"description": "Fast codebase reconnaissance with concise handoff findings",
|
|
24
|
+
"mode": "subagent",
|
|
25
|
+
"prompt": "Scout the codebase without modifying it. Return concise findings with exact file references, key code, architecture, and a recommended starting point.",
|
|
26
|
+
"permission": {
|
|
27
|
+
"*": "deny",
|
|
28
|
+
"read": {
|
|
29
|
+
"*": "allow",
|
|
30
|
+
"**/*.env": "ask",
|
|
31
|
+
"**/*.env.*": "ask",
|
|
32
|
+
"**/*.env.example": "allow"
|
|
33
|
+
},
|
|
34
|
+
"glob": "allow",
|
|
35
|
+
"grep": "allow",
|
|
36
|
+
"list": "allow"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
22
39
|
"explore": {
|
|
23
40
|
"description": "Fast read-focused agent for exploring codebases",
|
|
24
41
|
"mode": "subagent",
|