@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.
- package/README.md +11 -2
- package/dist/node/index.cjs +76 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +76 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +34 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +48 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +241 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +47 -0
- package/src/peers/peers-command.ts +142 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- 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
|
-
|
|
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-
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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,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
|
+
}
|