pi-landstrip 0.20.2 → 0.20.4
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 +6 -5
- package/dist/index.ts +52 -94
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,9 +18,10 @@ With sandboxing enabled, Bash (`bash`, `!`, `!!`) and workers are OS-isolated.
|
|
|
18
18
|
Primary file tools run in trusted Pi and preflight paths against the sandbox
|
|
19
19
|
filesystem policy. Tool approval never bypasses hard denials.
|
|
20
20
|
|
|
21
|
-
Configuration
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
Configuration is loaded in order from the bundled [`sandbox.json`](sandbox.json),
|
|
22
|
+
`~/.pi/agent/sandbox.json`, and `.pi/sandbox.json` in a trusted project. Later
|
|
23
|
+
files append to list settings and override non-list settings. Persistent
|
|
24
|
+
approvals are written to the global or project file. For policy-only shell reads:
|
|
24
25
|
|
|
25
26
|
```json
|
|
26
27
|
{ "shell": { "readAccess": "policy" } }
|
|
@@ -56,8 +57,8 @@ Configure `~/.pi/agent/landstrip.json`, trusted-project `.pi/landstrip.json`, or
|
|
|
56
57
|
}
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
`maxSubagents
|
|
60
|
-
and permissions.
|
|
60
|
+
`maxSubagents` accepts values from 0 to 16. Agent definitions can override
|
|
61
|
+
model, prompt, mode, options, and permissions.
|
|
61
62
|
|
|
62
63
|
Use `/landstrip-sandbox` for sandbox overview/policy (`Overview`, `Policy`, `Help`)
|
|
63
64
|
and `/landstrip-subagents` for subagent/task management (`Tasks`, `Log`, `Subagent`,
|
package/dist/index.ts
CHANGED
|
@@ -3964,6 +3964,32 @@ ${this.primaryAgent.prompt}` };
|
|
|
3964
3964
|
unsubscribe();
|
|
3965
3965
|
done(undefined);
|
|
3966
3966
|
};
|
|
3967
|
+
const runSaving = (errorPrefix, action) => {
|
|
3968
|
+
saving = true;
|
|
3969
|
+
action().catch((error) => ctx.ui.notify(`${errorPrefix}: ${formatError(error)}`, "error")).finally(() => {
|
|
3970
|
+
saving = false;
|
|
3971
|
+
tui.requestRender();
|
|
3972
|
+
});
|
|
3973
|
+
};
|
|
3974
|
+
const startSteering = (task) => {
|
|
3975
|
+
const input = new Input;
|
|
3976
|
+
input.focused = true;
|
|
3977
|
+
input.onEscape = () => {
|
|
3978
|
+
steering = undefined;
|
|
3979
|
+
tui.requestRender();
|
|
3980
|
+
};
|
|
3981
|
+
input.onSubmit = (value) => {
|
|
3982
|
+
const message = value.trim();
|
|
3983
|
+
if (!message) {
|
|
3984
|
+
ctx.ui.notify("Steering message must not be empty", "warning");
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
steering = undefined;
|
|
3988
|
+
this.steerTask(task, message).then(() => ctx.ui.notify(`Steered task ${task.id.slice(0, 8)}`, "info")).catch((error) => ctx.ui.notify(`Could not steer task: ${formatError(error)}`, "error")).finally(() => tui.requestRender());
|
|
3989
|
+
tui.requestRender();
|
|
3990
|
+
};
|
|
3991
|
+
steering = input;
|
|
3992
|
+
};
|
|
3967
3993
|
const renderAgentsTab = (frame) => {
|
|
3968
3994
|
const { tabs, pane, contentWidth, pad } = frame;
|
|
3969
3995
|
const listedAgents = agentsForTab();
|
|
@@ -4118,8 +4144,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
4118
4144
|
ctx.ui.notify("Agent editing requires a trusted project", "warning");
|
|
4119
4145
|
return;
|
|
4120
4146
|
}
|
|
4121
|
-
|
|
4122
|
-
(async () => {
|
|
4147
|
+
runSaving("Could not edit agent", async () => {
|
|
4123
4148
|
const document = prepareProjectAgentEditor(ctx.cwd, agent);
|
|
4124
4149
|
const edited = await ctx.ui.editor(document.title, document.content);
|
|
4125
4150
|
if (edited === undefined)
|
|
@@ -4127,9 +4152,6 @@ ${this.primaryAgent.prompt}` };
|
|
|
4127
4152
|
await document.save(edited);
|
|
4128
4153
|
await refreshAgentRuntime(agent.name);
|
|
4129
4154
|
ctx.ui.notify(`Updated project agent ${agent.name}`, "info");
|
|
4130
|
-
})().catch((error) => ctx.ui.notify(`Could not edit agent: ${formatError(error)}`, "error")).finally(() => {
|
|
4131
|
-
saving = false;
|
|
4132
|
-
tui.requestRender();
|
|
4133
4155
|
});
|
|
4134
4156
|
return;
|
|
4135
4157
|
} else if (data === " " && agent && !saving) {
|
|
@@ -4138,13 +4160,10 @@ ${this.primaryAgent.prompt}` };
|
|
|
4138
4160
|
return;
|
|
4139
4161
|
}
|
|
4140
4162
|
const disabled = !agent.disabled;
|
|
4141
|
-
|
|
4142
|
-
|
|
4163
|
+
runSaving("Could not update agent", async () => {
|
|
4164
|
+
await setAgentDisabledForScope(ctx.cwd, agent.name, disabled, "project");
|
|
4143
4165
|
await refreshAgentRuntime(agent.name);
|
|
4144
4166
|
ctx.ui.notify(`${agent.name} ${disabled ? "disabled" : "enabled"} in project settings`, "info");
|
|
4145
|
-
}).catch((error) => ctx.ui.notify(`Could not update agent: ${formatError(error)}`, "error")).finally(() => {
|
|
4146
|
-
saving = false;
|
|
4147
|
-
tui.requestRender();
|
|
4148
4167
|
});
|
|
4149
4168
|
return;
|
|
4150
4169
|
} else if (data.toLowerCase() === "i" && agent && !saving) {
|
|
@@ -4154,13 +4173,10 @@ ${this.primaryAgent.prompt}` };
|
|
|
4154
4173
|
}
|
|
4155
4174
|
if (!disabledOverrides.project.has(agent.name))
|
|
4156
4175
|
return;
|
|
4157
|
-
|
|
4158
|
-
|
|
4176
|
+
runSaving("Could not update agent", async () => {
|
|
4177
|
+
await clearAgentDisabledForScope(ctx.cwd, agent.name, "project");
|
|
4159
4178
|
await refreshAgentRuntime(agent.name);
|
|
4160
4179
|
ctx.ui.notify(`${agent.name} now inherits its global state`, "info");
|
|
4161
|
-
}).catch((error) => ctx.ui.notify(`Could not update agent: ${formatError(error)}`, "error")).finally(() => {
|
|
4162
|
-
saving = false;
|
|
4163
|
-
tui.requestRender();
|
|
4164
4180
|
});
|
|
4165
4181
|
return;
|
|
4166
4182
|
} else if (tab === "primary" && matchesKey(data, "return") && agent && !agent.disabled && isPrimaryAgent(agent)) {
|
|
@@ -4218,16 +4234,16 @@ ${this.primaryAgent.prompt}` };
|
|
|
4218
4234
|
...footer
|
|
4219
4235
|
]);
|
|
4220
4236
|
};
|
|
4237
|
+
const item = (label, value) => ` ${theme.fg("dim", label.padEnd(20))} ${value}`;
|
|
4238
|
+
const shellReadLabel = (current) => current.windowsMode !== undefined ? "Policy (required)" : current.shellReadMode === "host" ? "Host-aligned" : "Policy";
|
|
4221
4239
|
const renderTab = {
|
|
4222
4240
|
overview: (frame) => renderSandboxTab(frame, () => {
|
|
4223
4241
|
if (!overview)
|
|
4224
4242
|
return [];
|
|
4225
4243
|
const { contentWidth } = frame;
|
|
4226
4244
|
const status = overview.noSandboxFlag ? { color: "warning", label: "Disabled by --no-sandbox" } : !overview.enabled ? { color: "warning", label: "Disabled by configuration" } : !overview.running ? { color: "warning", label: "Inactive" } : { color: "success", label: "Active" };
|
|
4227
|
-
const shellReadMode = overview.windowsMode !== undefined ? "Policy (required)" : overview.shellReadMode === "host" ? "Host-aligned" : "Policy";
|
|
4228
4245
|
const changePath = overview.changeScope === "project" ? overview.paths.project : overview.paths.global;
|
|
4229
4246
|
const valueWidth = Math.max(10, contentWidth - 23);
|
|
4230
|
-
const item = (label, value) => ` ${theme.fg("dim", label.padEnd(20))} ${value}`;
|
|
4231
4247
|
const body = [
|
|
4232
4248
|
`${theme.fg(status.color, "●")} ${theme.fg("text", status.label)}`,
|
|
4233
4249
|
item("Configured", overview.enabled ? theme.fg("text", "Enabled") : theme.fg("warning", "Disabled")),
|
|
@@ -4236,7 +4252,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
4236
4252
|
"",
|
|
4237
4253
|
theme.fg("accent", "Protection"),
|
|
4238
4254
|
item("Network", overview.networkMode),
|
|
4239
|
-
item("Shell reads",
|
|
4255
|
+
item("Shell reads", shellReadLabel(overview)),
|
|
4240
4256
|
item("Read rules", `${overview.config.filesystem.denyRead.length + overview.config.filesystem.denyReadAlways.length + overview.config.filesystem.allowRead.length} · ${overview.readRuleScope}`),
|
|
4241
4257
|
item("Write rules", String(overview.config.filesystem.allowWrite.length + overview.config.filesystem.denyWrite.length + overview.config.filesystem.denyWriteAlways.length)),
|
|
4242
4258
|
item("Session grants", String(overview.sessionDomains.length + overview.sessionReadPaths.length + overview.sessionWritePaths.length)),
|
|
@@ -4256,8 +4272,6 @@ ${this.primaryAgent.prompt}` };
|
|
|
4256
4272
|
if (!overview)
|
|
4257
4273
|
return [];
|
|
4258
4274
|
const { listValue } = frame;
|
|
4259
|
-
const shellReadMode = overview.windowsMode !== undefined ? "Policy (required)" : overview.shellReadMode === "host" ? "Host-aligned" : "Policy";
|
|
4260
|
-
const item = (label, value) => ` ${theme.fg("dim", label.padEnd(20))} ${value}`;
|
|
4261
4275
|
return [
|
|
4262
4276
|
theme.fg("accent", "Network"),
|
|
4263
4277
|
item("Mode", overview.networkMode),
|
|
@@ -4266,7 +4280,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
4266
4280
|
item("Unix sockets", overview.config.network.allowAllUnixSockets ? "all" : listValue(overview.config.network.allowUnixSockets)),
|
|
4267
4281
|
"",
|
|
4268
4282
|
theme.fg("accent", "Filesystem"),
|
|
4269
|
-
item("Shell reads",
|
|
4283
|
+
item("Shell reads", shellReadLabel(overview)),
|
|
4270
4284
|
item("Read rules for", overview.readRuleScope),
|
|
4271
4285
|
item("Denied reads", listValue(overview.config.filesystem.denyRead)),
|
|
4272
4286
|
item("Always denied reads", listValue(overview.config.filesystem.denyReadAlways)),
|
|
@@ -4557,35 +4571,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
4557
4571
|
tab = "log";
|
|
4558
4572
|
follow = true;
|
|
4559
4573
|
scroll = 0;
|
|
4560
|
-
|
|
4561
|
-
input.focused = true;
|
|
4562
|
-
input.onEscape = () => {
|
|
4563
|
-
steering = undefined;
|
|
4564
|
-
tui.requestRender();
|
|
4565
|
-
};
|
|
4566
|
-
input.onSubmit = (value) => {
|
|
4567
|
-
const message = value.trim();
|
|
4568
|
-
if (!message) {
|
|
4569
|
-
ctx.ui.notify("Steering message must not be empty", "warning");
|
|
4570
|
-
return;
|
|
4571
|
-
}
|
|
4572
|
-
steering = undefined;
|
|
4573
|
-
(async () => {
|
|
4574
|
-
const running = this.running.get(task.id);
|
|
4575
|
-
if (running) {
|
|
4576
|
-
await running.rpc.steer(message);
|
|
4577
|
-
} else if ((task.state === "queued" || task.state === "running") && this.runPromises.has(task.id)) {
|
|
4578
|
-
const pending = this.pendingSteers.get(task.id) ?? [];
|
|
4579
|
-
pending.push(message);
|
|
4580
|
-
this.pendingSteers.set(task.id, pending);
|
|
4581
|
-
} else {
|
|
4582
|
-
throw new Error("Task is no longer active");
|
|
4583
|
-
}
|
|
4584
|
-
ctx.ui.notify(`Steered task ${task.id.slice(0, 8)}`, "info");
|
|
4585
|
-
})().catch((error) => ctx.ui.notify(`Could not steer task: ${formatError(error)}`, "error")).finally(() => tui.requestRender());
|
|
4586
|
-
tui.requestRender();
|
|
4587
|
-
};
|
|
4588
|
-
steering = input;
|
|
4574
|
+
startSteering(task);
|
|
4589
4575
|
} else if (data.toLowerCase() === "r") {
|
|
4590
4576
|
const finished = tasks.filter((t) => t.state === "completed" || t.state === "error" || t.state === "cancelled" || t.state === "interrupted");
|
|
4591
4577
|
if (finished.length === 0) {
|
|
@@ -4634,35 +4620,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
4634
4620
|
});
|
|
4635
4621
|
return;
|
|
4636
4622
|
} else if (matchesKey(data, "return") && (task.state === "queued" || task.state === "running") && task.kind !== "command") {
|
|
4637
|
-
|
|
4638
|
-
input.focused = true;
|
|
4639
|
-
input.onEscape = () => {
|
|
4640
|
-
steering = undefined;
|
|
4641
|
-
tui.requestRender();
|
|
4642
|
-
};
|
|
4643
|
-
input.onSubmit = (value) => {
|
|
4644
|
-
const message = value.trim();
|
|
4645
|
-
if (!message) {
|
|
4646
|
-
ctx.ui.notify("Steering message must not be empty", "warning");
|
|
4647
|
-
return;
|
|
4648
|
-
}
|
|
4649
|
-
steering = undefined;
|
|
4650
|
-
(async () => {
|
|
4651
|
-
const running = this.running.get(task.id);
|
|
4652
|
-
if (running) {
|
|
4653
|
-
await running.rpc.steer(message);
|
|
4654
|
-
} else if ((task.state === "queued" || task.state === "running") && this.runPromises.has(task.id)) {
|
|
4655
|
-
const pending = this.pendingSteers.get(task.id) ?? [];
|
|
4656
|
-
pending.push(message);
|
|
4657
|
-
this.pendingSteers.set(task.id, pending);
|
|
4658
|
-
} else {
|
|
4659
|
-
throw new Error("Task is no longer active");
|
|
4660
|
-
}
|
|
4661
|
-
ctx.ui.notify(`Steered task ${task.id.slice(0, 8)}`, "info");
|
|
4662
|
-
})().catch((error) => ctx.ui.notify(`Could not steer task: ${formatError(error)}`, "error")).finally(() => tui.requestRender());
|
|
4663
|
-
tui.requestRender();
|
|
4664
|
-
};
|
|
4665
|
-
steering = input;
|
|
4623
|
+
startSteering(task);
|
|
4666
4624
|
} else if (data.toLowerCase() === "f") {
|
|
4667
4625
|
follow = !follow;
|
|
4668
4626
|
} else if (matchesKey(data, "up")) {
|
|
@@ -4760,15 +4718,12 @@ ${this.primaryAgent.prompt}` };
|
|
|
4760
4718
|
tui.requestRender();
|
|
4761
4719
|
return;
|
|
4762
4720
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4721
|
+
runSaving("Could not delete agent", async () => {
|
|
4722
|
+
const deleted = await deleteProjectAgent(ctx.cwd, agent);
|
|
4765
4723
|
if (!deleted)
|
|
4766
4724
|
return;
|
|
4767
4725
|
await refreshAgentRuntime(name);
|
|
4768
4726
|
ctx.ui.notify(`Deleted project agent ${name}`, "info");
|
|
4769
|
-
}).catch((error) => ctx.ui.notify(`Could not delete agent: ${formatError(error)}`, "error")).finally(() => {
|
|
4770
|
-
saving = false;
|
|
4771
|
-
tui.requestRender();
|
|
4772
4727
|
});
|
|
4773
4728
|
return;
|
|
4774
4729
|
}
|
|
@@ -5004,16 +4959,7 @@ ${outputText}` }
|
|
|
5004
4959
|
if (task.state !== "running" && task.state !== "queued") {
|
|
5005
4960
|
throw new Error(`Task ${task.id.slice(0, 8)} is no longer active (${task.state})`);
|
|
5006
4961
|
}
|
|
5007
|
-
|
|
5008
|
-
if (running) {
|
|
5009
|
-
await running.rpc.steer(message);
|
|
5010
|
-
} else if (this.runPromises.has(task.id)) {
|
|
5011
|
-
const pending = this.pendingSteers.get(task.id) ?? [];
|
|
5012
|
-
pending.push(message);
|
|
5013
|
-
this.pendingSteers.set(task.id, pending);
|
|
5014
|
-
} else {
|
|
5015
|
-
throw new Error(`Task ${task.id.slice(0, 8)} is no longer active`);
|
|
5016
|
-
}
|
|
4962
|
+
await this.steerTask(task, message, `Task ${task.id.slice(0, 8)} is no longer active`);
|
|
5017
4963
|
return {
|
|
5018
4964
|
content: [
|
|
5019
4965
|
{
|
|
@@ -5041,6 +4987,18 @@ ${outputText}` }
|
|
|
5041
4987
|
}
|
|
5042
4988
|
};
|
|
5043
4989
|
}
|
|
4990
|
+
async steerTask(task, message, inactiveMessage = "Task is no longer active") {
|
|
4991
|
+
const running = this.running.get(task.id);
|
|
4992
|
+
if (running) {
|
|
4993
|
+
await running.rpc.steer(message);
|
|
4994
|
+
} else if ((task.state === "queued" || task.state === "running") && this.runPromises.has(task.id)) {
|
|
4995
|
+
const pending = this.pendingSteers.get(task.id) ?? [];
|
|
4996
|
+
pending.push(message);
|
|
4997
|
+
this.pendingSteers.set(task.id, pending);
|
|
4998
|
+
} else {
|
|
4999
|
+
throw new Error(inactiveMessage);
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5044
5002
|
async runBackgroundShellTask(params, ctx) {
|
|
5045
5003
|
const taskId = randomUUID();
|
|
5046
5004
|
const sessionId = this.activeSessionId ?? ctx.sessionManager.getSessionId();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.4",
|
|
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.20.
|
|
42
|
+
"@landstrip/landstrip-api": "^0.20.4",
|
|
43
43
|
"jsonc-parser": "^3.3.1",
|
|
44
44
|
"minimatch": "^10.2.5",
|
|
45
45
|
"undici": "^8.9.0",
|