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.
Files changed (3) hide show
  1. package/README.md +6 -5
  2. package/dist/index.ts +52 -94
  3. 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: 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:
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`: 0–16. Agent definitions can override model, prompt, mode, options,
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
- saving = true;
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
- saving = true;
4142
- setAgentDisabledForScope(ctx.cwd, agent.name, disabled, "project").then(async () => {
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
- saving = true;
4158
- clearAgentDisabledForScope(ctx.cwd, agent.name, "project").then(async () => {
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", shellReadMode),
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", shellReadMode),
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
- const input = new Input;
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
- const input = new Input;
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
- saving = true;
4764
- deleteProjectAgent(ctx.cwd, agent).then(async (deleted) => {
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
- const running = this.running.get(task.id);
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.2",
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.2",
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",