@yagni-app/code 1.0.6 → 1.0.8
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 +91 -46
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -5
- package/dist/claudePlugins.js +129 -21
- package/dist/cli.js +16 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +53 -28
- package/dist/extension/askUserQuestionTool.js +7 -2
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/config.d.ts +6 -0
- package/dist/extension/hooks.d.ts +3 -3
- package/dist/extension/hooks.js +30 -5
- package/dist/extension/index.d.ts +19 -0
- package/dist/extension/index.js +248 -43
- package/dist/extension/mcp/cliConfig.d.ts +1 -1
- package/dist/extension/mcp/cliConfig.js +1 -1
- package/dist/extension/mcp/config.d.ts +24 -2
- package/dist/extension/mcp/config.js +75 -3
- package/dist/extension/mcp/manager.d.ts +3 -1
- package/dist/extension/mcp/manager.js +2 -2
- package/dist/extension/mcp/panel.d.ts +0 -1
- package/dist/extension/mcp/panel.js +13 -3
- package/dist/extension/mcp/startup.js +8 -6
- package/dist/extension/permission/gate.d.ts +101 -2
- package/dist/extension/permission/gate.js +485 -42
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +64 -0
- package/dist/extension/permissionRules/loadConfig.js +105 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +60 -0
- package/dist/extension/permissionRules/pathRules.js +122 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +115 -0
- package/dist/extension/sandbox/config.js +388 -0
- package/dist/extension/sandbox/manager.d.ts +108 -0
- package/dist/extension/sandbox/manager.js +243 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +829 -0
- package/dist/extension/settingsFiles.d.ts +50 -0
- package/dist/extension/settingsFiles.js +206 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +103 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +47 -0
- package/dist/extension/telemetry/register.js +207 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- package/dist/upgrade.js +10 -1
- package/package.json +19 -3
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /sandbox interactive TUI panel — mirrors Claude Code's SandboxSettings
|
|
3
|
+
* tabs (Mode / Overrides / Config, plus Dependencies when checkDependencies
|
|
4
|
+
* reports errors). Rendered via ctx.ui.custom() in TUI mode; RPC/headless
|
|
5
|
+
* keeps the text summary (custom() is unavailable there).
|
|
6
|
+
*
|
|
7
|
+
* Structure follows Claude's components (SandboxSettings/SandboxModeTab/
|
|
8
|
+
* SandboxOverridesTab/SandboxConfigTab): tabs switched with left/right,
|
|
9
|
+
* SelectList-driven option tabs, read-only config tab, off-state
|
|
10
|
+
* placeholders on Overrides/Config. Copy mirrors Claude's where the
|
|
11
|
+
* mechanism is identical; persistence deviates deliberately (see
|
|
12
|
+
* persistSandboxMode in session.ts) until the project-local settings tier
|
|
13
|
+
* lands.
|
|
14
|
+
*
|
|
15
|
+
* Input contract (verified against pi's interactive mode): pi focuses the
|
|
16
|
+
* ROOT component and calls only root.handleInput(data). Children without
|
|
17
|
+
* their own handleInput (Box, Text) are never consulted — so the panel
|
|
18
|
+
* keeps a direct reference to the active SelectList and forwards keys to
|
|
19
|
+
* it. A Box wrapper does NOT forward input.
|
|
20
|
+
*/
|
|
21
|
+
import { Box, Container, HStack, Key, SelectList, Text, matchesKey, } from "@earendil-works/pi-tui";
|
|
22
|
+
import { mergeRulesIntoSandbox } from "./config.js";
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Pure derivation helpers (unit-tested; no TUI dependency)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Current mode derived from resolved settings (Claude's currentMode). */
|
|
27
|
+
export function deriveCurrentMode(settings, sessionToggledOff) {
|
|
28
|
+
if (!settings.enabled || sessionToggledOff)
|
|
29
|
+
return "disabled";
|
|
30
|
+
return settings.autoAllowBashIfSandboxed !== false ? "auto-allow" : "regular";
|
|
31
|
+
}
|
|
32
|
+
export function deriveCurrentOverride(settings) {
|
|
33
|
+
return settings.allowUnsandboxedCommands !== false ? "open" : "closed";
|
|
34
|
+
}
|
|
35
|
+
export function modeOptions(current) {
|
|
36
|
+
return [
|
|
37
|
+
{ value: "auto-allow", label: `Sandbox BashTool, with auto-allow${current === "auto-allow" ? " (current)" : ""}` },
|
|
38
|
+
{ value: "regular", label: `Sandbox BashTool, with regular permissions${current === "regular" ? " (current)" : ""}` },
|
|
39
|
+
{ value: "disabled", label: `No Sandbox${current === "disabled" ? " (current)" : ""}` },
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
export function overrideOptions(current) {
|
|
43
|
+
return [
|
|
44
|
+
{ value: "open", label: `Allow unsandboxed fallback${current === "open" ? " (current)" : ""}` },
|
|
45
|
+
{ value: "closed", label: `Strict sandbox mode${current === "closed" ? " (current)" : ""}` },
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
/** Per-mode explanation (Claude's copy, adapted to our surfaces). */
|
|
49
|
+
export function modeExplanation(mode) {
|
|
50
|
+
switch (mode) {
|
|
51
|
+
case "auto-allow":
|
|
52
|
+
return "Bash commands run inside the OS sandbox and are auto-allowed; explicit deny/ask permission rules still win. Writes stay confined to the workspace and /tmp; network follows the domain allowlist.";
|
|
53
|
+
case "regular":
|
|
54
|
+
return "Bash commands run inside the OS sandbox, but each command still goes through the normal permission flow (sandboxed execution is not auto-allowed).";
|
|
55
|
+
case "disabled":
|
|
56
|
+
return "No OS-level sandbox. Bash commands run through the ordinary permission system only.";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function overrideExplanation(choice) {
|
|
60
|
+
return choice === "open"
|
|
61
|
+
? "When a command fails due to sandbox restrictions, the agent can retry with dangerouslyDisableSandbox to run outside the sandbox (falling back to default permissions)."
|
|
62
|
+
: "All bash commands invoked by the model must run in the sandbox unless they are explicitly listed in excludedCommands.";
|
|
63
|
+
}
|
|
64
|
+
export function configSections(state) {
|
|
65
|
+
const { settings, merge } = state;
|
|
66
|
+
const sections = [];
|
|
67
|
+
const excluded = settings.excludedCommands ?? [];
|
|
68
|
+
sections.push({
|
|
69
|
+
title: "Excluded Commands",
|
|
70
|
+
detail: [excluded.length > 0 ? excluded.join(", ") : "None"],
|
|
71
|
+
});
|
|
72
|
+
if (merge.filesystem.denyRead.length > 0) {
|
|
73
|
+
const detail = [`Denied: ${merge.filesystem.denyRead.join(", ")}`];
|
|
74
|
+
if (merge.filesystem.allowRead.length > 0)
|
|
75
|
+
detail.push(`Allowed within denied: ${merge.filesystem.allowRead.join(", ")}`);
|
|
76
|
+
sections.push({ title: "Filesystem Read Restrictions", detail });
|
|
77
|
+
}
|
|
78
|
+
if (merge.filesystem.allowWrite.length > 0) {
|
|
79
|
+
const detail = [`Allowed: ${merge.filesystem.allowWrite.join(", ")}`];
|
|
80
|
+
if (merge.filesystem.denyWrite.length > 0)
|
|
81
|
+
detail.push(`Denied within allowed: ${merge.filesystem.denyWrite.join(", ")}`);
|
|
82
|
+
sections.push({ title: "Filesystem Write Restrictions", detail });
|
|
83
|
+
}
|
|
84
|
+
if (merge.network.allowedDomains.length > 0 || merge.network.deniedDomains.length > 0) {
|
|
85
|
+
const detail = [];
|
|
86
|
+
if (merge.network.allowedDomains.length > 0)
|
|
87
|
+
detail.push(`Allowed: ${merge.network.allowedDomains.join(", ")}`);
|
|
88
|
+
if (merge.network.deniedDomains.length > 0)
|
|
89
|
+
detail.push(`Denied: ${merge.network.deniedDomains.join(", ")}`);
|
|
90
|
+
sections.push({ title: "Network Restrictions", detail });
|
|
91
|
+
}
|
|
92
|
+
const unixSockets = settings.network?.allowUnixSockets ?? [];
|
|
93
|
+
if (unixSockets.length > 0)
|
|
94
|
+
sections.push({ title: "Allowed Unix Sockets", detail: [unixSockets.join(", ")] });
|
|
95
|
+
if (state.sessionGrants.length > 0) {
|
|
96
|
+
sections.push({ title: "Session Grants (memory-only)", detail: [state.sessionGrants.join(", ")] });
|
|
97
|
+
}
|
|
98
|
+
return sections;
|
|
99
|
+
}
|
|
100
|
+
/** Tabs offered, mirroring Claude: Dependencies appears only on errors. */
|
|
101
|
+
export function panelTabs(dependencyErrors) {
|
|
102
|
+
const tabs = ["mode", "overrides", "config"];
|
|
103
|
+
if (dependencyErrors.length > 0)
|
|
104
|
+
tabs.push("dependencies");
|
|
105
|
+
return tabs;
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// TUI component
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
const TAB_LABELS = {
|
|
111
|
+
mode: "Mode",
|
|
112
|
+
overrides: "Overrides",
|
|
113
|
+
config: "Config",
|
|
114
|
+
dependencies: "Dependencies",
|
|
115
|
+
};
|
|
116
|
+
/** Horizontal breathing room for panel content — the same left/right
|
|
117
|
+
* margin the footer chip uses. The border lines span the full width;
|
|
118
|
+
* everything between them is inset by this. */
|
|
119
|
+
const CONTENT_PADDING_X = 2;
|
|
120
|
+
function selectListTheme(theme) {
|
|
121
|
+
return {
|
|
122
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
123
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
124
|
+
description: (t) => theme.fg("muted", t),
|
|
125
|
+
scrollInfo: (t) => theme.fg("muted", t),
|
|
126
|
+
noMatch: (t) => theme.fg("muted", t),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Full-width border line (the house DynamicBorder pattern, color fn passed
|
|
130
|
+
* explicitly — extensions never read pi's global theme). */
|
|
131
|
+
class Border {
|
|
132
|
+
color;
|
|
133
|
+
constructor(color) {
|
|
134
|
+
this.color = color;
|
|
135
|
+
}
|
|
136
|
+
render(width) {
|
|
137
|
+
return [this.color("─".repeat(Math.max(1, width)))];
|
|
138
|
+
}
|
|
139
|
+
invalidate() { }
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The /sandbox panel. Structure: top border · tab strip · blank · tab
|
|
143
|
+
* content · blank · footer · bottom border. pi focuses this root and calls
|
|
144
|
+
* its handleInput; the panel forwards keys to the active tab's SelectList
|
|
145
|
+
* (when one exists) and switches tabs on left/right.
|
|
146
|
+
*/
|
|
147
|
+
export class SandboxPanel extends Container {
|
|
148
|
+
state;
|
|
149
|
+
actions;
|
|
150
|
+
theme;
|
|
151
|
+
done;
|
|
152
|
+
activeTab;
|
|
153
|
+
tabs;
|
|
154
|
+
/** The active tab's SelectList (mode/overrides) or null (config/deps) —
|
|
155
|
+
* input forwards here. */
|
|
156
|
+
activeList = null;
|
|
157
|
+
/** Padded content frame; children[0] = tab strip, children[1] = body. */
|
|
158
|
+
content;
|
|
159
|
+
constructor(state, actions, theme, done) {
|
|
160
|
+
super();
|
|
161
|
+
this.state = state;
|
|
162
|
+
this.actions = actions;
|
|
163
|
+
this.theme = theme;
|
|
164
|
+
this.done = done;
|
|
165
|
+
this.tabs = panelTabs(state.dependencyErrors);
|
|
166
|
+
this.activeTab = this.tabs[0];
|
|
167
|
+
this.content = new Box(CONTENT_PADDING_X, 0);
|
|
168
|
+
this.content.addChild(new Text("", 0, 0)); // tab strip placeholder
|
|
169
|
+
this.content.addChild(new Text("", 0, 0)); // body placeholder
|
|
170
|
+
this.addChild(new Border((s) => theme.fg("borderMuted", s)));
|
|
171
|
+
this.addChild(this.content);
|
|
172
|
+
this.addChild(new Border((s) => theme.fg("borderMuted", s)));
|
|
173
|
+
this.setTab(this.activeTab);
|
|
174
|
+
}
|
|
175
|
+
setTab(tab) {
|
|
176
|
+
this.activeTab = tab;
|
|
177
|
+
const t = this.theme;
|
|
178
|
+
const parts = this.tabs.map((candidate) => {
|
|
179
|
+
const active = candidate === tab;
|
|
180
|
+
return t.fg(active ? "accent" : "muted", `${active ? "[" : " "}${TAB_LABELS[candidate]}${active ? "]" : " "}`);
|
|
181
|
+
});
|
|
182
|
+
const strip = new HStack();
|
|
183
|
+
strip.addChild(new Text(parts.join(""), 0, 1));
|
|
184
|
+
this.content.children[0] = strip;
|
|
185
|
+
this.content.children[1] = this.buildBody();
|
|
186
|
+
this.invalidate();
|
|
187
|
+
}
|
|
188
|
+
buildBody() {
|
|
189
|
+
const wrap = new Container();
|
|
190
|
+
wrap.addChild(this.buildTabContent());
|
|
191
|
+
// Footer on its own line with a top margin — a distinct element, and
|
|
192
|
+
// consistent across every tab.
|
|
193
|
+
wrap.addChild(new Text(this.theme.fg("muted", "left/right to switch tabs · up/down to navigate · Enter to select · Esc to close"), 0, 1));
|
|
194
|
+
return wrap;
|
|
195
|
+
}
|
|
196
|
+
buildTabContent() {
|
|
197
|
+
const t = this.theme;
|
|
198
|
+
switch (this.activeTab) {
|
|
199
|
+
case "mode":
|
|
200
|
+
return this.buildModeTab();
|
|
201
|
+
case "overrides":
|
|
202
|
+
return this.buildOverridesTab();
|
|
203
|
+
case "config":
|
|
204
|
+
return this.buildConfigTab();
|
|
205
|
+
case "dependencies":
|
|
206
|
+
return this.buildDependenciesTab();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
buildModeTab() {
|
|
210
|
+
const t = this.theme;
|
|
211
|
+
const current = deriveCurrentMode(this.state.settings, this.state.sessionToggledOff);
|
|
212
|
+
const options = modeOptions(current);
|
|
213
|
+
const list = new SelectList(options, 10, selectListTheme(t));
|
|
214
|
+
const preselect = options.findIndex((o) => o.value === current);
|
|
215
|
+
if (preselect >= 0)
|
|
216
|
+
list.setSelectedIndex(preselect);
|
|
217
|
+
// Top margin (paddingY=1 + the container's separator) separates the
|
|
218
|
+
// explanation from the option list — it's a distinct element and reads
|
|
219
|
+
// too tight flush to it.
|
|
220
|
+
const explanation = new Text(t.fg("muted", modeExplanation(current)), 0, 1);
|
|
221
|
+
list.onSelectionChange = (item) => {
|
|
222
|
+
explanation.setText(t.fg("muted", modeExplanation(item.value)));
|
|
223
|
+
this.invalidate();
|
|
224
|
+
};
|
|
225
|
+
list.onSelect = (item) => {
|
|
226
|
+
// The outcome (confirmation or write-failure) rides done() out of the
|
|
227
|
+
// panel; the session notifies it once custom() resolves — Claude's
|
|
228
|
+
// onComplete(message) flow. It must NOT be dropped here.
|
|
229
|
+
void this.actions
|
|
230
|
+
.onModeSelect(item.value)
|
|
231
|
+
.then((outcome) => this.done(outcome))
|
|
232
|
+
.catch(() => this.done());
|
|
233
|
+
};
|
|
234
|
+
list.onCancel = () => this.done();
|
|
235
|
+
this.activeList = list;
|
|
236
|
+
const wrap = new Container();
|
|
237
|
+
wrap.addChild(new Text(t.bold("Configure mode:"), 0, 0));
|
|
238
|
+
wrap.addChild(list);
|
|
239
|
+
wrap.addChild(explanation);
|
|
240
|
+
return wrap;
|
|
241
|
+
}
|
|
242
|
+
buildOverridesTab() {
|
|
243
|
+
const t = this.theme;
|
|
244
|
+
if (!this.state.settings.enabled) {
|
|
245
|
+
this.activeList = null;
|
|
246
|
+
return new Text(t.fg("muted", "Sandbox is not enabled. Enable sandbox to configure override settings."), 0, 0);
|
|
247
|
+
}
|
|
248
|
+
const current = deriveCurrentOverride(this.state.settings);
|
|
249
|
+
const options = overrideOptions(current);
|
|
250
|
+
const list = new SelectList(options, 10, selectListTheme(t));
|
|
251
|
+
const preselect = options.findIndex((o) => o.value === current);
|
|
252
|
+
if (preselect >= 0)
|
|
253
|
+
list.setSelectedIndex(preselect);
|
|
254
|
+
const explanation = new Text(t.fg("muted", overrideExplanation(current)), 0, 1);
|
|
255
|
+
list.onSelectionChange = (item) => {
|
|
256
|
+
explanation.setText(t.fg("muted", overrideExplanation(item.value)));
|
|
257
|
+
this.invalidate();
|
|
258
|
+
};
|
|
259
|
+
list.onSelect = (item) => {
|
|
260
|
+
void this.actions
|
|
261
|
+
.onOverrideSelect(item.value)
|
|
262
|
+
.then((outcome) => this.done(outcome))
|
|
263
|
+
.catch(() => this.done());
|
|
264
|
+
};
|
|
265
|
+
list.onCancel = () => this.done();
|
|
266
|
+
this.activeList = list;
|
|
267
|
+
const wrap = new Container();
|
|
268
|
+
wrap.addChild(new Text(t.bold("Configure Overrides:"), 0, 0));
|
|
269
|
+
wrap.addChild(list);
|
|
270
|
+
wrap.addChild(explanation);
|
|
271
|
+
return wrap;
|
|
272
|
+
}
|
|
273
|
+
buildConfigTab() {
|
|
274
|
+
const t = this.theme;
|
|
275
|
+
this.activeList = null;
|
|
276
|
+
if (!this.state.settings.enabled) {
|
|
277
|
+
const lines = [t.fg("muted", "Sandbox is not enabled")];
|
|
278
|
+
for (const w of this.state.dependencyWarnings)
|
|
279
|
+
lines.push(t.fg("muted", w));
|
|
280
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
281
|
+
}
|
|
282
|
+
// One block per section, bold title + muted detail lines, with a blank
|
|
283
|
+
// line between sections — a structured read, not one dense paragraph.
|
|
284
|
+
// Sections are one Text block each (bold title + detail lines), and
|
|
285
|
+
// Text(paddingY) pads ABOVE and below — so section 1 stays flush and
|
|
286
|
+
// every later section carries the blank-line separation. No doubling:
|
|
287
|
+
// Container adds no separator of its own.
|
|
288
|
+
const wrap = new Container();
|
|
289
|
+
let first = true;
|
|
290
|
+
for (const section of configSections(this.state)) {
|
|
291
|
+
const block = [t.bold(section.title), ...section.detail.map((d) => t.fg("muted", d))].join("\n");
|
|
292
|
+
wrap.addChild(new Text(block, 0, first ? 0 : 1));
|
|
293
|
+
first = false;
|
|
294
|
+
}
|
|
295
|
+
if (this.state.dependencyWarnings.length > 0) {
|
|
296
|
+
wrap.addChild(new Text(this.state.dependencyWarnings.map((w) => t.fg("warning", w)).join("\n"), 0, 1));
|
|
297
|
+
}
|
|
298
|
+
return wrap;
|
|
299
|
+
}
|
|
300
|
+
buildDependenciesTab() {
|
|
301
|
+
const t = this.theme;
|
|
302
|
+
this.activeList = null;
|
|
303
|
+
const lines = this.state.dependencyErrors.map((e) => t.fg("error", e));
|
|
304
|
+
lines.push(...this.state.dependencyWarnings.map((w) => t.fg("warning", w)));
|
|
305
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
306
|
+
}
|
|
307
|
+
handleInput(data) {
|
|
308
|
+
// Tab switching first — works from every tab (Claude's </> + arrows).
|
|
309
|
+
if (matchesKey(data, Key.left) || matchesKey(data, "<")) {
|
|
310
|
+
const i = this.tabs.indexOf(this.activeTab);
|
|
311
|
+
this.setTab(this.tabs[(i - 1 + this.tabs.length) % this.tabs.length]);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (matchesKey(data, Key.right) || matchesKey(data, ">")) {
|
|
315
|
+
const i = this.tabs.indexOf(this.activeTab);
|
|
316
|
+
this.setTab(this.tabs[(i + 1) % this.tabs.length]);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (matchesKey(data, Key.escape)) {
|
|
320
|
+
this.done();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Everything else (up/down/enter) goes to the active SelectList.
|
|
324
|
+
this.activeList?.handleInput(data);
|
|
325
|
+
this.invalidate();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Build the panel state from the pieces session.ts already holds. Exported
|
|
330
|
+
* for unit tests; the TUI component consumes it.
|
|
331
|
+
*/
|
|
332
|
+
export function buildPanelState(settings, sessionToggledOff, rules, paths, sessionGrants, dependencyStatus) {
|
|
333
|
+
return {
|
|
334
|
+
settings,
|
|
335
|
+
sessionToggledOff,
|
|
336
|
+
dependencyErrors: dependencyStatus.errors,
|
|
337
|
+
dependencyWarnings: dependencyStatus.warnings,
|
|
338
|
+
merge: mergeRulesIntoSandbox(settings, rules, paths),
|
|
339
|
+
sessionGrants: [...sessionGrants],
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=panel.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox session wiring: owns the per-session YagniSandboxManager
|
|
3
|
+
* and hooks it into pi — bash tool re-registration with the sandbox
|
|
4
|
+
* spawnHook, user_bash (! commands) routing, session lifecycle, and the
|
|
5
|
+
* network ask flow (interactive prompt in TUI/RPC, fail-closed in
|
|
6
|
+
* print/headless).
|
|
7
|
+
*
|
|
8
|
+
* Registration model: the sandbox re-registers the bash tool ONLY when
|
|
9
|
+
* sandbox.enabled is true in the loaded settings — default-off sessions keep
|
|
10
|
+
* pi's stock bash byte-identical (a duplicate tool name is a hard 400 on
|
|
11
|
+
* some providers). /sandbox (M5) toggles by rewriting settings and asking
|
|
12
|
+
* for a session restart, the same way Claude persists the toggle.
|
|
13
|
+
*/
|
|
14
|
+
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { type SandboxSettings } from "./config.js";
|
|
16
|
+
import { YagniSandboxManager } from "./manager.js";
|
|
17
|
+
import type { PermissionRule } from "../permissionRules/loadConfig.js";
|
|
18
|
+
/**
|
|
19
|
+
* Build the sandbox bash composition: given ANY stock bash definition (pi's
|
|
20
|
+
* own or condensedTools'), return the sandbox-aware version — schema gains
|
|
21
|
+
* dangerouslyDisableSandbox, execute wraps via the manager when the decision
|
|
22
|
+
* says so, and EPERM output is annotated. Identity when the sandbox is
|
|
23
|
+
* disabled at load time (default-off sessions stay byte-identical).
|
|
24
|
+
*
|
|
25
|
+
* Exported so condensedTools consumes it through its wrapBash seam — the
|
|
26
|
+
* M4 e2e lesson: condensed registers bash LAST, so a competing registration
|
|
27
|
+
* here would be overwritten and the sandbox would silently never reach
|
|
28
|
+
* model-driven tool calls.
|
|
29
|
+
*/
|
|
30
|
+
export declare function makeBashComposition(manager: YagniSandboxManager, settings: () => SandboxSettings, cwd: string): (def: ToolDefinition) => ToolDefinition;
|
|
31
|
+
export interface SandboxSessionHandle {
|
|
32
|
+
manager: YagniSandboxManager;
|
|
33
|
+
settings: () => SandboxSettings;
|
|
34
|
+
/** Latest permission rules snapshot (updated by the gate on load). */
|
|
35
|
+
setRules: (rules: readonly PermissionRule[]) => void;
|
|
36
|
+
/** Session-scoped network grants (memory-only, agent-invisible). */
|
|
37
|
+
grantDomainSession: (domain: string) => void;
|
|
38
|
+
sessionDomains: () => readonly string[];
|
|
39
|
+
/** The bash-definition composition (identity when disabled). condensedTools
|
|
40
|
+
* consumes this via its wrapBash seam; index.ts passes it through. */
|
|
41
|
+
composeBash: (def: ToolDefinition) => ToolDefinition;
|
|
42
|
+
/**
|
|
43
|
+
* Register the sandbox-wrapped bash directly on pi (the fallback path when
|
|
44
|
+
* condensedTools is NOT active: eval mode, classic rows, desktop). No-op
|
|
45
|
+
* when the sandbox is disabled. index.ts calls this in the else-branch so
|
|
46
|
+
* the two bash-defining sites never double-register.
|
|
47
|
+
*/
|
|
48
|
+
registerOwnBash: () => void;
|
|
49
|
+
}
|
|
50
|
+
export interface RegisterSandboxOptions {
|
|
51
|
+
cwd: string;
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
53
|
+
userHome?: string;
|
|
54
|
+
stateHomeOverride?: string | null;
|
|
55
|
+
projectRoot?: string | null;
|
|
56
|
+
rgPath?: string;
|
|
57
|
+
/** Injectable manager factory (tests simulate init failure / disabled
|
|
58
|
+
* platforms without touching the real srt singleton). */
|
|
59
|
+
makeManager?: (opts: {
|
|
60
|
+
cwd: string;
|
|
61
|
+
env?: NodeJS.ProcessEnv;
|
|
62
|
+
userHome?: string;
|
|
63
|
+
stateHomeOverride?: string | null;
|
|
64
|
+
projectRoot?: string | null;
|
|
65
|
+
rgPath?: string;
|
|
66
|
+
}) => Pick<YagniSandboxManager, "initialized" | "initialize" | "reset" | "refreshConfig" | "setAskHandler" | "wrapWithSandbox" | "cleanupAfterCommand" | "annotateStderrWithSandboxFailures"> & YagniSandboxManager;
|
|
67
|
+
/** Whether interactive prompts are possible (TUI/RPC). */
|
|
68
|
+
hasUI: (ctx: ExtensionContext) => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* When true, the sandbox registers its OWN wrapped bash tool at extension
|
|
71
|
+
* load (the fallback path — index.ts decides and calls registerOwnBash
|
|
72
|
+
* explicitly when condensedTools is inactive). Default false: the common
|
|
73
|
+
* path composes through condensedTools' wrapBash seam instead, because
|
|
74
|
+
* condensed registers last and would otherwise overwrite a competing
|
|
75
|
+
* registration (the duplicate-name 400 + silent-unwrap lesson from M4).
|
|
76
|
+
*/
|
|
77
|
+
registerOwnBash?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Register the sandbox surfaces on the ExtensionAPI. Returns the session
|
|
81
|
+
* handle (manager + accessors) or null when the platform can never run the
|
|
82
|
+
* sandbox (no-op registration, stock bash stays).
|
|
83
|
+
*/
|
|
84
|
+
export declare function registerSandbox(pi: ExtensionAPI, opts: RegisterSandboxOptions): SandboxSessionHandle | null;
|
|
85
|
+
//# sourceMappingURL=session.d.ts.map
|