pi-landstrip 0.18.34 → 0.18.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.
Files changed (4) hide show
  1. package/README.md +42 -163
  2. package/dist/index.ts +361 -276
  3. package/package.json +2 -2
  4. package/LICENSE +0 -201
package/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # pi-landstrip
2
2
 
3
- `pi-landstrip` adds sandboxed Bash, primary agents, and process-backed subagents
4
- to [Pi](https://pi.dev/). It requires Pi 0.82.0 or later and Node.js 22.19.0 or
5
- later.
3
+ Pi extension for sandboxed Bash, primary agents, and process-backed subagents.
4
+ Pi 0.82.0 and Node.js 22.19.0 or newer are required.
6
5
 
7
6
  ## Install
8
7
 
@@ -10,189 +9,69 @@ later.
10
9
  pi install npm:pi-landstrip
11
10
  ```
12
11
 
13
- For a manual global or trusted-project install, add the package to Pi's
14
- `settings.json`:
15
-
16
- ```json
17
- {
18
- "packages": ["npm:pi-landstrip"]
19
- }
20
- ```
21
-
22
- The package includes Landstrip binaries for Linux, macOS, and Windows on x64 and
23
- Arm64. If sandboxing is enabled but the binary or platform is unusable, Bash,
24
- shell commands, and sandboxed workers fail closed. Only `--no-sandbox` or
25
- `"enabled": false` permits execution without OS isolation.
12
+ The package includes Landstrip binaries for Linux, macOS, and Windows on x64
13
+ and Arm64. When sandboxing is enabled, an unusable binary or platform fails
14
+ closed. Use `--no-sandbox` or set `enabled` to `false` for unsandboxed execution.
26
15
 
27
16
  ## Permissions
28
17
 
29
- Pi evaluates two permission categories:
30
-
31
- | Category | Checked | Controls |
32
- | -------- | --------------------------------------- | -------------------------------- |
33
- | Agent | Before dispatch | Whether an agent may call a tool |
34
- | Sandbox | Before file access and during processes | Filesystem and network resources |
35
-
36
- Agent rules return `deny`, `ask`, or `allow`. An agent approval authorizes only
37
- tool dispatch and cannot bypass a sandbox hard denial.
38
-
39
- By default, primary `read`, `write`, `edit`, and `apply_patch` tools run in the
40
- trusted Pi process and only agent permissions apply. Set
41
- `toolFilesystemPolicy` to `"sandbox"` to preflight their paths against
42
- `sandbox.json`. `denyWrite` remains a hard denial. If both agent and sandbox
43
- approval are needed, Pi presents one prompt for the call.
44
-
45
- AI Bash, `!`, `!!`, and subagent processes use OS isolation. Subagents rely on
46
- that isolation instead of repeating file-tool preflight. Pi presents all agent
47
- and sandbox requests through one FIFO prompt queue.
48
-
49
- Sandbox approvals may apply once, for the session, to the project, or globally.
50
- Persistent approvals update `.pi/sandbox.json` or
51
- `~/.pi/agent/sandbox.json`. Headless requests that require approval are denied.
52
-
53
- ## Sandbox configuration
18
+ Agent permissions control tool dispatch. Sandbox permissions control filesystem
19
+ and network access. Agent approval never bypasses a sandbox hard denial.
54
20
 
55
- Sandbox policy merges in this order:
21
+ AI Bash, `!`, `!!`, and subagents use OS isolation. Primary file tools use the
22
+ trusted Pi process unless `toolFilesystemPolicy` is `"sandbox"`. Persistent
23
+ sandbox approvals update `.pi/sandbox.json` or `~/.pi/agent/sandbox.json`.
56
24
 
57
- 1. bundled [`sandbox.json`](./sandbox.json);
58
- 2. `~/.pi/agent/sandbox.json`;
59
- 3. `.pi/sandbox.json` for a trusted project.
25
+ ## Sandbox settings
60
26
 
61
- Objects merge recursively, arrays combine, and later scalar values replace
62
- earlier values. `/sandbox` displays or toggles the policy. It writes the project
63
- policy in a trusted project and the global policy otherwise.
27
+ Policy merges from bundled `sandbox.json`, global
28
+ `~/.pi/agent/sandbox.json`, then trusted-project `.pi/sandbox.json`.
29
+ `/settings` displays or toggles the effective policy.
64
30
 
65
- | Field | Bundled default |
66
- | ----------------------------- | -------------------------------------------------------------------------------------------------- |
67
- | `enabled` | `true` |
68
- | `shell.readAccess` | `"host"` |
69
- | `filesystem.denyRead` | `["/Users", "/home"]` |
70
- | `filesystem.allowRead` | `[".", "~/.gitconfig", "~/.config/git/config", "/dev/null"]` |
71
- | `filesystem.allowWrite` | `[".", "/dev/null"]` |
72
- | `filesystem.denyWrite` | `["**/.env", "**/.env.*", "**/*.pem", "**/*.key", ".pi/sandbox.json", "~/.pi/agent/sandbox.json"]` |
73
- | `network.allowNetwork` | `false` |
74
- | `network.allowLocalBinding` | `false` |
75
- | `network.allowAllUnixSockets` | `false` |
76
- | `network.allowUnixSockets` | `[]` |
77
- | `network.allowedDomains` | `[]` |
78
- | `network.deniedDomains` | `[]` |
79
- | `windows.appContainerMode` | `"standard"` |
80
- | `windows.allowLoopback` | `false` |
81
-
82
- Filesystem and core network semantics follow the main
83
- [Landstrip policy](https://github.com/landstrip/landstrip#policy). `allowedDomains`
84
- and `deniedDomains` are plugin fields enforced by the local HTTP/HTTPS proxy.
85
-
86
- `shell.readAccess: "host"` gives primary Bash and `!`/`!!` the trusted host read
87
- view on Linux and macOS while retaining sandboxed writes. `"policy"` applies
88
- `denyRead` and `allowRead`. Workers always use policy reads. Windows always uses
89
- policy reads because it requires an explicit read allowlist.
90
-
91
- With `enabled: false`, subagents remain separate processes but lose Landstrip OS
92
- isolation; Pi warns once per session.
93
-
94
- ## Agent configuration
31
+ ```json
32
+ {
33
+ "enabled": true,
34
+ "shell": { "readAccess": "host" },
35
+ "filesystem": {
36
+ "allowWrite": ["."],
37
+ "denyWrite": ["**/.env", "**/*.pem"]
38
+ },
39
+ "network": { "allowNetwork": false }
40
+ }
41
+ ```
95
42
 
96
- Agent configuration merges built-ins, global configuration, then trusted-project
97
- configuration. Use either dedicated files:
43
+ See [`sandbox.json`](sandbox.json) for exact defaults. Workers always use policy
44
+ reads. Windows requires an explicit read allowlist.
98
45
 
99
- - `~/.pi/agent/landstrip.json`
100
- - `.pi/landstrip.json`
46
+ ## Agents
101
47
 
102
- or a top-level `landstrip` object in the matching `settings.json`. Using both
103
- forms at one scope is an error.
48
+ Configure agents in `~/.pi/agent/landstrip.json`, `.pi/landstrip.json`, or a
49
+ `landstrip` object in the matching Pi settings file:
104
50
 
105
51
  ```json
106
52
  {
107
53
  "maxSubagents": 2,
108
54
  "toolFilesystemPolicy": "sandbox",
109
- "agent": {
110
- "review": {
111
- "description": "Review without modifying files",
112
- "mode": "subagent",
113
- "prompt": "Report concrete findings.",
114
- "permission": {
115
- "edit": "deny",
116
- "bash": "ask"
117
- }
118
- }
119
- },
120
55
  "permission": {
121
- "task": {
122
- "*": "deny",
123
- "review": "allow"
124
- }
56
+ "task": { "*": "deny", "review": "allow" }
125
57
  }
126
58
  }
127
59
  ```
128
60
 
129
- | Field | Default | Values |
130
- | ---------------------- | --------------- | -------------------------------------------- |
131
- | `maxSubagents` | `1` | integer from 0 through 16; 0 disables `task` |
132
- | `toolFilesystemPolicy` | `"host"` | `"host"` or `"sandbox"` |
133
- | `agent` | built-in agents | named agent definitions |
134
- | `permission` | built-in rules | global tool/resource rules |
135
-
136
- Agent definitions accept `name`, `description`, `prompt`, `mode`, `model`,
137
- `variant`, `temperature`, `top_p`, `steps`, `color`, `hidden`, `disable`,
138
- `options`, and `permission`. `mode` is `primary`, `subagent`, or `all` and
139
- defaults to `all`. `/agents` Primary lists only `mode: primary` agents. The
140
- Subagent tab lists `mode: subagent` and `mode: all` agents, including JSON and
141
- Markdown definitions.
142
-
143
- The built-in primary agents are `build` and `plan`; built-in subagents are
144
- `general`, `scout`, and `explore`. Pi also loads Markdown agents from
145
- `~/.pi/agent/agents/` and `.pi/agents/`. Project definitions overlay global
146
- definitions. Configured fields overlay Markdown agents with the same name, so a
147
- model-only `landstrip.json` entry keeps the Markdown agent's mode, prompt, and
148
- permissions.
61
+ `maxSubagents` accepts 0 through 16. Agent definitions can override model,
62
+ prompt, mode, options, and permissions. Global definitions load before trusted
63
+ project definitions.
149
64
 
150
65
  ## Commands and tasks
151
66
 
152
- - `/sandbox` inspects and toggles sandboxing.
153
- - `/agents` selects or edits agents and lists task sessions.
154
- - `Ctrl+Shift+A` cycles visible primary agents while Pi is idle.
155
-
156
- The `task` tool requires `description`, `prompt`, and `subagent_type`. Optional
157
- `task_id` continues a saved task, `command` records the originating command, and
158
- `background: true` returns immediately. `/agents <task-id>` inspects a task.
159
- Each invocation starts a fresh Pi RPC process; continuation restores its saved
160
- session in a new process.
161
-
162
- Workers use normal Pi resource and plugin discovery. Their model requests can
163
- read Pi authentication and inherited credential environment variables. Install
164
- only trusted worker plugins and use credentials appropriate for the sandboxed
165
- task.
166
-
167
- ## Permission ask providers
168
-
169
- A separate Pi extension can resolve agent permission rules that return `ask` by
170
- registering one `LandstripPermissionAskProvider`:
171
-
172
- ```ts
173
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
174
- import { provideLandstripPermissionAsk } from 'pi-landstrip/api';
175
-
176
- export default function permissionReviewer(pi: ExtensionAPI): void {
177
- provideLandstripPermissionAsk(pi, {
178
- id: 'example-reviewer',
179
- async decide(request) {
180
- if (request.toolName === 'bash' && request.input.command === 'git status') {
181
- return { decision: 'allow' };
182
- }
183
- return { decision: 'abstain' };
184
- },
185
- });
186
- }
187
- ```
67
+ - `/settings` opens a screen-wide pane with sandbox and agent settings.
68
+ - `/sandbox` opens the same pane on its sandbox section.
69
+ - `/agents` selects agents and lists task sessions.
70
+ - `Ctrl+Shift+A` cycles primary agents while Pi is idle.
188
71
 
189
- The provider receives the tool name and input, pending permission/resource pairs,
190
- the primary or subagent context, and an abort signal. `allow` approves that call,
191
- `deny` blocks it, and `abstain` uses the normal UI prompt or headless denial.
192
- Provider errors also fall back safely. Only one provider may be registered at a
193
- time; the helper works regardless of extension load order and returns a disposer.
72
+ The `task` tool starts isolated workers. Set `background: true` to return
73
+ immediately and use `task_id` to continue a saved task.
194
74
 
195
- ## License
75
+ ## Licensing
196
76
 
197
- `pi-landstrip` is licensed under `Apache-2.0`. The bundled Landstrip package is
198
- licensed separately as `Apache-2.0 AND LGPL-2.1-or-later`.
77
+ [Apache 2.0](../../LICENSE-APACHE-2.0).
package/dist/index.ts CHANGED
@@ -31,7 +31,6 @@ import {
31
31
  SettingsManager,
32
32
  withFileMutationQueue as withFileMutationQueue3
33
33
  } from "@earendil-works/pi-coding-agent";
34
- import { matchesKey as matchesKey2, truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
35
34
  import {
36
35
  binaryPath
37
36
  } from "@landstrip/landstrip";
@@ -599,6 +598,23 @@ async function setMaxSubagentsConfigForScope(cwd, maxSubagents, scope, agentDir
599
598
  async function clearMaxSubagentsConfigForScope(cwd, scope, agentDir = getAgentDir()) {
600
599
  await writeMaxSubagentsConfigForScope(cwd, undefined, scope, agentDir);
601
600
  }
601
+ async function writeToolFilesystemPolicyConfigForScope(cwd, policy, scope, agentDir) {
602
+ await updateLandstripConfigForScope(cwd, scope, agentDir, (landstrip) => {
603
+ if (policy === undefined)
604
+ delete landstrip.toolFilesystemPolicy;
605
+ else
606
+ landstrip.toolFilesystemPolicy = policy;
607
+ });
608
+ }
609
+ async function setToolFilesystemPolicyConfigForScope(cwd, policy, scope, agentDir = getAgentDir()) {
610
+ if (policy !== "host" && policy !== "sandbox") {
611
+ throw new Error("toolFilesystemPolicy must be host or sandbox");
612
+ }
613
+ await writeToolFilesystemPolicyConfigForScope(cwd, policy, scope, agentDir);
614
+ }
615
+ async function clearToolFilesystemPolicyConfigForScope(cwd, scope, agentDir = getAgentDir()) {
616
+ await writeToolFilesystemPolicyConfigForScope(cwd, undefined, scope, agentDir);
617
+ }
602
618
  async function setAgentDisabledForScope(cwd, name, disabled, scope, agentDir = getAgentDir()) {
603
619
  await updateLandstripConfigForScope(cwd, scope, agentDir, (landstrip) => {
604
620
  if (landstrip.agent !== undefined && !isRecord2(landstrip.agent)) {
@@ -696,6 +712,16 @@ function loadMaxSubagentsSettings(cwd, includeProject = true, agentDir = getAgen
696
712
  }
697
713
  return { global, project };
698
714
  }
715
+ function loadToolFilesystemPolicySettings(cwd, includeProject = true, agentDir = getAgentDir()) {
716
+ const global = loadLandstripConfig(cwd, false, agentDir).toolFilesystemPolicy;
717
+ if (!includeProject)
718
+ return { global };
719
+ const project = readLandstripConfigSource(cwd, "project", agentDir).config.toolFilesystemPolicy;
720
+ if (project !== undefined && project !== "host" && project !== "sandbox") {
721
+ throw new Error("toolFilesystemPolicy must be host or sandbox");
722
+ }
723
+ return { global, project };
724
+ }
699
725
 
700
726
  // opencode-agents.ts
701
727
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
@@ -1755,19 +1781,17 @@ var PI_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xh
1755
1781
  var AGENTS_HELP_ROWS = [
1756
1782
  ["Tab", "Next tab"],
1757
1783
  ["↑ / ↓", "Select item or scroll when follow is off"],
1784
+ ["Enter", "Activate: set primary, open log, confirm"],
1785
+ ["Esc", "Back one level, or close from a top-level tab"],
1786
+ ["Ctrl+C", "Close"],
1787
+ ["Space", "Toggle agent enabled or select task"],
1788
+ ["X", "Delete selected agent or task sessions"],
1789
+ ["E", "Edit project agent"],
1790
+ ["I", "Inherit global agent state"],
1758
1791
  ["F", "Toggle task log follow"],
1759
1792
  ["Page Up / Down", "Scroll task output by page"],
1760
1793
  ["Home / End", "Jump to task output boundary"],
1761
- ["Enter", "Open log, set primary, or edit setting"],
1762
- ["Esc", "Cancel edit, go back, or close"],
1763
- ["Ctrl+C", "Close"],
1764
- ["Ctrl+D", "Delete selected task sessions"],
1765
- ["Space", "Enable agent or select task"],
1766
- ["E", "Edit project agent"],
1767
- ["X", "Delete project agent"],
1768
- ["Y / N", "Confirm or cancel deletion"],
1769
- ["Empty + Enter", "Inherit global setting"],
1770
- ["Backspace", "Open parent task or edit setting"]
1794
+ ["Backspace", "Open parent task"]
1771
1795
  ];
1772
1796
  var MIN_SUPPORTED_PI_VERSION = [0, 82, 0];
1773
1797
  var cachedPiPackage;
@@ -2622,6 +2646,14 @@ class SubagentRuntime {
2622
2646
  description: "Manage agents, tasks, and concurrency",
2623
2647
  handler: async (args, ctx) => this.openAgents(args, ctx)
2624
2648
  });
2649
+ this.pi.registerCommand("settings", {
2650
+ description: "Manage sandbox and agent settings",
2651
+ handler: async (_args, ctx) => this.openSettings(ctx)
2652
+ });
2653
+ this.pi.registerCommand("sandbox", {
2654
+ description: "Inspect and toggle the sandbox",
2655
+ handler: async (_args, ctx) => this.openSettings(ctx)
2656
+ });
2625
2657
  this.pi.registerShortcut("ctrl+shift+a", {
2626
2658
  description: "Cycle to the next primary agent",
2627
2659
  handler: async (ctx) => this.cyclePrimaryAgent(ctx)
@@ -2749,7 +2781,6 @@ ${this.primaryAgent.prompt}` };
2749
2781
  ctx.ui.notify(diagnostic, "warning");
2750
2782
  let agents = [...catalog.agents.values()].filter((agent) => !agent.hidden).sort((left, right) => left.name.localeCompare(right.name));
2751
2783
  let disabledOverrides = loadAgentDisabledOverrides(ctx.cwd, projectTrusted);
2752
- let maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
2753
2784
  let tab = requested ? "log" : "primary";
2754
2785
  const agentsForTab = () => agents.filter((agent) => tab === "subagent" ? agentSupportsMode(agent, "subagent") : isPrimaryAgent(agent));
2755
2786
  let selectedAgent = Math.max(0, agentsForTab().findIndex((agent) => agent.name === this.primaryAgent?.name));
@@ -2758,8 +2789,6 @@ ${this.primaryAgent.prompt}` };
2758
2789
  ctx.ui.notify(`Unknown task session: ${requested}`, "error");
2759
2790
  return;
2760
2791
  }
2761
- let selectedSetting = 0;
2762
- let settingEditor;
2763
2792
  let saving = false;
2764
2793
  let confirmingDeleteAgent;
2765
2794
  let pendingTaskAction;
@@ -2775,9 +2804,6 @@ ${this.primaryAgent.prompt}` };
2775
2804
  const nextSelected = selectedName ? listedAgents.findIndex((agent) => agent.name === selectedName) : selectedAgent;
2776
2805
  selectedAgent = Math.max(0, Math.min(nextSelected < 0 ? 0 : nextSelected, listedAgents.length - 1));
2777
2806
  };
2778
- const reloadSettings = () => {
2779
- maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
2780
- };
2781
2807
  const refreshTasks = () => {
2782
2808
  const selectedId = tasks[selectedTask]?.id;
2783
2809
  tasks = [...this.tasks.values()];
@@ -2804,7 +2830,6 @@ ${this.primaryAgent.prompt}` };
2804
2830
  this.pi.registerTool(this.createTaskTool(undefined, this.primaryRules, ctx));
2805
2831
  }
2806
2832
  };
2807
- const selectedMaxSubagents = () => maxSubagentsSettings.project ?? maxSubagentsSettings.global;
2808
2833
  await ctx.ui.custom((tui, theme, _keybindings, done) => ({
2809
2834
  render: (width) => {
2810
2835
  const contentWidth = Math.max(1, width - 4);
@@ -2818,20 +2843,34 @@ ${this.primaryAgent.prompt}` };
2818
2843
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
2819
2844
  };
2820
2845
  refreshTasks();
2821
- const tabs = dialogTabs(theme, ["Primary", "Subagent", "Tasks", "Log", "Settings", "Help"], `${tab[0]?.toUpperCase()}${tab.slice(1)}`);
2846
+ const tabs = dialogTabs(theme, ["Primary", "Subagent", "Tasks", "Log", "Help"], `${tab[0]?.toUpperCase()}${tab.slice(1)}`);
2822
2847
  if (tab === "primary" || tab === "subagent") {
2823
2848
  const listedAgents = agentsForTab();
2824
2849
  const start = Math.max(0, Math.min(selectedAgent - 3, listedAgents.length - 7));
2825
2850
  const shown2 = listedAgents.slice(start, start + 7);
2826
2851
  const nameWidth = Math.min(20, Math.max(5, ...listedAgents.map((agent2) => visibleWidth2(`@${agent2.name}`))));
2827
- const sourceWidth = 8;
2828
- const stateWidth = 13;
2829
- const fixedWidth = 2 + nameWidth + 1 + sourceWidth + 1 + stateWidth;
2830
- const modelWidth = Math.max(10, Math.min(30, contentWidth - fixedWidth - 1));
2852
+ const modelWidth = Math.max(10, Math.min(24, Math.floor((contentWidth - 2) / 3)));
2853
+ const enabledWidth = 13;
2854
+ const runtimeWidth = Math.max(12, contentWidth - 2 - nameWidth - 1 - modelWidth - 1 - enabledWidth - 1);
2855
+ const runtimeFor = (agent2) => {
2856
+ const parts = [];
2857
+ if (agent2.name === this.primaryAgent?.name)
2858
+ parts.push("★ primary");
2859
+ const byState = (state) => [...this.tasks.values()].filter((task2) => task2.agent === agent2.name && task2.state === state).length;
2860
+ const running = byState("running");
2861
+ const queued = byState("queued");
2862
+ if (running > 0)
2863
+ parts.push(`${running} running`);
2864
+ if (queued > 0)
2865
+ parts.push(`${queued} queued`);
2866
+ const rules = [...catalog.permissions, ...agent2.permissions].length;
2867
+ parts.push(rules > 0 ? `${rules} rules` : "ask by default");
2868
+ return parts.length > 0 ? parts.join(" · ") : "—";
2869
+ };
2831
2870
  const lines2 = [
2832
2871
  tabs,
2833
2872
  "",
2834
- theme.fg("dim", ` ${pad("Agent", nameWidth)} ${pad("Source", sourceWidth)} ${pad("Model", modelWidth)} ${pad("State", stateWidth)}`)
2873
+ theme.fg("dim", ` ${pad("Agent", nameWidth)} ${pad("Model", modelWidth)} ${pad("Enabled", enabledWidth)} ${pad("Runtime", runtimeWidth)}`)
2835
2874
  ];
2836
2875
  for (const [offset, agent2] of shown2.entries()) {
2837
2876
  const index = start + offset;
@@ -2840,11 +2879,12 @@ ${this.primaryAgent.prompt}` };
2840
2879
  const disabled = agent2.disabled;
2841
2880
  const deleting = confirmingDeleteAgent === agent2.name;
2842
2881
  const inherited = !disabledOverrides.project.has(agent2.name);
2843
- const state = inherited ? `inherited ${disabled ? "off" : "on"}` : disabled ? "disabled" : "enabled";
2882
+ const enabled = inherited ? `inherited ${disabled ? "off" : "on"}` : disabled ? "disabled" : "enabled";
2844
2883
  const color = deleting ? "error" : disabled ? "muted" : "text";
2845
2884
  const plainName = pad(`@${agent2.name}`, nameWidth);
2846
2885
  const name = deleting ? theme.fg("error", plainName) : disabled ? theme.fg("muted", plainName) : colorizeAgentText(agent2.color, plainName, (agentColor, text) => theme.fg(agentColor, text));
2847
- const line = `${cursor} ${name} ${theme.fg(color, pad(agent2.source, sourceWidth))} ${theme.fg(color, pad(agent2.model ?? "current model", modelWidth))} ${theme.fg(deleting ? "error" : disabled ? "warning" : color, pad(state, stateWidth))}`;
2886
+ const runtime = runtimeFor(agent2);
2887
+ const line = `${cursor} ${name} ${theme.fg(color, pad(agent2.model ?? "current model", modelWidth))} ${theme.fg(deleting ? "error" : disabled ? "warning" : color, pad(enabled, enabledWidth))} ${theme.fg(disabled ? "muted" : "dim", pad(runtime, runtimeWidth))}`;
2848
2888
  lines2.push(agent2.name === this.primaryAgent?.name ? theme.bold(line) : line);
2849
2889
  }
2850
2890
  if (listedAgents.length === 0) {
@@ -2854,6 +2894,7 @@ ${this.primaryAgent.prompt}` };
2854
2894
  if (agent) {
2855
2895
  lines2.push("", theme.fg(agent.disabled ? "muted" : "text", agent.description ?? "No description"));
2856
2896
  const details = [
2897
+ agent.source,
2857
2898
  agent.variant ? `variant ${agent.variant}` : undefined,
2858
2899
  agent.steps ? `${agent.steps} steps` : undefined
2859
2900
  ].filter(Boolean);
@@ -2886,44 +2927,14 @@ ${this.primaryAgent.prompt}` };
2886
2927
  }
2887
2928
  if (confirmingDeleteAgent) {
2888
2929
  lines2.push("", theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
2889
- ["Y", "delete"],
2890
- ["N / Esc", "cancel"]
2930
+ ["Enter", "confirm"],
2931
+ ["Esc", "cancel"]
2891
2932
  ]));
2892
2933
  } else if (saving) {
2893
2934
  lines2.push("", theme.fg("dim", "Saving…"));
2894
2935
  }
2895
2936
  return box(lines2);
2896
2937
  }
2897
- if (tab === "settings") {
2898
- const maxSubagentsInherited = maxSubagentsSettings.project === undefined;
2899
- const rows = [
2900
- {
2901
- label: "maxSubagents",
2902
- value: String(selectedMaxSubagents()),
2903
- inherited: maxSubagentsInherited,
2904
- unavailable: false
2905
- }
2906
- ];
2907
- const lines2 = [tabs, ""];
2908
- if (settingEditor) {
2909
- const current = String(selectedMaxSubagents());
2910
- lines2.push(theme.fg("dim", ` Project maxSubagents · current ${current}`), "", ` ${theme.fg("accent", "›")} [ ${settingEditor.value}${theme.fg("accent", "█")} ]`, "", theme.fg("dim", " empty inherit · enter submit · esc cancel"));
2911
- if (saving)
2912
- lines2.push("", theme.fg("dim", "Saving…"));
2913
- return box(lines2);
2914
- }
2915
- for (const [index, row] of rows.entries()) {
2916
- const selected = index === selectedSetting;
2917
- const cursor = selected ? theme.fg("accent", "›") : " ";
2918
- const text = `[ ${row.value} ]`;
2919
- const value = row.inherited ? theme.fg("dim", text) : selected ? theme.fg("accent", text) : theme.fg(row.unavailable ? "muted" : "text", text);
2920
- const label = theme.fg(row.unavailable ? "muted" : "text", row.label);
2921
- lines2.push(`${cursor} ${value} ${label}`);
2922
- }
2923
- if (saving)
2924
- lines2.push("", theme.fg("dim", "Saving…"));
2925
- return box(lines2);
2926
- }
2927
2938
  if (tab === "help") {
2928
2939
  const shortcutWidth = Math.max(visibleWidth2("Shortcut"), ...AGENTS_HELP_ROWS.map(([shortcut]) => visibleWidth2(shortcut)));
2929
2940
  const descriptionWidth = Math.max(1, contentWidth - shortcutWidth - 4);
@@ -2960,7 +2971,7 @@ ${this.primaryAgent.prompt}` };
2960
2971
  const label = count === 1 ? `Delete ${pendingTaskAction.taskIds[0]?.slice(0, 8) ?? "task"}?` : `Delete ${count} selected task sessions?`;
2961
2972
  listLines.push(`${theme.fg("warning", label)} ${theme.fg("dim", "enter confirm · esc cancel")}`);
2962
2973
  } else {
2963
- listLines.push(theme.fg("dim", "↑↓ move · space select · enter inspect · ctrl+d delete · esc close"));
2974
+ listLines.push(theme.fg("dim", "↑↓ move · space select · enter inspect · x delete · esc close"));
2964
2975
  }
2965
2976
  return box([tabs, "", ...listLines]);
2966
2977
  }
@@ -2996,12 +3007,16 @@ ${this.primaryAgent.prompt}` };
2996
3007
  },
2997
3008
  handleInput: (data) => {
2998
3009
  if (confirmingDeleteAgent !== undefined) {
2999
- if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
3010
+ if (matchesKey(data, "ctrl+c")) {
3011
+ done();
3012
+ return;
3013
+ }
3014
+ if (matchesKey(data, "escape")) {
3000
3015
  confirmingDeleteAgent = undefined;
3001
3016
  tui.requestRender();
3002
3017
  return;
3003
3018
  }
3004
- if (data.toLowerCase() === "y") {
3019
+ if (matchesKey(data, "return")) {
3005
3020
  const name = confirmingDeleteAgent;
3006
3021
  const agent = agents.find((candidate) => candidate.name === name);
3007
3022
  confirmingDeleteAgent = undefined;
@@ -3024,10 +3039,8 @@ ${this.primaryAgent.prompt}` };
3024
3039
  return;
3025
3040
  }
3026
3041
  if (matchesKey(data, "tab")) {
3027
- if (settingEditor !== undefined)
3028
- return;
3029
3042
  const selectedName = tab === "primary" || tab === "subagent" ? agentsForTab()[selectedAgent]?.name : undefined;
3030
- const tabs = ["primary", "subagent", "tasks", "log", "settings", "help"];
3043
+ const tabs = ["primary", "subagent", "tasks", "log", "help"];
3031
3044
  tab = tabs[(tabs.indexOf(tab) + 1) % tabs.length] ?? "primary";
3032
3045
  if (tab === "primary" || tab === "subagent") {
3033
3046
  const listedAgents = agentsForTab();
@@ -3123,98 +3136,6 @@ ${this.primaryAgent.prompt}` };
3123
3136
  tui.requestRender();
3124
3137
  return;
3125
3138
  }
3126
- if (tab === "settings") {
3127
- if (saving)
3128
- return;
3129
- if (matchesKey(data, "ctrl+c")) {
3130
- done();
3131
- return;
3132
- }
3133
- if (settingEditor) {
3134
- if (matchesKey(data, "escape")) {
3135
- settingEditor = undefined;
3136
- tui.requestRender();
3137
- return;
3138
- }
3139
- if (matchesKey(data, "backspace")) {
3140
- settingEditor.value = settingEditor.value.slice(0, -1);
3141
- tui.requestRender();
3142
- return;
3143
- }
3144
- if (matchesKey(data, "return")) {
3145
- const input = settingEditor.value.trim();
3146
- let save;
3147
- let notifyMessage;
3148
- if (input === "") {
3149
- if (!projectTrusted) {
3150
- ctx.ui.notify("Project settings require a trusted project", "warning");
3151
- return;
3152
- }
3153
- save = clearMaxSubagentsConfigForScope(ctx.cwd, "project");
3154
- notifyMessage = "Project maxSubagents inherits Global";
3155
- } else {
3156
- if (!projectTrusted) {
3157
- ctx.ui.notify("Project settings require a trusted project", "warning");
3158
- return;
3159
- }
3160
- if (!/^\d+$/.test(input)) {
3161
- ctx.ui.notify(`Enter a whole number between 0 and ${MAX_SUBAGENTS}`, "error");
3162
- return;
3163
- }
3164
- const parsed = Number(input);
3165
- if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > MAX_SUBAGENTS) {
3166
- ctx.ui.notify(`Enter a whole number between 0 and ${MAX_SUBAGENTS}`, "error");
3167
- return;
3168
- }
3169
- save = setMaxSubagentsConfigForScope(ctx.cwd, parsed, "project");
3170
- notifyMessage = `Project maxSubagents = ${parsed}`;
3171
- }
3172
- saving = true;
3173
- settingEditor = undefined;
3174
- tui.requestRender();
3175
- save.then(() => {
3176
- reloadSettings();
3177
- const loadedMaxSubagents = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
3178
- this.setMaxSubagents(loadedMaxSubagents.project ?? loadedMaxSubagents.global);
3179
- maxSubagentsSettings = loadedMaxSubagents;
3180
- ctx.ui.notify(notifyMessage ?? "Setting updated", "info");
3181
- }).catch((error) => ctx.ui.notify(`Could not save setting: ${formatError(error)}`, "error")).finally(() => {
3182
- saving = false;
3183
- tui.requestRender();
3184
- });
3185
- return;
3186
- }
3187
- const kittyChar = decodeKittyPrintable(data);
3188
- const printable = kittyChar ?? (data.length === 1 && !isControlChar(data) ? data : undefined);
3189
- if (printable === undefined)
3190
- return;
3191
- if (!/^\d$/.test(printable))
3192
- return;
3193
- settingEditor.value += printable;
3194
- tui.requestRender();
3195
- return;
3196
- }
3197
- if (matchesKey(data, "escape")) {
3198
- done();
3199
- return;
3200
- }
3201
- if (matchesKey(data, "up"))
3202
- selectedSetting = 0;
3203
- else if (matchesKey(data, "down"))
3204
- selectedSetting = 0;
3205
- else if (matchesKey(data, "return")) {
3206
- if (!projectTrusted) {
3207
- ctx.ui.notify("Project settings require a trusted project", "warning");
3208
- return;
3209
- }
3210
- settingEditor = {
3211
- value: maxSubagentsSettings.project !== undefined ? String(maxSubagentsSettings.project) : ""
3212
- };
3213
- } else
3214
- return;
3215
- tui.requestRender();
3216
- return;
3217
- }
3218
3139
  if (tab === "help") {
3219
3140
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
3220
3141
  done();
@@ -3235,7 +3156,11 @@ ${this.primaryAgent.prompt}` };
3235
3156
  tui.requestRender();
3236
3157
  return;
3237
3158
  }
3238
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
3159
+ if (matchesKey(data, "ctrl+c")) {
3160
+ done();
3161
+ return;
3162
+ }
3163
+ if (matchesKey(data, "escape")) {
3239
3164
  pendingTaskAction = undefined;
3240
3165
  tui.requestRender();
3241
3166
  }
@@ -3253,7 +3178,7 @@ ${this.primaryAgent.prompt}` };
3253
3178
  return;
3254
3179
  if (!selectedTaskIds.delete(task2.id))
3255
3180
  selectedTaskIds.add(task2.id);
3256
- } else if (matchesKey(data, "ctrl+d")) {
3181
+ } else if (data.toLowerCase() === "x") {
3257
3182
  const targets = taskTargets();
3258
3183
  if (targets.length === 0)
3259
3184
  return;
@@ -3315,6 +3240,262 @@ ${this.primaryAgent.prompt}` };
3315
3240
  invalidate() {}
3316
3241
  }), { overlay: true, overlayOptions: { anchor: "center", width: 96, margin: 2 } });
3317
3242
  }
3243
+ async openSettings(ctx) {
3244
+ if (ctx.mode !== "tui") {
3245
+ ctx.ui.notify("Settings are available in TUI mode", "warning");
3246
+ return;
3247
+ }
3248
+ const projectTrusted = isProjectTrusted(ctx);
3249
+ let tab = "sandbox";
3250
+ let overview = this.integration.sandboxCallbacks?.overview?.(ctx.cwd, projectTrusted);
3251
+ let maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
3252
+ let policySettings = loadToolFilesystemPolicySettings(ctx.cwd, projectTrusted);
3253
+ let selectedSetting = 0;
3254
+ let settingEditor;
3255
+ let saving = false;
3256
+ await ctx.ui.custom((tui, theme, _keybindings, done) => ({
3257
+ render: (width) => {
3258
+ const contentWidth = Math.max(1, width - 4);
3259
+ const box = (lines2) => [
3260
+ boxTop(theme, width, "Settings"),
3261
+ ...lines2.map((line) => boxRow(theme, width, line)),
3262
+ boxBottom(theme, width)
3263
+ ];
3264
+ const tabs = dialogTabs(theme, ["Sandbox", "Agents"], tab === "sandbox" ? "Sandbox" : "Agents");
3265
+ const listValue = (values) => {
3266
+ const value = values.join(", ") || "none";
3267
+ return truncateToWidth2(value, Math.max(10, contentWidth - 20));
3268
+ };
3269
+ if (tab === "sandbox") {
3270
+ const lines2 = [tabs, ""];
3271
+ if (!overview) {
3272
+ lines2.push(theme.fg("muted", " Sandbox settings are unavailable."));
3273
+ lines2.push("", dialogKeys(theme, [["Esc", "close"]]));
3274
+ return box(lines2);
3275
+ }
3276
+ 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" };
3277
+ const networkMode = overview.networkMode;
3278
+ const shellReadMode = overview.windowsMode !== undefined ? "Policy (required)" : overview.shellReadMode === "host" ? "Host-aligned" : "Policy";
3279
+ const changePath = overview.changeScope === "project" ? overview.paths.project : overview.paths.global;
3280
+ const item = (label, value) => ` ${theme.fg("dim", label.padEnd(16))} ${value}`;
3281
+ lines2.push(`${theme.fg(status.color, "●")} ${theme.fg("text", status.label)}`, item("Configured", overview.enabled ? theme.fg("text", "Enabled") : theme.fg("warning", "Disabled")), item("Runtime", overview.running ? theme.fg("success", "Running") : theme.fg("dim", "Not running")), "", theme.fg("accent", "Protection"), item("Network", networkMode), item("Shell reads", shellReadMode), item("Read rules", `${overview.config.filesystem.denyRead.length + overview.config.filesystem.allowRead.length} · ${overview.readRuleScope}`), item("Write rules", String(overview.config.filesystem.allowWrite.length + overview.config.filesystem.denyWrite.length)), item("Session grants", String(overview.sessionDomains.length + overview.sessionReadPaths.length + overview.sessionWritePaths.length)), "", theme.fg("accent", "Configuration"), item("Change scope", overview.changeScope === "project" ? "Project" : "Global"), item("Change file", truncateToWidth2(changePath, Math.max(10, contentWidth - 20))), item("Project file", truncateToWidth2(overview.paths.project, Math.max(10, contentWidth - 20))), item("Global file", truncateToWidth2(overview.paths.global, Math.max(10, contentWidth - 20))), item("Landstrip binary", truncateToWidth2(overview.paths.binary, Math.max(10, contentWidth - 20))));
3282
+ if (overview.windowsMode !== undefined)
3283
+ lines2.push(item("Windows mode", overview.windowsMode));
3284
+ lines2.push("", theme.fg("accent", "Network"), item("Allowed domains", listValue(overview.config.network.allowedDomains)), item("Denied domains", listValue(overview.config.network.deniedDomains)), item("Unix sockets", overview.config.network.allowAllUnixSockets ? "all" : listValue(overview.config.network.allowUnixSockets)), "", theme.fg("accent", "Filesystem"), item("Shell reads", shellReadMode), item("Read rules for", overview.readRuleScope), item("Denied reads", listValue(overview.config.filesystem.denyRead)), item("Allowed reads", listValue(overview.config.filesystem.allowRead)), item("Allowed writes", listValue(overview.config.filesystem.allowWrite)), item("Denied writes", listValue(overview.config.filesystem.denyWrite)), "", theme.fg("accent", "Session grants"), item("Domains", listValue(overview.sessionDomains)), item("Read paths", listValue(overview.sessionReadPaths)), item("Write paths", listValue(overview.sessionWritePaths)));
3285
+ if (saving)
3286
+ lines2.push("", theme.fg("dim", "Saving…"));
3287
+ lines2.push("", dialogKeys(theme, [
3288
+ ["Tab", "next tab"],
3289
+ ["Enter", `${overview.enabled ? "disable" : "enable"} in ${overview.changeScope}`],
3290
+ ["Esc", "close"]
3291
+ ]));
3292
+ return box(lines2);
3293
+ }
3294
+ const rows = [
3295
+ {
3296
+ kind: "maxSubagents",
3297
+ label: "maxSubagents",
3298
+ value: String(maxSubagentsSettings.project ?? maxSubagentsSettings.global),
3299
+ inherited: maxSubagentsSettings.project === undefined
3300
+ },
3301
+ {
3302
+ kind: "toolFilesystemPolicy",
3303
+ label: "toolFilesystemPolicy",
3304
+ value: policySettings.project ?? policySettings.global,
3305
+ inherited: policySettings.project === undefined
3306
+ }
3307
+ ];
3308
+ const lines = [tabs, ""];
3309
+ if (settingEditor) {
3310
+ const kindLabel = settingEditor.kind === "maxSubagents" ? "Project maxSubagents" : "Project toolFilesystemPolicy";
3311
+ const current = settingEditor.kind === "maxSubagents" ? String(maxSubagentsSettings.project ?? maxSubagentsSettings.global) : policySettings.project ?? policySettings.global;
3312
+ const hint = settingEditor.kind === "maxSubagents" ? "empty inherit · enter submit · esc cancel" : "h host · s sandbox · backspace inherit · enter submit · esc cancel";
3313
+ lines.push(theme.fg("dim", ` ${kindLabel} · current ${current}`), "", ` ${theme.fg("accent", "›")} [ ${settingEditor.value}${theme.fg("accent", "█")} ]`, "", theme.fg("dim", ` ${hint}`));
3314
+ if (saving)
3315
+ lines.push("", theme.fg("dim", "Saving…"));
3316
+ return box(lines);
3317
+ }
3318
+ for (const [index, row] of rows.entries()) {
3319
+ const selected = index === selectedSetting;
3320
+ const cursor = selected ? theme.fg("accent", "›") : " ";
3321
+ const value = row.inherited ? theme.fg("dim", `[ ${row.value} ]`) : selected ? theme.fg("accent", `[ ${row.value} ]`) : theme.fg("text", `[ ${row.value} ]`);
3322
+ lines.push(`${cursor} ${value} ${theme.fg("text", row.label)}`);
3323
+ }
3324
+ lines.push("", theme.fg("dim", " enter edit · ↑↓ select · esc close"));
3325
+ if (saving)
3326
+ lines.push("", theme.fg("dim", "Saving…"));
3327
+ return box(lines);
3328
+ },
3329
+ handleInput: (data) => {
3330
+ if (saving)
3331
+ return;
3332
+ if (matchesKey(data, "ctrl+c")) {
3333
+ done();
3334
+ return;
3335
+ }
3336
+ if (matchesKey(data, "escape")) {
3337
+ if (settingEditor) {
3338
+ settingEditor = undefined;
3339
+ tui.requestRender();
3340
+ return;
3341
+ }
3342
+ done();
3343
+ return;
3344
+ }
3345
+ if (settingEditor) {
3346
+ if (matchesKey(data, "backspace")) {
3347
+ settingEditor.value = settingEditor.kind === "maxSubagents" ? settingEditor.value.slice(0, -1) : "";
3348
+ tui.requestRender();
3349
+ return;
3350
+ }
3351
+ if (matchesKey(data, "return")) {
3352
+ const input = settingEditor.value.trim();
3353
+ if (!projectTrusted) {
3354
+ ctx.ui.notify("Project settings require a trusted project", "warning");
3355
+ return;
3356
+ }
3357
+ let save;
3358
+ let notifyMessage;
3359
+ if (settingEditor.kind === "maxSubagents") {
3360
+ if (input === "") {
3361
+ save = clearMaxSubagentsConfigForScope(ctx.cwd, "project");
3362
+ notifyMessage = "Project maxSubagents inherits Global";
3363
+ } else {
3364
+ if (!/^\d+$/.test(input)) {
3365
+ ctx.ui.notify(`Enter a whole number between 0 and ${MAX_SUBAGENTS}`, "error");
3366
+ return;
3367
+ }
3368
+ const parsed = Number(input);
3369
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > MAX_SUBAGENTS) {
3370
+ ctx.ui.notify(`Enter a whole number between 0 and ${MAX_SUBAGENTS}`, "error");
3371
+ return;
3372
+ }
3373
+ save = setMaxSubagentsConfigForScope(ctx.cwd, parsed, "project");
3374
+ notifyMessage = `Project maxSubagents = ${parsed}`;
3375
+ }
3376
+ } else {
3377
+ if (input === "") {
3378
+ save = clearToolFilesystemPolicyConfigForScope(ctx.cwd, "project");
3379
+ notifyMessage = "Project toolFilesystemPolicy inherits Global";
3380
+ } else {
3381
+ if (input !== "host" && input !== "sandbox") {
3382
+ ctx.ui.notify("Enter host or sandbox", "error");
3383
+ return;
3384
+ }
3385
+ const policy = input;
3386
+ save = setToolFilesystemPolicyConfigForScope(ctx.cwd, policy, "project");
3387
+ notifyMessage = `Project toolFilesystemPolicy = ${policy}`;
3388
+ }
3389
+ }
3390
+ const kind = settingEditor.kind;
3391
+ saving = true;
3392
+ settingEditor = undefined;
3393
+ tui.requestRender();
3394
+ save.then(() => {
3395
+ if (kind === "maxSubagents") {
3396
+ maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
3397
+ this.setMaxSubagents(maxSubagentsSettings.project ?? maxSubagentsSettings.global);
3398
+ } else {
3399
+ policySettings = loadToolFilesystemPolicySettings(ctx.cwd, projectTrusted);
3400
+ this.toolFilesystemPolicy = policySettings.project ?? policySettings.global;
3401
+ }
3402
+ ctx.ui.notify(notifyMessage, "info");
3403
+ }).catch((error) => ctx.ui.notify(`Could not save setting: ${formatError(error)}`, "error")).finally(() => {
3404
+ saving = false;
3405
+ tui.requestRender();
3406
+ });
3407
+ return;
3408
+ }
3409
+ const kittyChar = decodeKittyPrintable(data);
3410
+ const printable = kittyChar ?? (data.length === 1 && !isControlChar(data) ? data : undefined);
3411
+ if (printable === undefined)
3412
+ return;
3413
+ if (settingEditor.kind === "maxSubagents") {
3414
+ if (!/^\d$/.test(printable))
3415
+ return;
3416
+ settingEditor.value += printable;
3417
+ } else {
3418
+ const lower = printable.toLowerCase();
3419
+ if (lower !== "h" && lower !== "s")
3420
+ return;
3421
+ settingEditor.value = lower === "h" ? "host" : "sandbox";
3422
+ }
3423
+ tui.requestRender();
3424
+ return;
3425
+ }
3426
+ if (matchesKey(data, "tab")) {
3427
+ tab = tab === "sandbox" ? "agents" : "sandbox";
3428
+ tui.requestRender();
3429
+ return;
3430
+ }
3431
+ if (tab === "sandbox") {
3432
+ if (!matchesKey(data, "return"))
3433
+ return;
3434
+ const callbacks = this.integration.sandboxCallbacks;
3435
+ const current = overview;
3436
+ if (!callbacks || !current) {
3437
+ ctx.ui.notify("Sandbox settings are unavailable", "warning");
3438
+ return;
3439
+ }
3440
+ saving = true;
3441
+ tui.requestRender();
3442
+ (async () => {
3443
+ try {
3444
+ const enabled = !current.enabled;
3445
+ if (!enabled) {
3446
+ const confirmed = await ctx.ui.confirm("Disable sandbox?", `Commands will run without OS isolation.
3447
+
3448
+ Change: ${current.changeScope} configuration
3449
+ ${current.changeScope === "project" ? current.paths.project : current.paths.global}`);
3450
+ if (!confirmed)
3451
+ return;
3452
+ }
3453
+ await callbacks.setEnabled(ctx, enabled, current.changeScope);
3454
+ overview = callbacks.overview(ctx.cwd, projectTrusted);
3455
+ if (enabled && overview.noSandboxFlag) {
3456
+ ctx.ui.notify("Sandbox remains disabled via --no-sandbox", "warning");
3457
+ } else {
3458
+ ctx.ui.notify(`Sandbox ${enabled ? "enabled" : "disabled"} in ${current.changeScope} config`, "info");
3459
+ }
3460
+ } catch (error) {
3461
+ ctx.ui.notify(`Could not update config: ${formatError(error)}`, "error");
3462
+ } finally {
3463
+ saving = false;
3464
+ tui.requestRender();
3465
+ }
3466
+ })();
3467
+ return;
3468
+ }
3469
+ if (matchesKey(data, "up"))
3470
+ selectedSetting = Math.max(0, selectedSetting - 1);
3471
+ else if (matchesKey(data, "down"))
3472
+ selectedSetting = Math.min(1, selectedSetting + 1);
3473
+ else if (matchesKey(data, "return")) {
3474
+ if (!projectTrusted) {
3475
+ ctx.ui.notify("Project settings require a trusted project", "warning");
3476
+ return;
3477
+ }
3478
+ if (selectedSetting === 0) {
3479
+ settingEditor = {
3480
+ kind: "maxSubagents",
3481
+ value: maxSubagentsSettings.project !== undefined ? String(maxSubagentsSettings.project) : ""
3482
+ };
3483
+ } else {
3484
+ settingEditor = {
3485
+ kind: "toolFilesystemPolicy",
3486
+ value: policySettings.project ?? policySettings.global
3487
+ };
3488
+ }
3489
+ } else
3490
+ return;
3491
+ tui.requestRender();
3492
+ },
3493
+ invalidate() {}
3494
+ }), {
3495
+ overlay: true,
3496
+ overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "60%" }
3497
+ });
3498
+ }
3318
3499
  async selectPrimaryAgent(name, ctx) {
3319
3500
  const catalog = this.getAgentCatalog(ctx);
3320
3501
  for (const diagnostic of catalog.diagnostics)
@@ -4582,11 +4763,6 @@ async function writeSandboxConfigEnabled(cwd, enabled, scope) {
4582
4763
  writeConfigFile(path, config);
4583
4764
  });
4584
4765
  }
4585
- async function setSandboxConfigEnabled(cwd, enabled, includeProject = true) {
4586
- const { scope } = getSandboxConfigWriteTarget(cwd, includeProject);
4587
- await writeSandboxConfigEnabled(cwd, enabled, scope);
4588
- return scope;
4589
- }
4590
4766
  function loadSandboxEnabledSettings(cwd, includeProject) {
4591
4767
  const { projectPath } = getConfigPaths(cwd);
4592
4768
  const global = loadSandboxConfig(cwd, false).enabled;
@@ -4778,14 +4954,6 @@ function setTuiStatus(ctx, key, value) {
4778
4954
  return;
4779
4955
  ctx.ui.setStatus(key, value);
4780
4956
  }
4781
- function themeColors(theme) {
4782
- return {
4783
- dim: (value) => theme.fg("dim", value),
4784
- muted: (value) => theme.fg("muted", value),
4785
- accent: (value) => theme.fg("accent", value),
4786
- text: (value) => theme.fg("text", value)
4787
- };
4788
- }
4789
4957
  function patchProcessGroupKill(child) {
4790
4958
  const kill = child.kill.bind(child);
4791
4959
  child.kill = (signal) => {
@@ -6437,110 +6605,6 @@ ${message}
6437
6605
  unpublishRuntime?.();
6438
6606
  unpublishRuntime = undefined;
6439
6607
  });
6440
- maybePi.registerCommand?.("sandbox", {
6441
- description: "Inspect and toggle the sandbox",
6442
- handler: async (_args, ctx) => {
6443
- if (!ctx.hasUI)
6444
- return;
6445
- const trustContext = ctx;
6446
- const projectTrusted = trustContext.isProjectTrusted?.() ?? projectConfigTrusted;
6447
- const config = loadSandboxConfig(ctx.cwd, projectTrusted);
6448
- const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
6449
- const windowsImplementation = process.platform === "win32" ? activeWindowsImplementation() : undefined;
6450
- const target = getSandboxConfigWriteTarget(ctx.cwd, projectTrusted);
6451
- let tab = "Overview";
6452
- const shouldToggle = await ctx.ui.custom((tui, theme, _kb, done) => {
6453
- const { muted, accent, text } = themeColors(theme);
6454
- const sandboxStatus = () => {
6455
- if (getNoSandboxFlag())
6456
- return { color: "warning", label: "Disabled by --no-sandbox" };
6457
- if (!config.enabled)
6458
- return { color: "warning", label: "Disabled by configuration" };
6459
- if (!sandboxEnabled || !sandboxReady)
6460
- return { color: "warning", label: "Inactive" };
6461
- return { color: "success", label: "Active" };
6462
- };
6463
- const networkMode = config.network.allowNetwork ? "Unrestricted" : windowsImplementation === "appContainer" && !config.windows.allowLoopback ? "Blocked" : "Proxied";
6464
- const shellReadMode = process.platform === "win32" ? "Policy (required)" : config.shell.readAccess === "host" ? "Host-aligned" : "Policy";
6465
- const readRuleScope = enforcesShellReadPolicy(config.shell.readAccess) ? "Workers and shell" : "Workers only";
6466
- return {
6467
- render(width) {
6468
- const innerWidth = Math.max(1, width - 4);
6469
- const status = sandboxStatus();
6470
- const tabs = dialogTabs(theme, ["Overview", "Policy"], tab);
6471
- const listValue = (values) => {
6472
- const value = values.join(", ") || "none";
6473
- return text(truncateToWidth3(value, Math.max(10, innerWidth - 20)));
6474
- };
6475
- const item = (label, value) => ` ${muted(label.padEnd(16))} ${value}`;
6476
- const footer = dialogKeys(theme, [
6477
- ["Tab", "next tab"],
6478
- ["Enter", `${config.enabled ? "disable" : "enable"} in ${target.scope}`],
6479
- ["Esc", "close"]
6480
- ]);
6481
- const content = [tabs, ""];
6482
- if (tab === "Overview") {
6483
- content.push(`${theme.fg(status.color, "●")} ${text(status.label)}`, item("Configured", config.enabled ? text("Enabled") : theme.fg("warning", "Disabled")), item("Runtime", sandboxEnabled && sandboxReady ? theme.fg("success", "Running") : muted("Not running")), "", accent("Protection"), item("Network", text(networkMode)), item("Shell reads", text(shellReadMode)), item("Read rules", text(`${config.filesystem.denyRead.length + config.filesystem.allowRead.length} · ${readRuleScope}`)), item("Write rules", text(String(config.filesystem.allowWrite.length + config.filesystem.denyWrite.length))), item("Session grants", text(String(sessionAllowedDomains.length + sessionAllowedReadPaths.length + sessionAllowedWritePaths.length))), "", accent("Configuration"), item("Change scope", text(target.scope === "project" ? "Project" : "Global")), item("Change file", text(truncateToWidth3(target.path, Math.max(10, innerWidth - 20)))), item("Project file", text(truncateToWidth3(projectPath, Math.max(10, innerWidth - 20)))), item("Global file", text(truncateToWidth3(globalPath, Math.max(10, innerWidth - 20)))), item("Landstrip binary", text(truncateToWidth3(binaryPath(), Math.max(10, innerWidth - 20)))));
6484
- if (windowsImplementation !== undefined) {
6485
- content.push(item("Windows mode", text(windowsImplementation === "restrictedUser" ? "Restricted user" : `AppContainer (${config.windows.appContainerMode})`)));
6486
- }
6487
- } else {
6488
- content.push(accent(`Network · ${networkMode}`), item("Allowed domains", listValue(config.network.allowedDomains)), item("Denied domains", listValue(config.network.deniedDomains)), item("Unix sockets", config.network.allowAllUnixSockets ? text("all") : listValue(config.network.allowUnixSockets)), "", accent("Filesystem"), item("Shell reads", text(shellReadMode)), item("Read rules for", text(readRuleScope)), item("Denied reads", listValue(config.filesystem.denyRead)), item("Allowed reads", listValue(config.filesystem.allowRead)), item("Allowed writes", listValue(config.filesystem.allowWrite)), item("Denied writes", listValue(config.filesystem.denyWrite)), "", accent("Session grants"), item("Domains", listValue(sessionAllowedDomains)), item("Read paths", listValue(sessionAllowedReadPaths)), item("Write paths", listValue(sessionAllowedWritePaths)));
6489
- }
6490
- content.push("", footer);
6491
- return [
6492
- boxTop(theme, width, "Sandbox"),
6493
- ...content.map((line) => boxRow(theme, width, line)),
6494
- boxBottom(theme, width)
6495
- ];
6496
- },
6497
- handleInput(data) {
6498
- if (matchesKey2(data, "tab")) {
6499
- tab = tab === "Overview" ? "Policy" : "Overview";
6500
- tui.requestRender();
6501
- return;
6502
- }
6503
- if (matchesKey2(data, "return")) {
6504
- done(true);
6505
- return;
6506
- }
6507
- if (matchesKey2(data, "escape") || matchesKey2(data, "ctrl+c"))
6508
- done(false);
6509
- },
6510
- invalidate() {}
6511
- };
6512
- }, {
6513
- overlay: true,
6514
- overlayOptions: { anchor: "center", width: 78, margin: 2 }
6515
- });
6516
- if (!shouldToggle)
6517
- return;
6518
- const enabled = !config.enabled;
6519
- if (!enabled) {
6520
- const confirmed = await ctx.ui.confirm("Disable sandbox?", `Commands will run without OS isolation.
6521
-
6522
- Change: ${target.scope} configuration
6523
- ${target.path}`);
6524
- if (!confirmed)
6525
- return;
6526
- }
6527
- try {
6528
- const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectTrusted);
6529
- if (!enabled) {
6530
- disableSandbox(ctx);
6531
- } else if (!getNoSandboxFlag()) {
6532
- enableSandbox(ctx);
6533
- }
6534
- if (enabled && getNoSandboxFlag()) {
6535
- notify(ctx, "Sandbox remains disabled via --no-sandbox", "warning");
6536
- } else {
6537
- notify(ctx, `Sandbox ${enabled ? "enabled" : "disabled"} in ${scope} config`, "info");
6538
- }
6539
- } catch (error) {
6540
- notify(ctx, `Could not update config: ${error}`, "error");
6541
- }
6542
- }
6543
- });
6544
6608
  }
6545
6609
  async function applySandboxSetting(ctx, enabled, scope) {
6546
6610
  await writeSandboxConfigEnabled(ctx.cwd, enabled, scope);
@@ -6551,10 +6615,31 @@ ${target.path}`);
6551
6615
  enableSandbox(ctx);
6552
6616
  }
6553
6617
  }
6618
+ function sandboxOverview(cwd, includeProject) {
6619
+ const config = loadSandboxConfig(cwd, includeProject);
6620
+ const { globalPath, projectPath } = getConfigPaths(cwd);
6621
+ const windowsImplementation = process.platform === "win32" ? activeWindowsImplementation() : undefined;
6622
+ return {
6623
+ enabled: config.enabled,
6624
+ running: sandboxEnabled && sandboxReady,
6625
+ noSandboxFlag: getNoSandboxFlag(),
6626
+ networkMode: config.network.allowNetwork ? "unrestricted" : windowsImplementation === "appContainer" && !config.windows.allowLoopback ? "blocked" : "proxied",
6627
+ shellReadMode: process.platform === "win32" ? "policy" : config.shell.readAccess,
6628
+ readRuleScope: enforcesShellReadPolicy(config.shell.readAccess) ? "Workers and shell" : "Workers only",
6629
+ windowsMode: windowsImplementation === undefined ? undefined : windowsImplementation === "restrictedUser" ? "Restricted user" : `AppContainer (${config.windows.appContainerMode})`,
6630
+ changeScope: getSandboxConfigWriteTarget(cwd, includeProject).scope,
6631
+ paths: { global: globalPath, project: projectPath, binary: binaryPath() },
6632
+ config,
6633
+ sessionDomains: [...sessionAllowedDomains],
6634
+ sessionReadPaths: [...sessionAllowedReadPaths],
6635
+ sessionWritePaths: [...sessionAllowedWritePaths]
6636
+ };
6637
+ }
6554
6638
  const sandboxCallbacks = {
6555
6639
  load: loadSandboxEnabledSettings,
6556
6640
  setEnabled: (ctx, enabled, scope) => applySandboxSetting(ctx, enabled, scope),
6557
- clearProject: (ctx) => applySandboxSetting(ctx, undefined, "project")
6641
+ clearProject: (ctx) => applySandboxSetting(ctx, undefined, "project"),
6642
+ overview: sandboxOverview
6558
6643
  };
6559
6644
  const integration = {
6560
6645
  version: LANDSTRIP_RUNTIME_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.18.34",
3
+ "version": "0.18.35",
4
4
  "description": "Sandboxed Bash and process-backed agents for Pi",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -39,7 +39,7 @@
39
39
  "ci:test": "npm run test"
40
40
  },
41
41
  "dependencies": {
42
- "@landstrip/landstrip": "^0.18.34",
42
+ "@landstrip/landstrip": "^0.18.35",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"
package/LICENSE DELETED
@@ -1,201 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright [yyyy] [name of copyright owner]
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
201
- limitations under the License.