tau-coding-agent 0.1.6 → 0.2.0

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 (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +17 -2
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +41 -38
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -0,0 +1,605 @@
1
+ import type { SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ import {
5
+ cloneRuntimeConfig,
6
+ escapeSlashCommandArg,
7
+ mutateStringList,
8
+ type ListOp,
9
+ type PromptMode,
10
+ type SandboxConfigPath,
11
+ } from "./config.js";
12
+ import {
13
+ describeSandboxRuntimeState,
14
+ formatSandboxEventTimestamp,
15
+ getSandboxRunMode,
16
+ notify,
17
+ type SandboxEvent,
18
+ type SandboxRuntime,
19
+ type SandboxState,
20
+ } from "./runtime.js";
21
+ import type { FilesystemList } from "./permissions/filesystem.js";
22
+ import { isValidMachLookupRule, mutateMachLookupAllowList } from "./permissions/mach-lookup.js";
23
+
24
+ const IS_MACOS = process.platform === "darwin";
25
+
26
+ type NetworkList = "allow" | "deny";
27
+
28
+ function showHelp(ctx: ExtensionContext): void {
29
+ const lines = [
30
+ "Usage:",
31
+ " /sandbox enable|on",
32
+ " /sandbox disable|off",
33
+ " /sandbox show",
34
+ " /sandbox doctor",
35
+ " /sandbox mode <interactive|non-interactive>",
36
+ " /sandbox network <allow|deny> <add|remove> <domain>",
37
+ ...(IS_MACOS
38
+ ? [
39
+ " /sandbox mach-lookup <add|remove> <service>",
40
+ " Service rules support one trailing *; use * for all services.",
41
+ ]
42
+ : []),
43
+ " /sandbox filesystem <deny-read|allow-write|deny-write> <add|remove> <path>",
44
+ "",
45
+ "Startup flags:",
46
+ " --no-sandbox",
47
+ " --sandbox-config <path>",
48
+ ];
49
+ notify(ctx, lines.join("\n"), "info");
50
+ }
51
+
52
+ function parseCommandArgs(args?: string): string[] {
53
+ if (!args?.trim()) return [];
54
+
55
+ const input = args.trim();
56
+ const tokens: string[] = [];
57
+ const tokenPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'|((?:\\.|[^\s])+)/g;
58
+
59
+ for (const match of input.matchAll(tokenPattern)) {
60
+ if (match[1] !== undefined) {
61
+ tokens.push(match[1].replace(/\\(.)/g, "$1"));
62
+ } else if (match[2] !== undefined) {
63
+ tokens.push(match[2]);
64
+ } else if (match[3] !== undefined) {
65
+ tokens.push(match[3].replace(/\\(.)/g, "$1"));
66
+ }
67
+ }
68
+
69
+ return tokens;
70
+ }
71
+
72
+ function normalizeSubcommand(token?: string): string | undefined {
73
+ switch (token?.toLowerCase()) {
74
+ case "on":
75
+ return "enable";
76
+ case "off":
77
+ return "disable";
78
+ default:
79
+ return token?.toLowerCase();
80
+ }
81
+ }
82
+
83
+ type CommandCompletionOption = {
84
+ value: string;
85
+ label?: string;
86
+ description?: string;
87
+ };
88
+
89
+ const SANDBOX_TOP_LEVEL_COMPLETIONS: CommandCompletionOption[] = [
90
+ { value: "enable", label: "enable" },
91
+ { value: "on", label: "on" },
92
+ { value: "disable", label: "disable" },
93
+ { value: "off", label: "off" },
94
+ { value: "show", label: "show" },
95
+ { value: "doctor", label: "doctor" },
96
+ { value: "mode ", label: "mode" },
97
+ { value: "network ", label: "network" },
98
+ ...(IS_MACOS ? [{ value: "mach-lookup ", label: "mach-lookup" }] : []),
99
+ { value: "filesystem ", label: "filesystem" },
100
+ { value: "help", label: "help" },
101
+ ];
102
+
103
+ const SANDBOX_MODE_COMPLETIONS: CommandCompletionOption[] = [
104
+ { value: "interactive", label: "interactive" },
105
+ { value: "non-interactive", label: "non-interactive" },
106
+ ];
107
+
108
+ const SANDBOX_NETWORK_LIST_COMPLETIONS: CommandCompletionOption[] = [
109
+ { value: "allow ", label: "allow" },
110
+ { value: "deny ", label: "deny" },
111
+ ];
112
+
113
+ const SANDBOX_FILESYSTEM_LIST_COMPLETIONS: CommandCompletionOption[] = [
114
+ { value: "deny-read ", label: "deny-read" },
115
+ { value: "allow-write ", label: "allow-write" },
116
+ { value: "deny-write ", label: "deny-write" },
117
+ ];
118
+
119
+ const SANDBOX_LIST_OPERATION_COMPLETIONS: CommandCompletionOption[] = [
120
+ { value: "add ", label: "add" },
121
+ { value: "remove ", label: "remove" },
122
+ ];
123
+
124
+ function normalizeCompletionFilter(value: string): string {
125
+ return value.trim().replace(/^['"]/, "").toLowerCase();
126
+ }
127
+
128
+ function getCommandCompletions(
129
+ base: string,
130
+ partial: string,
131
+ options: CommandCompletionOption[],
132
+ ): Array<{ value: string; label: string; description?: string }> | null {
133
+ const normalizedPartial = normalizeCompletionFilter(partial);
134
+ const matches = options.filter((option) => {
135
+ const label = option.label ?? option.value.trimEnd();
136
+ return label.toLowerCase().startsWith(normalizedPartial);
137
+ });
138
+ if (matches.length === 0) return null;
139
+
140
+ return matches.map((option) => ({
141
+ value: `${base}${option.value}`,
142
+ label: option.label ?? option.value.trimEnd(),
143
+ ...(option.description ? { description: option.description } : {}),
144
+ }));
145
+ }
146
+
147
+ function getStringValueCompletions(
148
+ base: string,
149
+ partial: string,
150
+ values: string[],
151
+ ): Array<{ value: string; label: string }> | null {
152
+ const normalizedPartial = normalizeCompletionFilter(partial);
153
+ const matches = Array.from(new Set(values)).filter((value) =>
154
+ value.toLowerCase().startsWith(normalizedPartial),
155
+ );
156
+ if (matches.length === 0) return null;
157
+
158
+ return matches.map((value) => ({
159
+ value: `${base}${escapeSlashCommandArg(value)}`,
160
+ label: value,
161
+ }));
162
+ }
163
+
164
+ function getMachLookupArgumentCompletions(options: {
165
+ tokens: string[];
166
+ endsWithSpace: boolean;
167
+ runtimeConfig: SandboxRuntimeConfig | null;
168
+ }): Array<{ value: string; label: string; description?: string }> | null {
169
+ const { tokens, endsWithSpace, runtimeConfig } = options;
170
+
171
+ if (tokens.length === 1 && endsWithSpace) {
172
+ return getCommandCompletions("mach-lookup ", "", SANDBOX_LIST_OPERATION_COMPLETIONS);
173
+ }
174
+ if (tokens.length === 2 && !endsWithSpace) {
175
+ return getCommandCompletions(
176
+ "mach-lookup ",
177
+ tokens[1] ?? "",
178
+ SANDBOX_LIST_OPERATION_COMPLETIONS,
179
+ );
180
+ }
181
+
182
+ if (tokens[1]?.toLowerCase() !== "remove") return null;
183
+
184
+ const values = runtimeConfig?.network.allowMachLookup ?? [];
185
+ const valueBase = "mach-lookup remove ";
186
+ if (tokens.length === 2 && endsWithSpace) {
187
+ return getStringValueCompletions(valueBase, "", values);
188
+ }
189
+ if (tokens.length === 3 && !endsWithSpace) {
190
+ return getStringValueCompletions(valueBase, tokens[2] ?? "", values);
191
+ }
192
+ return null;
193
+ }
194
+
195
+ function getSandboxArgumentCompletions(
196
+ prefix: string,
197
+ runtimeConfig: SandboxRuntimeConfig | null,
198
+ ): Array<{ value: string; label: string; description?: string }> | null {
199
+ const endsWithSpace = /\s$/.test(prefix);
200
+ const tokens = parseCommandArgs(prefix);
201
+
202
+ if (tokens.length === 0) {
203
+ return getCommandCompletions("", "", SANDBOX_TOP_LEVEL_COMPLETIONS);
204
+ }
205
+
206
+ if (tokens.length === 1 && !endsWithSpace) {
207
+ return getCommandCompletions("", tokens[0] ?? "", SANDBOX_TOP_LEVEL_COMPLETIONS);
208
+ }
209
+
210
+ const subcommand = normalizeSubcommand(tokens[0]);
211
+ if (!subcommand) return null;
212
+
213
+ if (subcommand === "mode") {
214
+ if (tokens.length === 1 && endsWithSpace) {
215
+ return getCommandCompletions("mode ", "", SANDBOX_MODE_COMPLETIONS);
216
+ }
217
+ if (tokens.length === 2 && !endsWithSpace) {
218
+ return getCommandCompletions("mode ", tokens[1] ?? "", SANDBOX_MODE_COMPLETIONS);
219
+ }
220
+ return null;
221
+ }
222
+
223
+ if (subcommand === "network") {
224
+ if (tokens.length === 1 && endsWithSpace) {
225
+ return getCommandCompletions("network ", "", SANDBOX_NETWORK_LIST_COMPLETIONS);
226
+ }
227
+ if (tokens.length === 2 && !endsWithSpace) {
228
+ return getCommandCompletions("network ", tokens[1] ?? "", SANDBOX_NETWORK_LIST_COMPLETIONS);
229
+ }
230
+
231
+ const list = tokens[1]?.toLowerCase();
232
+ if (list !== "allow" && list !== "deny") return null;
233
+
234
+ const listBase = `network ${list} `;
235
+ if (tokens.length === 2 && endsWithSpace) {
236
+ return getCommandCompletions(listBase, "", SANDBOX_LIST_OPERATION_COMPLETIONS);
237
+ }
238
+ if (tokens.length === 3 && !endsWithSpace) {
239
+ return getCommandCompletions(listBase, tokens[2] ?? "", SANDBOX_LIST_OPERATION_COMPLETIONS);
240
+ }
241
+
242
+ if (tokens[2]?.toLowerCase() !== "remove") return null;
243
+
244
+ const values =
245
+ list === "allow"
246
+ ? (runtimeConfig?.network.allowedDomains ?? [])
247
+ : (runtimeConfig?.network.deniedDomains ?? []);
248
+ const valueBase = `${listBase}remove `;
249
+ if (tokens.length === 3 && endsWithSpace) {
250
+ return getStringValueCompletions(valueBase, "", values);
251
+ }
252
+ if (tokens.length === 4 && !endsWithSpace) {
253
+ return getStringValueCompletions(valueBase, tokens[3] ?? "", values);
254
+ }
255
+ return null;
256
+ }
257
+
258
+ if (subcommand === "mach-lookup" && IS_MACOS) {
259
+ return getMachLookupArgumentCompletions({ tokens, endsWithSpace, runtimeConfig });
260
+ }
261
+
262
+ if (subcommand === "filesystem") {
263
+ if (tokens.length === 1 && endsWithSpace) {
264
+ return getCommandCompletions("filesystem ", "", SANDBOX_FILESYSTEM_LIST_COMPLETIONS);
265
+ }
266
+ if (tokens.length === 2 && !endsWithSpace) {
267
+ return getCommandCompletions(
268
+ "filesystem ",
269
+ tokens[1] ?? "",
270
+ SANDBOX_FILESYSTEM_LIST_COMPLETIONS,
271
+ );
272
+ }
273
+
274
+ const list = tokens[1]?.toLowerCase() as FilesystemList | undefined;
275
+ if (list !== "deny-read" && list !== "allow-write" && list !== "deny-write") {
276
+ return null;
277
+ }
278
+
279
+ const listBase = `filesystem ${list} `;
280
+ if (tokens.length === 2 && endsWithSpace) {
281
+ return getCommandCompletions(listBase, "", SANDBOX_LIST_OPERATION_COMPLETIONS);
282
+ }
283
+ if (tokens.length === 3 && !endsWithSpace) {
284
+ return getCommandCompletions(listBase, tokens[2] ?? "", SANDBOX_LIST_OPERATION_COMPLETIONS);
285
+ }
286
+
287
+ if (tokens[2]?.toLowerCase() !== "remove") return null;
288
+
289
+ const values =
290
+ list === "deny-read"
291
+ ? (runtimeConfig?.filesystem.denyRead ?? [])
292
+ : list === "allow-write"
293
+ ? (runtimeConfig?.filesystem.allowWrite ?? [])
294
+ : (runtimeConfig?.filesystem.denyWrite ?? []);
295
+ const valueBase = `${listBase}remove `;
296
+ if (tokens.length === 3 && endsWithSpace) {
297
+ return getStringValueCompletions(valueBase, "", values);
298
+ }
299
+ if (tokens.length === 4 && !endsWithSpace) {
300
+ return getStringValueCompletions(valueBase, tokens[3] ?? "", values);
301
+ }
302
+ return null;
303
+ }
304
+
305
+ return null;
306
+ }
307
+
308
+ function requireRuntimeConfig(
309
+ ctx: ExtensionContext,
310
+ runtime: SandboxRuntime,
311
+ ): SandboxRuntimeConfig | null {
312
+ const runtimeConfig = runtime.getRuntimeConfig();
313
+ if (!runtimeConfig) {
314
+ notify(ctx, "Sandbox is not initialized", "info");
315
+ return null;
316
+ }
317
+ return runtimeConfig;
318
+ }
319
+
320
+ function renderSandboxDoctorReport(options: {
321
+ state: SandboxState;
322
+ promptMode: PromptMode;
323
+ configPaths: SandboxConfigPath[];
324
+ events: SandboxEvent[];
325
+ }): string {
326
+ const { state, promptMode, configPaths, events } = options;
327
+ const lines = ["Sandbox doctor", `- Runtime: ${describeSandboxRuntimeState(state, promptMode)}`];
328
+
329
+ if (configPaths.length === 0) {
330
+ lines.push("- Config paths: (none loaded)");
331
+ } else {
332
+ lines.push("- Config paths:");
333
+ for (const configPath of configPaths) {
334
+ const status =
335
+ configPath.status === "loaded"
336
+ ? "loaded"
337
+ : configPath.status === "skipped-untrusted"
338
+ ? "skipped (project not trusted)"
339
+ : "parse error";
340
+ lines.push(` - ${configPath.label}: ${configPath.path} (${status})`);
341
+ }
342
+ }
343
+
344
+ lines.push(`- Events: ${events.length}`);
345
+
346
+ if (events.length === 0) {
347
+ lines.push("", "- No sandbox events recorded in this session.");
348
+ return lines.join("\n");
349
+ }
350
+
351
+ for (const event of [...events].reverse()) {
352
+ lines.push(
353
+ "",
354
+ `- [${formatSandboxEventTimestamp(event.timestamp)}] [${event.kind}] [${event.outcome}] ${event.reason}`,
355
+ ...(event.target ? [` Target: ${event.target}`] : []),
356
+ ...(event.command ? [` Command: ${event.command}`] : []),
357
+ ` Summary: ${event.summary}`,
358
+ ...(event.suggestedCommand
359
+ ? [" Suggested session fix:", ` ${event.suggestedCommand}`]
360
+ : []),
361
+ );
362
+ }
363
+
364
+ return lines.join("\n");
365
+ }
366
+
367
+ export function registerSandboxCommand(pi: ExtensionAPI, runtime: SandboxRuntime): void {
368
+ pi.registerCommand("sandbox", {
369
+ description: "Manage sandbox runtime overrides",
370
+ getArgumentCompletions: (prefix) =>
371
+ getSandboxArgumentCompletions(prefix, runtime.getRuntimeConfig()),
372
+ handler: async (args, ctx) => {
373
+ const tokens = parseCommandArgs(args);
374
+ const subcommand = normalizeSubcommand(tokens[0]);
375
+
376
+ if (!subcommand || subcommand === "help") {
377
+ showHelp(ctx);
378
+ return;
379
+ }
380
+
381
+ if (subcommand === "doctor") {
382
+ if (tokens.length > 1) {
383
+ notify(ctx, "Usage: /sandbox doctor", "warning");
384
+ return;
385
+ }
386
+
387
+ notify(
388
+ ctx,
389
+ renderSandboxDoctorReport({
390
+ state: runtime.state,
391
+ promptMode: runtime.promptMode,
392
+ configPaths: runtime.configPaths,
393
+ events: runtime.events,
394
+ }),
395
+ "info",
396
+ );
397
+ return;
398
+ }
399
+
400
+ if (subcommand === "enable") {
401
+ if (tokens.length > 1) {
402
+ notify(ctx, "Usage: /sandbox enable|on", "warning");
403
+ return;
404
+ }
405
+
406
+ await runtime.enable(ctx);
407
+ return;
408
+ }
409
+
410
+ if (subcommand === "disable") {
411
+ if (tokens.length > 1) {
412
+ notify(ctx, "Usage: /sandbox disable|off", "warning");
413
+ return;
414
+ }
415
+
416
+ await runtime.disable(ctx);
417
+ return;
418
+ }
419
+
420
+ if (subcommand === "show") {
421
+ if (tokens.length > 1) {
422
+ notify(ctx, "Usage: /sandbox show", "warning");
423
+ return;
424
+ }
425
+
426
+ if (runtime.state.status !== "active") {
427
+ notify(ctx, `Sandbox is disabled (mode: ${getSandboxRunMode(runtime.state)})`, "info");
428
+ return;
429
+ }
430
+
431
+ const runtimeConfig = runtime.state.runtimeConfig;
432
+ const lines = [
433
+ "Sandbox Configuration (session):",
434
+ ` State: enabled`,
435
+ ` Mode: ${runtime.promptMode}`,
436
+ ` Runtime state: ${getSandboxRunMode(runtime.state)}`,
437
+ "",
438
+ " Network:",
439
+ ` Allowed: ${runtimeConfig.network.allowedDomains.join(", ") || "(none)"}`,
440
+ ` Denied: ${runtimeConfig.network.deniedDomains.join(", ") || "(none)"}`,
441
+ ` allowLocalBinding: ${runtimeConfig.network.allowLocalBinding ? "true" : "false"}`,
442
+ ` allowAllUnixSockets: ${runtimeConfig.network.allowAllUnixSockets ? "true" : "false"}`,
443
+ ` allowUnixSockets: ${runtimeConfig.network.allowUnixSockets?.join(", ") || "(none)"}`,
444
+ ...(IS_MACOS
445
+ ? [
446
+ "",
447
+ " macOS service lookup (mach-lookup):",
448
+ ` Allowed: ${runtimeConfig.network.allowMachLookup?.join(", ") || "(none)"}`,
449
+ ]
450
+ : []),
451
+ "",
452
+ " Filesystem:",
453
+ ` Deny Read: ${runtimeConfig.filesystem.denyRead.join(", ") || "(none)"}`,
454
+ ` Allow Read: ${runtimeConfig.filesystem.allowRead?.join(", ") || "(none)"}`,
455
+ ` Allow Write: ${runtimeConfig.filesystem.allowWrite.join(", ") || "(none)"}`,
456
+ ` Deny Write: ${runtimeConfig.filesystem.denyWrite.join(", ") || "(none)"}`,
457
+ ` allowTempDirs: ${runtime.config?.filesystem.allowTempDirs ? "true" : "false"}`,
458
+ ` allowGitConfig: ${runtimeConfig.filesystem.allowGitConfig ? "true" : "false"}`,
459
+ ` allowGitCommonDir: ${runtime.config?.filesystem.allowGitCommonDir ? "true" : "false"}`,
460
+ "",
461
+ " Advanced:",
462
+ ` ignoreViolations: ${runtimeConfig.ignoreViolations ? "configured" : "(none)"}`,
463
+ ` enableWeakerNestedSandbox: ${runtimeConfig.enableWeakerNestedSandbox ? "true" : "false"}`,
464
+ ` enableWeakerNetworkIsolation: ${runtimeConfig.enableWeakerNetworkIsolation ? "true" : "false"}`,
465
+ ];
466
+
467
+ notify(ctx, lines.join("\n"), "info");
468
+ return;
469
+ }
470
+
471
+ if (subcommand === "mode") {
472
+ if (tokens.length !== 2) {
473
+ notify(ctx, "Usage: /sandbox mode <interactive|non-interactive>", "warning");
474
+ return;
475
+ }
476
+
477
+ const modeToken = tokens[1].toLowerCase();
478
+ if (modeToken !== "interactive" && modeToken !== "non-interactive") {
479
+ notify(ctx, "Usage: /sandbox mode <interactive|non-interactive>", "warning");
480
+ return;
481
+ }
482
+
483
+ runtime.setPromptMode(ctx, modeToken);
484
+ notify(ctx, `Sandbox mode set to ${runtime.promptMode}`, "info");
485
+ return;
486
+ }
487
+
488
+ if (subcommand === "network") {
489
+ const runtimeConfig = requireRuntimeConfig(ctx, runtime);
490
+ if (!runtimeConfig) return;
491
+
492
+ const list = tokens[1]?.toLowerCase() as NetworkList | undefined;
493
+ const op = tokens[2]?.toLowerCase() as ListOp | undefined;
494
+ const domain = tokens[3]?.trim() ?? "";
495
+
496
+ if (
497
+ (list !== "allow" && list !== "deny") ||
498
+ (op !== "add" && op !== "remove") ||
499
+ tokens.length !== 4 ||
500
+ !domain ||
501
+ /\s/.test(domain)
502
+ ) {
503
+ notify(ctx, "Usage: /sandbox network <allow|deny> <add|remove> <domain>", "warning");
504
+ return;
505
+ }
506
+
507
+ const nextConfig = cloneRuntimeConfig(runtimeConfig);
508
+ const values =
509
+ list === "allow" ? nextConfig.network.allowedDomains : nextConfig.network.deniedDomains;
510
+ const changed = mutateStringList(values, op, domain);
511
+ if (!changed) {
512
+ notify(
513
+ ctx,
514
+ `No change: network ${list} list already ${op === "add" ? "contains" : "omits"} ${domain}`,
515
+ );
516
+ return;
517
+ }
518
+
519
+ runtime.applyRuntimeConfigForSession(ctx, nextConfig);
520
+ notify(ctx, `Updated network ${list} list (${op}: ${domain})`, "info");
521
+ return;
522
+ }
523
+
524
+ if (subcommand === "mach-lookup") {
525
+ if (!IS_MACOS) {
526
+ notify(ctx, "Mach service lookup controls are only available on macOS.", "warning");
527
+ return;
528
+ }
529
+
530
+ const runtimeConfig = requireRuntimeConfig(ctx, runtime);
531
+ if (!runtimeConfig) return;
532
+
533
+ const op = tokens[1]?.toLowerCase() as ListOp | undefined;
534
+ const service = tokens[2]?.trim() ?? "";
535
+
536
+ if (
537
+ (op !== "add" && op !== "remove") ||
538
+ tokens.length !== 3 ||
539
+ !isValidMachLookupRule(service)
540
+ ) {
541
+ notify(ctx, "Usage: /sandbox mach-lookup <add|remove> <service>", "warning");
542
+ return;
543
+ }
544
+
545
+ const nextConfig = cloneRuntimeConfig(runtimeConfig);
546
+ const changed = mutateMachLookupAllowList(nextConfig, op, service);
547
+ if (!changed) {
548
+ notify(
549
+ ctx,
550
+ `No change: mach-lookup allow list already ${op === "add" ? "contains" : "omits"} ${service}`,
551
+ );
552
+ return;
553
+ }
554
+
555
+ runtime.applyRuntimeConfigForSession(ctx, nextConfig);
556
+ notify(ctx, `Updated mach-lookup allow list (${op}: ${service})`, "info");
557
+ return;
558
+ }
559
+
560
+ if (subcommand === "filesystem") {
561
+ const runtimeConfig = requireRuntimeConfig(ctx, runtime);
562
+ if (!runtimeConfig) return;
563
+
564
+ const list = tokens[1]?.toLowerCase() as FilesystemList | undefined;
565
+ const op = tokens[2]?.toLowerCase() as ListOp | undefined;
566
+ const targetPath = tokens.slice(3).join(" ").trim();
567
+
568
+ if (
569
+ (list !== "deny-read" && list !== "allow-write" && list !== "deny-write") ||
570
+ (op !== "add" && op !== "remove") ||
571
+ !targetPath
572
+ ) {
573
+ notify(
574
+ ctx,
575
+ "Usage: /sandbox filesystem <deny-read|allow-write|deny-write> <add|remove> <path>",
576
+ "warning",
577
+ );
578
+ return;
579
+ }
580
+
581
+ const nextConfig = cloneRuntimeConfig(runtimeConfig);
582
+ const values =
583
+ list === "deny-read"
584
+ ? nextConfig.filesystem.denyRead
585
+ : list === "allow-write"
586
+ ? nextConfig.filesystem.allowWrite
587
+ : nextConfig.filesystem.denyWrite;
588
+ const changed = mutateStringList(values, op, targetPath);
589
+ if (!changed) {
590
+ notify(
591
+ ctx,
592
+ `No change: filesystem ${list} list already ${op === "add" ? "contains" : "omits"} ${targetPath}`,
593
+ );
594
+ return;
595
+ }
596
+
597
+ runtime.applyRuntimeConfigForSession(ctx, nextConfig);
598
+ notify(ctx, `Updated filesystem ${list} list (${op}: ${targetPath})`, "info");
599
+ return;
600
+ }
601
+
602
+ notify(ctx, `Unknown subcommand: ${subcommand}. Use /sandbox for help`, "error");
603
+ },
604
+ });
605
+ }