@robota-sdk/agent-command 3.0.0-beta.79 → 3.0.0-beta.81

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 (200) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +76 -38
  3. package/dist/node/index.d.cts +946 -0
  4. package/dist/node/index.d.cts.map +1 -0
  5. package/dist/node/index.d.ts +564 -51
  6. package/dist/node/index.d.ts.map +1 -1
  7. package/dist/node/index.js +76 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +34 -16
  10. package/src/advisor/advisor-command-module.ts +47 -0
  11. package/src/advisor/advisor-command.test.ts +102 -0
  12. package/src/advisor/advisor-command.ts +55 -0
  13. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  14. package/src/advisor/index.ts +6 -0
  15. package/src/agent/__tests__/agent-command.test.ts +39 -8
  16. package/src/agent/agent-command-module.ts +14 -7
  17. package/src/agent/agent-command-parser.ts +1 -1
  18. package/src/agent/agent-command.ts +3 -3
  19. package/src/background/__tests__/background-command-module.test.ts +59 -52
  20. package/src/background/background-command-module.ts +2 -1
  21. package/src/background/background-command.ts +3 -3
  22. package/src/command-module-utils.ts +33 -0
  23. package/src/compact/__tests__/compact-command-module.test.ts +31 -23
  24. package/src/compact/compact-command-module.ts +10 -1
  25. package/src/compact/compact-command.ts +6 -3
  26. package/src/context/__tests__/context-command-module.test.ts +214 -63
  27. package/src/context/auto-compact-format.ts +25 -0
  28. package/src/context/context-breakdown.ts +295 -0
  29. package/src/context/context-command-module.ts +45 -14
  30. package/src/context/context-command.ts +34 -257
  31. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
  32. package/src/default/__tests__/default-command-modules.test.ts +59 -7
  33. package/src/default/__tests__/model-exposure.test.ts +241 -0
  34. package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
  35. package/src/default/default-command-modules.ts +130 -18
  36. package/src/default/index.ts +4 -1
  37. package/src/devices/__tests__/devices-command-module.test.ts +212 -0
  38. package/src/devices/devices-command-module.ts +224 -0
  39. package/src/devices/devices-command-port.ts +82 -0
  40. package/src/devices/index.ts +11 -0
  41. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  42. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  43. package/src/doctor/__tests__/doctor-fixture.ts +145 -0
  44. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  45. package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
  46. package/src/doctor/doctor-command-module.ts +132 -0
  47. package/src/doctor/doctor-extensions-probe.ts +299 -0
  48. package/src/doctor/doctor-node-deps.ts +89 -0
  49. package/src/doctor/doctor-redaction.ts +99 -0
  50. package/src/doctor/doctor-render.ts +70 -0
  51. package/src/doctor/doctor-repair.ts +136 -0
  52. package/src/doctor/doctor-runner.ts +122 -0
  53. package/src/doctor/doctor-settings-probe.ts +239 -0
  54. package/src/doctor/doctor-storage-probe.ts +187 -0
  55. package/src/doctor/doctor-types.ts +109 -0
  56. package/src/doctor/index.ts +32 -0
  57. package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
  58. package/src/editor/editor-command-module.ts +6 -5
  59. package/src/editor/editor-command.ts +10 -5
  60. package/src/effort/effort-command-module.ts +51 -0
  61. package/src/effort/effort-command.test.ts +140 -0
  62. package/src/effort/effort-command.ts +122 -0
  63. package/src/effort/index.ts +6 -0
  64. package/src/exit/__tests__/exit-command-module.test.ts +16 -9
  65. package/src/exit/exit-command-module.ts +2 -1
  66. package/src/exit/exit-command.ts +6 -6
  67. package/src/fork/__tests__/fork-command.test.ts +184 -0
  68. package/src/fork/fork-command-module.ts +61 -0
  69. package/src/fork/fork-command.ts +162 -0
  70. package/src/fork/index.ts +8 -0
  71. package/src/git/__tests__/fake-git-port.ts +28 -0
  72. package/src/git/__tests__/git-command-module.test.ts +103 -0
  73. package/src/git/__tests__/git-commit.test.ts +185 -0
  74. package/src/git/__tests__/git-diff.test.ts +125 -0
  75. package/src/git/__tests__/git-process.test.ts +221 -0
  76. package/src/git/__tests__/git-status.test.ts +117 -0
  77. package/src/git/git-command-module.ts +139 -0
  78. package/src/git/git-commit.ts +251 -0
  79. package/src/git/git-diff.ts +129 -0
  80. package/src/git/git-process.ts +169 -0
  81. package/src/git/git-status.ts +134 -0
  82. package/src/git/index.ts +17 -0
  83. package/src/goal/__tests__/goal-command.test.ts +9 -4
  84. package/src/goal/goal-command-module.ts +2 -1
  85. package/src/goal/goal-command.ts +6 -9
  86. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  87. package/src/handoff/handoff-command-module.ts +48 -0
  88. package/src/handoff/handoff-command.ts +187 -0
  89. package/src/handoff/index.ts +6 -0
  90. package/src/help/__tests__/help-command-module.test.ts +63 -40
  91. package/src/help/__tests__/help-command.test.ts +46 -37
  92. package/src/help/help-command-module.ts +2 -1
  93. package/src/help/help-command.ts +3 -3
  94. package/src/index.ts +124 -2
  95. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  96. package/src/keybindings/index.ts +6 -0
  97. package/src/keybindings/keybindings-command-module.ts +74 -0
  98. package/src/language/__tests__/language-command-module.test.ts +33 -28
  99. package/src/language/language-command-module.ts +2 -1
  100. package/src/language/language-command.ts +8 -6
  101. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  102. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  103. package/src/mcp-activation/index.ts +12 -0
  104. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  105. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  106. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  107. package/src/mcp-activation/mcp-model-status.ts +99 -0
  108. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  109. package/src/memory/memory-command-module.ts +18 -1
  110. package/src/memory/memory-command.ts +38 -38
  111. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  112. package/src/mode/mode-command-module.ts +2 -1
  113. package/src/mode/mode-command.ts +15 -7
  114. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  115. package/src/output-style/index.ts +6 -0
  116. package/src/output-style/output-style-command-module.ts +143 -0
  117. package/src/peers/__tests__/peers-command.test.ts +241 -0
  118. package/src/peers/index.ts +9 -0
  119. package/src/peers/peers-command-module.ts +47 -0
  120. package/src/peers/peers-command.ts +142 -0
  121. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  122. package/src/permissions/permissions-command-module.ts +2 -1
  123. package/src/permissions/permissions-command.ts +46 -4
  124. package/src/plan/__tests__/plan-command.test.ts +129 -0
  125. package/src/plan/index.ts +9 -0
  126. package/src/plan/plan-command-module.ts +49 -0
  127. package/src/plan/plan-command.ts +92 -0
  128. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  129. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  130. package/src/plugin/plugin-command-module.ts +3 -1
  131. package/src/plugin/plugin-command.ts +17 -16
  132. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  133. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  134. package/src/preset/preset-command-module.ts +26 -8
  135. package/src/preset/preset-command.ts +85 -27
  136. package/src/provider/__tests__/org-policy.test.ts +4 -5
  137. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  138. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  139. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  140. package/src/provider/provider-command-execution.ts +4 -4
  141. package/src/provider/provider-command-module.ts +2 -1
  142. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  143. package/src/provider/provider-command-profile-operations.ts +3 -3
  144. package/src/provider/provider-command-profile.ts +1 -1
  145. package/src/provider/provider-command-setup.ts +4 -4
  146. package/src/provider/provider-startup.ts +62 -35
  147. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  148. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  149. package/src/remote-control/index.ts +8 -0
  150. package/src/remote-control/remote-control-command-module.ts +61 -0
  151. package/src/remote-control/remote-control-command.ts +138 -0
  152. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  153. package/src/reset/reset-command-module.ts +2 -1
  154. package/src/reset/reset-command.ts +3 -4
  155. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  156. package/src/rewind/rewind-command-module.ts +2 -1
  157. package/src/rewind/rewind-command.ts +73 -6
  158. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  159. package/src/sandbox/index.ts +2 -0
  160. package/src/sandbox/sandbox-command-module.ts +56 -0
  161. package/src/sandbox/sandbox-command.ts +94 -0
  162. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  163. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  164. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  165. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  166. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  167. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  168. package/src/schedule/index.ts +3 -0
  169. package/src/schedule/loop-command.ts +333 -0
  170. package/src/schedule/loop-jitter.ts +41 -0
  171. package/src/schedule/schedule-command-module.ts +92 -14
  172. package/src/schedule/schedule-command.ts +98 -9
  173. package/src/schedule/schedule-spec-parser.ts +5 -1
  174. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  175. package/src/session/__tests__/session-command-module.test.ts +210 -45
  176. package/src/session/session-command-module.ts +68 -37
  177. package/src/session/session-command.ts +147 -80
  178. package/src/settings/settings-command-module.ts +3 -2
  179. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  180. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  181. package/src/shell/resolve-shell.ts +2 -2
  182. package/src/shell/shell-command-module.ts +6 -5
  183. package/src/shell/shell-command.ts +9 -5
  184. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  185. package/src/skills/index.ts +1 -0
  186. package/src/skills/skills-command-module.ts +22 -5
  187. package/src/skills/skills-command.ts +14 -13
  188. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  189. package/src/statusline/statusline-command-module.ts +2 -1
  190. package/src/statusline/statusline-command.ts +4 -4
  191. package/src/theme/__tests__/theme-command.test.ts +204 -0
  192. package/src/theme/index.ts +12 -0
  193. package/src/theme/theme-command-module.ts +211 -0
  194. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  195. package/src/user-local/user-local-command-constants.ts +1 -1
  196. package/src/user-local/user-local-command-module.ts +6 -5
  197. package/src/user-local/user-local-command.ts +12 -6
  198. package/src/user-local/user-local-memory-command.ts +17 -7
  199. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  200. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
@@ -3,15 +3,18 @@ import {
3
3
  listCommandEditCheckpoints,
4
4
  restoreCommandEditCheckpoint,
5
5
  rollbackCommandEditCheckpoint,
6
+ forkCommandEditCheckpoint,
7
+ switchCommandEditCheckpointBranch,
8
+ listCommandEditCheckpointBranches,
6
9
  } from '@robota-sdk/agent-framework';
7
10
 
8
11
  import type {
9
- ICommandHostContext,
12
+ ICommandHostCheckpoints,
10
13
  IEditCheckpointInspection,
11
14
  IEditCheckpointRestoreResult,
12
15
  IEditCheckpointSummary,
13
16
  } from '@robota-sdk/agent-framework';
14
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
17
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
15
18
 
16
19
  const SUBCOMMAND_INDEX = 0;
17
20
  const CHECKPOINT_ID_INDEX = 1;
@@ -125,7 +128,10 @@ function formatError(error: Error | string): ICommandResult {
125
128
  };
126
129
  }
127
130
 
128
- function inspect(context: ICommandHostContext, checkpointId: string | undefined): ICommandResult {
131
+ function inspect(
132
+ context: ICommandHostCheckpoints,
133
+ checkpointId: string | undefined,
134
+ ): ICommandResult {
129
135
  if (!checkpointId) return usage();
130
136
  try {
131
137
  return formatInspection(inspectCommandEditCheckpoint(context, checkpointId));
@@ -135,7 +141,7 @@ function inspect(context: ICommandHostContext, checkpointId: string | undefined)
135
141
  }
136
142
 
137
143
  async function restore(
138
- context: ICommandHostContext,
144
+ context: ICommandHostCheckpoints,
139
145
  checkpointId: string | undefined,
140
146
  ): Promise<ICommandResult> {
141
147
  if (!checkpointId) return usage();
@@ -147,7 +153,7 @@ async function restore(
147
153
  }
148
154
 
149
155
  async function rollback(
150
- context: ICommandHostContext,
156
+ context: ICommandHostCheckpoints,
151
157
  checkpointId: string | undefined,
152
158
  ): Promise<ICommandResult> {
153
159
  if (!checkpointId) return usage();
@@ -158,8 +164,58 @@ async function rollback(
158
164
  }
159
165
  }
160
166
 
167
+ // SELFHOST-007: branching time-travel handlers.
168
+ async function fork(
169
+ context: ICommandHostCheckpoints,
170
+ checkpointId: string | undefined,
171
+ ): Promise<ICommandResult> {
172
+ if (!checkpointId) return usage();
173
+ try {
174
+ const result = await forkCommandEditCheckpoint(context, checkpointId);
175
+ return {
176
+ message: [
177
+ `Forked a new branch from ${result.target.id} (the previous future is preserved).`,
178
+ `Restored files: ${result.restoredFileCount}`,
179
+ ].join('\n'),
180
+ success: true,
181
+ data: { target: result.target, restoredFileCount: result.restoredFileCount },
182
+ };
183
+ } catch (error) {
184
+ return formatError(error instanceof Error ? error : String(error));
185
+ }
186
+ }
187
+
188
+ function switchBranch(
189
+ context: ICommandHostCheckpoints,
190
+ checkpointId: string | undefined,
191
+ ): ICommandResult {
192
+ if (!checkpointId) return usage();
193
+ try {
194
+ switchCommandEditCheckpointBranch(context, checkpointId);
195
+ return { message: `Switched to checkpoint branch ${checkpointId}.`, success: true };
196
+ } catch (error) {
197
+ return formatError(error instanceof Error ? error : String(error));
198
+ }
199
+ }
200
+
201
+ function branches(context: ICommandHostCheckpoints): ICommandResult {
202
+ try {
203
+ const tips = listCommandEditCheckpointBranches(context);
204
+ return {
205
+ message:
206
+ tips.length === 0
207
+ ? 'No checkpoint branches yet.'
208
+ : `Branch tips:\n${tips.map((id) => `- ${id}`).join('\n')}`,
209
+ success: true,
210
+ data: { branches: tips },
211
+ };
212
+ } catch (error) {
213
+ return formatError(error instanceof Error ? error : String(error));
214
+ }
215
+ }
216
+
161
217
  export async function executeRewindCommand(
162
- context: ICommandHostContext,
218
+ context: ICommandHostCheckpoints,
163
219
  rawArgs: string,
164
220
  ): Promise<ICommandResult> {
165
221
  const args = rawArgs.trim().split(/\s+/).filter(Boolean);
@@ -181,5 +237,16 @@ export async function executeRewindCommand(
181
237
  return rollback(context, args[CHECKPOINT_ID_INDEX]);
182
238
  }
183
239
 
240
+ // SELFHOST-007: branching time-travel subcommands.
241
+ if (subcommand === 'fork') {
242
+ return fork(context, args[CHECKPOINT_ID_INDEX]);
243
+ }
244
+ if (subcommand === 'switch') {
245
+ return switchBranch(context, args[CHECKPOINT_ID_INDEX]);
246
+ }
247
+ if (subcommand === 'branches') {
248
+ return branches(context);
249
+ }
250
+
184
251
  return usage();
185
252
  }
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { executeSandboxCommand } from '../sandbox-command.js';
4
+
5
+ import type {
6
+ ICommandSandboxAdapter,
7
+ ICommandSandboxStatus,
8
+ TSandboxCommandMode,
9
+ } from '@robota-sdk/agent-framework';
10
+
11
+ /** Issue #3082 — `/sandbox` shows and changes how shell commands are confined. */
12
+ function contextWith(
13
+ status: Partial<ICommandSandboxStatus>,
14
+ answer?: string,
15
+ ): { context: Parameters<typeof executeSandboxCommand>[0]; setMode: ReturnType<typeof vi.fn> } {
16
+ let current: ICommandSandboxStatus = {
17
+ mode: 'off',
18
+ backend: 'bubblewrap',
19
+ network: false,
20
+ excludedCommands: [],
21
+ ...status,
22
+ };
23
+ const setMode = vi.fn((mode: TSandboxCommandMode) => {
24
+ current = { ...current, mode };
25
+ });
26
+ const adapter: ICommandSandboxAdapter = { status: () => current, setMode };
27
+ const ui =
28
+ answer === undefined
29
+ ? undefined
30
+ : { ask: vi.fn().mockResolvedValue({ type: 'answer', values: [answer] }) };
31
+ return {
32
+ context: {
33
+ getCommandHostAdapters: () => ({ sandbox: adapter }),
34
+ getUserInteraction: () => ui,
35
+ } as never,
36
+ setMode,
37
+ };
38
+ }
39
+
40
+ describe('/sandbox', () => {
41
+ it('shows the current state when no mode is given and nobody is asked', async () => {
42
+ const { context } = contextWith({ mode: 'auto-allow', excludedCommands: ['docker'] });
43
+ const result = await executeSandboxCommand(context, '');
44
+ expect(result.message).toContain('Sandbox: auto-allow (bubblewrap)');
45
+ expect(result.message).toContain('Network: blocked.');
46
+ expect(result.message).toContain('Run unconfined: docker.');
47
+ });
48
+
49
+ it('sets a mode named on the command line', async () => {
50
+ const { context, setMode } = contextWith({});
51
+ const result = await executeSandboxCommand(context, 'regular');
52
+ expect(setMode).toHaveBeenCalledWith('regular');
53
+ expect(result.message).toContain('Sandbox mode set to: regular');
54
+ });
55
+
56
+ it('asks for a mode through the picker', async () => {
57
+ const { context, setMode } = contextWith({}, 'auto-allow');
58
+ await executeSandboxCommand(context, '');
59
+ expect(setMode).toHaveBeenCalledWith('auto-allow');
60
+ });
61
+
62
+ it('says what is missing when the backend cannot run', async () => {
63
+ const { context } = contextWith({ mode: 'auto-allow', unavailable: 'missing bubblewrap' });
64
+ const result = await executeSandboxCommand(context, '');
65
+ expect(result.message).toContain(
66
+ 'Cannot run here: missing bubblewrap. Commands run unconfined.',
67
+ );
68
+ });
69
+
70
+ it('refuses an unknown mode and a host without a sandbox', async () => {
71
+ const { context, setMode } = contextWith({});
72
+ expect((await executeSandboxCommand(context, 'loose')).success).toBe(false);
73
+ expect(setMode).not.toHaveBeenCalled();
74
+ const bare = {
75
+ getCommandHostAdapters: () => ({}),
76
+ getUserInteraction: () => undefined,
77
+ } as never;
78
+ expect((await executeSandboxCommand(bare, '')).message).toBe('This host has no OS sandbox.');
79
+ });
80
+ });
@@ -0,0 +1,2 @@
1
+ export { createSandboxCommandModule, SandboxCommandSource } from './sandbox-command-module.js';
2
+ export { executeSandboxCommand, formatSandboxStatus, SANDBOX_MODES } from './sandbox-command.js';
@@ -0,0 +1,56 @@
1
+ import { executeSandboxCommand, SANDBOX_MODES } from './sandbox-command.js';
2
+
3
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
4
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
5
+
6
+ const DESCRIPTION = 'Show or change how shell commands are confined';
7
+ const ARGUMENT_HINT = SANDBOX_MODES.map((entry) => entry.mode).join(' | ');
8
+
9
+ export function createSandboxCommandEntry(): ICommand {
10
+ return {
11
+ name: 'sandbox',
12
+ displayName: 'Sandbox',
13
+ description: DESCRIPTION,
14
+ source: 'sandbox',
15
+ argumentHint: ARGUMENT_HINT,
16
+ subcommands: SANDBOX_MODES.map(({ mode, description }) => ({
17
+ name: mode,
18
+ description,
19
+ source: 'sandbox',
20
+ })),
21
+ // User-only: changes the sandbox boundary; permission widening is the user's.
22
+ modelInvocable: false,
23
+ };
24
+ }
25
+
26
+ function createSandboxSystemCommand(): ISystemCommand {
27
+ const entry = createSandboxCommandEntry();
28
+ return {
29
+ name: entry.name,
30
+ displayName: entry.displayName,
31
+ description: entry.description,
32
+ requiresPermission: false,
33
+ userInvocable: true,
34
+ modelInvocable: false,
35
+ argumentHint: entry.argumentHint,
36
+ subcommands: entry.subcommands,
37
+ lifecycle: 'inline',
38
+ execute: executeSandboxCommand,
39
+ };
40
+ }
41
+
42
+ export class SandboxCommandSource implements ICommandSource {
43
+ readonly name = 'sandbox';
44
+
45
+ getCommands(): ICommand[] {
46
+ return [createSandboxCommandEntry()];
47
+ }
48
+ }
49
+
50
+ export function createSandboxCommandModule(): ICommandModule {
51
+ return {
52
+ name: 'agent-command-sandbox',
53
+ commandSources: [new SandboxCommandSource()],
54
+ systemCommands: [createSandboxSystemCommand()],
55
+ };
56
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `/sandbox`: see how shell commands are confined and change it (issue #3082).
3
+ *
4
+ * The OS sandbox is live — the choice applies to the next command — and is saved in the user
5
+ * settings so the next session starts the same way.
6
+ */
7
+ import { selectAction } from '@robota-sdk/agent-core';
8
+
9
+ import type {
10
+ ICommandHostAdapterAccess,
11
+ ICommandHostUserInteraction,
12
+ ICommandSandboxStatus,
13
+ TSandboxCommandMode,
14
+ } from '@robota-sdk/agent-framework';
15
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
16
+
17
+ export const SANDBOX_MODES: readonly {
18
+ readonly mode: TSandboxCommandMode;
19
+ readonly description: string;
20
+ }[] = [
21
+ { mode: 'auto-allow', description: 'Confine shell commands; confined ones run without a prompt' },
22
+ { mode: 'regular', description: 'Confine shell commands; prompts work as usual' },
23
+ { mode: 'off', description: 'Run shell commands on this machine, unconfined' },
24
+ ];
25
+
26
+ function isSandboxMode(value: string): value is TSandboxCommandMode {
27
+ return SANDBOX_MODES.some((entry) => entry.mode === value);
28
+ }
29
+
30
+ export function formatSandboxStatus(status: ICommandSandboxStatus): string {
31
+ const lines = [
32
+ `Sandbox: ${status.mode}${status.backend !== undefined ? ` (${status.backend})` : ''}`,
33
+ ];
34
+ if (status.unavailable !== undefined) {
35
+ lines.push(
36
+ status.mode === 'off'
37
+ ? `Not available here: ${status.unavailable}.`
38
+ : `Cannot run here: ${status.unavailable}. Commands run unconfined.`,
39
+ );
40
+ }
41
+ if (status.mode !== 'off' && status.unavailable === undefined) {
42
+ lines.push(
43
+ 'Writes are limited to the workspace and temp directories; settings and git hooks stay read-only.',
44
+ `Network: ${status.network ? 'allowed' : 'blocked'}.`,
45
+ );
46
+ if (status.excludedCommands.length > 0) {
47
+ lines.push(`Run unconfined: ${status.excludedCommands.join(', ')}.`);
48
+ }
49
+ }
50
+ return lines.join('\n');
51
+ }
52
+
53
+ async function askForMode(
54
+ context: ICommandHostUserInteraction,
55
+ current: TSandboxCommandMode,
56
+ ): Promise<string | undefined> {
57
+ const ui = context.getUserInteraction();
58
+ if (!ui) return undefined;
59
+ const options = SANDBOX_MODES.map(({ mode, description }) => ({
60
+ value: mode,
61
+ label: mode === current ? `${mode} (current)` : mode,
62
+ description,
63
+ }));
64
+ const response = await ui.ask(selectAction('sandbox', 'Sandbox mode', options));
65
+ return response.type === 'answer' ? response.values[0] : undefined;
66
+ }
67
+
68
+ export async function executeSandboxCommand(
69
+ context: ICommandHostAdapterAccess & ICommandHostUserInteraction,
70
+ args: string,
71
+ ): Promise<ICommandResult> {
72
+ const adapter = context.getCommandHostAdapters?.().sandbox;
73
+ if (adapter === undefined) {
74
+ return { message: 'This host has no OS sandbox.', success: false };
75
+ }
76
+ const before = adapter.status();
77
+ const requested = args.trim().split(/\s+/)[0] || (await askForMode(context, before.mode));
78
+ if (requested === undefined || requested === '') {
79
+ return { message: formatSandboxStatus(before), success: true, data: { ...before } };
80
+ }
81
+ if (!isSandboxMode(requested)) {
82
+ return {
83
+ message: `Unknown sandbox mode "${requested}". Valid: ${SANDBOX_MODES.map((entry) => entry.mode).join(' | ')}`,
84
+ success: false,
85
+ };
86
+ }
87
+ adapter.setMode(requested);
88
+ const after = adapter.status();
89
+ return {
90
+ message: `Sandbox mode set to: ${requested}\n${formatSandboxStatus(after)}`,
91
+ success: true,
92
+ data: { ...after },
93
+ };
94
+ }