@robota-sdk/agent-command 3.0.0-beta.100
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/CHANGELOG.md +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- 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 +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -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 +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -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 +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- 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 +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -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 +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- 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 +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- 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-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- 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-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- 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 +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- 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 +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -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 +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
4
|
+
|
|
5
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
6
|
+
|
|
7
|
+
import { createMCPActivationCommandModule } from '../mcp-activation-command-module.js';
|
|
8
|
+
import { executeMCPActivationCommand } from '../mcp-activation-command.js';
|
|
9
|
+
import {
|
|
10
|
+
mcpUnavailableServersNotice,
|
|
11
|
+
mcpUserActionCommand,
|
|
12
|
+
mcpUserActionNotice,
|
|
13
|
+
} from '../mcp-model-notice.js';
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ICommandMCPActivationAdapter,
|
|
17
|
+
ICommandMCPActivationSummary,
|
|
18
|
+
} from '@robota-sdk/agent-framework';
|
|
19
|
+
import type { TCommandInvocationSource } from '@robota-sdk/agent-interface-command';
|
|
20
|
+
|
|
21
|
+
function summary(overrides: Partial<ICommandMCPActivationSummary>): ICommandMCPActivationSummary {
|
|
22
|
+
return {
|
|
23
|
+
serverId: 'server-1',
|
|
24
|
+
displayName: 'Example server',
|
|
25
|
+
source: 'project',
|
|
26
|
+
status: 'pending',
|
|
27
|
+
allowed: false,
|
|
28
|
+
reason: 'SECRET-REASON-TEXT from a definition',
|
|
29
|
+
provenanceId: 'project-settings:/home/me/repo/.fixture-state/settings.json',
|
|
30
|
+
definitionFingerprint: 'fingerprint-abc',
|
|
31
|
+
securityIdentity: 'https://user:hunter2@mcp.example.com/?token=abc',
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const adapter: ICommandMCPActivationAdapter = {
|
|
37
|
+
list: () => [
|
|
38
|
+
summary({ serverId: 'github', status: 'approved', allowed: true }),
|
|
39
|
+
summary({ serverId: 'linear', status: 'pending' }),
|
|
40
|
+
summary({ serverId: 'jira', status: 'untrusted' }),
|
|
41
|
+
summary({ serverId: 'evil; rm -rf ~', status: 'stale' }),
|
|
42
|
+
summary({ serverId: 'gone', status: 'rejected' }),
|
|
43
|
+
],
|
|
44
|
+
sourceProblems: () => [
|
|
45
|
+
{ source: 'project', origin: '/home/me/repo/.fixture-state/settings.json', reason: 'not json' },
|
|
46
|
+
],
|
|
47
|
+
approve: () => {
|
|
48
|
+
throw new Error('not used');
|
|
49
|
+
},
|
|
50
|
+
reject: () => {
|
|
51
|
+
throw new Error('not used');
|
|
52
|
+
},
|
|
53
|
+
revoke: () => {
|
|
54
|
+
throw new Error('not used');
|
|
55
|
+
},
|
|
56
|
+
oauthStatus: async () => [{ serverId: 'github', state: 'sign-in-required' }],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function host(source: TCommandInvocationSource, cliName = 'test-agent', terminal = false) {
|
|
60
|
+
return createTestCommandHost({
|
|
61
|
+
overrides: {
|
|
62
|
+
getCommandHostAdapters: () => ({
|
|
63
|
+
mcpActivation: terminal ? { ...adapter, userActionSurface: 'terminal' } : adapter,
|
|
64
|
+
}),
|
|
65
|
+
getCommandInvocationSource: () => source,
|
|
66
|
+
getCommandProductVocabulary: () => ({ cliName, displayName: cliName }),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('/mcp status as the model sees it', () => {
|
|
72
|
+
it('names each server, its states, and the command to suggest', async () => {
|
|
73
|
+
const result = await executeMCPActivationCommand(host('model'), 'status');
|
|
74
|
+
|
|
75
|
+
expect(result.success).toBe(true);
|
|
76
|
+
expect(result.message).toBe(
|
|
77
|
+
[
|
|
78
|
+
'MCP servers:',
|
|
79
|
+
' github — approved — sign-in: sign-in-required — ask the user to run `/mcp login github`',
|
|
80
|
+
' linear — pending — ask the user to run `/mcp approve linear`',
|
|
81
|
+
' jira — untrusted — ask the user to run `test-agent trust`',
|
|
82
|
+
' (name not shown) — stale — ask the user to run `/mcp approve <server>`',
|
|
83
|
+
' gone — rejected',
|
|
84
|
+
'1 MCP configuration source(s) could not be read; the user can see which with `/mcp status`.',
|
|
85
|
+
].join('\n'),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('names the terminal sign-in command in a run with no session prompt', async () => {
|
|
90
|
+
const terminal = { ...adapter, userActionSurface: 'terminal' as const };
|
|
91
|
+
const result = await executeMCPActivationCommand(
|
|
92
|
+
createTestCommandHost({
|
|
93
|
+
overrides: {
|
|
94
|
+
getCommandHostAdapters: () => ({ mcpActivation: terminal }),
|
|
95
|
+
getCommandInvocationSource: () => 'model',
|
|
96
|
+
getCommandProductVocabulary: () => ({ cliName: 'test-agent', displayName: 'Test Agent' }),
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
'status',
|
|
100
|
+
);
|
|
101
|
+
expect(result.message).toContain(
|
|
102
|
+
'github — approved — sign-in: sign-in-required — ask the user to run `test-agent mcp login github`',
|
|
103
|
+
);
|
|
104
|
+
expect(result.message).not.toContain('/mcp login');
|
|
105
|
+
// Approval has no terminal command; it stays the session command.
|
|
106
|
+
expect(result.message).toContain('`/mcp approve linear`');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('carries no reason text, source path, fingerprint, identity or unsafe name', async () => {
|
|
110
|
+
const result = await executeMCPActivationCommand(host('model'), '');
|
|
111
|
+
const everything = JSON.stringify(result);
|
|
112
|
+
|
|
113
|
+
for (const leaked of [
|
|
114
|
+
'SECRET-REASON-TEXT',
|
|
115
|
+
'/home/me/repo',
|
|
116
|
+
'fingerprint-abc',
|
|
117
|
+
'hunter2',
|
|
118
|
+
'token=abc',
|
|
119
|
+
'mcp.example.com',
|
|
120
|
+
'rm -rf',
|
|
121
|
+
'Example server',
|
|
122
|
+
]) {
|
|
123
|
+
expect(everything).not.toContain(leaked);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('keeps the full view for the user', async () => {
|
|
128
|
+
const result = await executeMCPActivationCommand(host('user'), 'status');
|
|
129
|
+
expect(result.message).toContain('SECRET-REASON-TEXT');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('gives a caller that does not say who it is the restricted view', async () => {
|
|
133
|
+
const { getCommandInvocationSource: _unsaid, ...anonymous } = host('user');
|
|
134
|
+
const result = await executeMCPActivationCommand(anonymous, 'status');
|
|
135
|
+
expect(result.message).toContain('`/mcp approve linear`');
|
|
136
|
+
expect(result.message).not.toContain('SECRET-REASON-TEXT');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('the `/mcp login` the notices suggest', () => {
|
|
141
|
+
it('is a real verb for the user and is refused for the model before it runs', async () => {
|
|
142
|
+
// A user-typed `/mcp login` reaches the sign-in path (here: no host adapter), never "Unknown".
|
|
143
|
+
const userResult = await executeMCPActivationCommand(
|
|
144
|
+
createTestCommandHost({ overrides: { getCommandInvocationSource: () => 'user' } }),
|
|
145
|
+
'login github',
|
|
146
|
+
);
|
|
147
|
+
expect(userResult.message).not.toContain('Unknown argument');
|
|
148
|
+
expect(userResult.message).toContain('not available in this environment');
|
|
149
|
+
|
|
150
|
+
const [command] = createMCPActivationCommandModule().systemCommands ?? [];
|
|
151
|
+
expect(command?.subcommands).toContainEqual(
|
|
152
|
+
expect.objectContaining({ name: 'login', modelInvocable: false }),
|
|
153
|
+
);
|
|
154
|
+
const execute = vi.fn();
|
|
155
|
+
const executor = new SystemCommandExecutor([{ ...command!, execute }]);
|
|
156
|
+
const modelResult = await executor.executeModelInvocable(
|
|
157
|
+
'mcp',
|
|
158
|
+
createTestCommandHost(),
|
|
159
|
+
'login github',
|
|
160
|
+
);
|
|
161
|
+
expect(modelResult?.success).toBe(false);
|
|
162
|
+
expect(modelResult?.message).toContain('only the user can');
|
|
163
|
+
expect(execute).not.toHaveBeenCalled();
|
|
164
|
+
expect(mcpUserActionCommand('github', 'sign-in')).toBe('/mcp login github');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('MCP notices for the model', () => {
|
|
169
|
+
it('names a shell-safe server and the user command', () => {
|
|
170
|
+
expect(mcpUserActionNotice('linear', 'approve')).toBe(
|
|
171
|
+
'MCP server "linear" is waiting for the user’s approval, so its tools are unavailable. You ' +
|
|
172
|
+
'cannot do this yourself; ask the user to run `/mcp approve linear`.',
|
|
173
|
+
);
|
|
174
|
+
expect(mcpUserActionNotice('github', 'sign-in')).toContain('`/mcp login github`');
|
|
175
|
+
expect(mcpUserActionNotice('jira', 'trust-workspace', 'session', 'test-agent')).toContain(
|
|
176
|
+
'`test-agent trust` in a terminal, then restart the session',
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('names the terminal sign-in command where no /mcp command can be typed', () => {
|
|
181
|
+
expect(mcpUserActionNotice('github', 'sign-in', 'terminal', 'test-agent')).toBe(
|
|
182
|
+
'MCP server "github" needs the user to sign in, so its tools are unavailable. You cannot do ' +
|
|
183
|
+
'this yourself; ask the user to run `test-agent mcp login github` in a terminal, then restart ' +
|
|
184
|
+
'the session.',
|
|
185
|
+
);
|
|
186
|
+
expect(mcpUserActionCommand('$(curl evil)', 'sign-in', 'terminal', 'test-agent')).toBe(
|
|
187
|
+
'test-agent mcp login <server>',
|
|
188
|
+
);
|
|
189
|
+
// Only sign-in moves: approval has no terminal command.
|
|
190
|
+
expect(mcpUserActionCommand('linear', 'approve', 'terminal')).toBe('/mcp approve linear');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('uses the command vocabulary supplied by each host and stays generic when it is absent', async () => {
|
|
194
|
+
const first = await executeMCPActivationCommand(host('model', 'alpha-agent', true), 'status');
|
|
195
|
+
const second = await executeMCPActivationCommand(host('model', 'beta-agent', true), 'status');
|
|
196
|
+
const noVocabulary = createTestCommandHost({
|
|
197
|
+
overrides: {
|
|
198
|
+
getCommandHostAdapters: () => ({
|
|
199
|
+
mcpActivation: { ...adapter, userActionSurface: 'terminal' },
|
|
200
|
+
}),
|
|
201
|
+
getCommandInvocationSource: () => 'model',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
const generic = await executeMCPActivationCommand(noVocabulary, 'status');
|
|
205
|
+
|
|
206
|
+
expect(first.message).toContain('`alpha-agent mcp login github`');
|
|
207
|
+
expect(first.message).not.toContain('beta-agent');
|
|
208
|
+
expect(second.message).toContain('`beta-agent mcp login github`');
|
|
209
|
+
expect(second.message).not.toContain('alpha-agent');
|
|
210
|
+
expect(generic.message).not.toContain('mcp login');
|
|
211
|
+
expect(generic.message).not.toMatch(/undefined|fixture-agent/i);
|
|
212
|
+
expect(mcpUserActionNotice('jira', 'trust-workspace', 'session', 'alpha-agent')).toContain(
|
|
213
|
+
'`alpha-agent trust` in a terminal, then restart the session',
|
|
214
|
+
);
|
|
215
|
+
expect(mcpUserActionNotice('jira', 'trust-workspace')).toContain(
|
|
216
|
+
'ask the user to trust this workspace in a terminal',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('stays generic for a name that is not safe to show', () => {
|
|
221
|
+
const notice = mcpUserActionNotice('$(curl evil)', 'sign-in');
|
|
222
|
+
expect(notice).not.toContain('curl');
|
|
223
|
+
expect(notice).toContain('`/mcp status` lists it');
|
|
224
|
+
expect(mcpUserActionCommand('$(curl evil)', 'approve')).toBe('/mcp approve <server>');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('lists every unavailable server, or says nothing', () => {
|
|
228
|
+
expect(mcpUnavailableServersNotice(new Map())).toBeUndefined();
|
|
229
|
+
const notice = mcpUnavailableServersNotice(
|
|
230
|
+
new Map([
|
|
231
|
+
['linear', 'approve'],
|
|
232
|
+
['github', 'sign-in'],
|
|
233
|
+
]),
|
|
234
|
+
);
|
|
235
|
+
expect(notice?.split('\n')).toEqual([
|
|
236
|
+
'MCP servers that did not start this session. Mention this only when the user needs one of ' +
|
|
237
|
+
'these servers or its tools:',
|
|
238
|
+
`- ${mcpUserActionNotice('linear', 'approve')}`,
|
|
239
|
+
`- ${mcpUserActionNotice('github', 'sign-in')}`,
|
|
240
|
+
]);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { expect, it, vi } from 'vitest';
|
|
2
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
3
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
4
|
+
import { executeMCPActivationCommand } from '../mcp-activation-command.js';
|
|
5
|
+
import { createMCPActivationCommandModule } from '../mcp-activation-command-module.js';
|
|
6
|
+
import type { TCommandInvocationSource } from '@robota-sdk/agent-interface-command';
|
|
7
|
+
|
|
8
|
+
const preview = {
|
|
9
|
+
serverId: 'docs',
|
|
10
|
+
uri: 'skill://docs/demo/SKILL.md',
|
|
11
|
+
namespace: 'origin',
|
|
12
|
+
fingerprint: 'a'.repeat(64),
|
|
13
|
+
name: 'demo',
|
|
14
|
+
description: 'Use this workflow',
|
|
15
|
+
frontmatter: { name: 'demo', description: 'Use this workflow', 'allowed-tools': 'Write' },
|
|
16
|
+
content: '---\nname: demo\n---\nInstructions for review.',
|
|
17
|
+
};
|
|
18
|
+
function fixture(source: TCommandInvocationSource | undefined, locality?: 'local' | 'remote') {
|
|
19
|
+
const skills = {
|
|
20
|
+
list: vi.fn(async () => [{ ...preview, content: undefined }]),
|
|
21
|
+
inspect: vi.fn(async () => preview),
|
|
22
|
+
approve: vi.fn(async () => preview),
|
|
23
|
+
withdraw: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
const host = createTestCommandHost({
|
|
26
|
+
overrides: {
|
|
27
|
+
getCommandHostAdapters: () => ({
|
|
28
|
+
mcpActivation: {
|
|
29
|
+
list: () => [],
|
|
30
|
+
approve: vi.fn(),
|
|
31
|
+
reject: vi.fn(),
|
|
32
|
+
revoke: vi.fn(),
|
|
33
|
+
skills,
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
getCommandInvocationSource: () => source ?? 'user',
|
|
37
|
+
getCommandSurfaceLocalityEvidence: () => locality,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const {
|
|
41
|
+
getCommandInvocationSource: _source,
|
|
42
|
+
getCommandSurfaceLocalityEvidence: _locality,
|
|
43
|
+
...base
|
|
44
|
+
} = host;
|
|
45
|
+
return {
|
|
46
|
+
skills,
|
|
47
|
+
host: {
|
|
48
|
+
...base,
|
|
49
|
+
...(source === undefined ? {} : { getCommandInvocationSource: () => source }),
|
|
50
|
+
...(locality === undefined ? {} : { getCommandSurfaceLocalityEvidence: () => locality }),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it('lists model-visible metadata without inspecting or approving instructions', async () => {
|
|
56
|
+
const f = fixture('model');
|
|
57
|
+
const result = await executeMCPActivationCommand(f.host, 'skill-list docs');
|
|
58
|
+
expect(result.success).toBe(true);
|
|
59
|
+
expect(result.message).toContain('Use this workflow');
|
|
60
|
+
expect(f.skills.list).toHaveBeenCalledWith('docs');
|
|
61
|
+
expect(f.skills.inspect).not.toHaveBeenCalled();
|
|
62
|
+
expect(f.skills.approve).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('shows exact content and supplies a content-bound approval command for the user', async () => {
|
|
66
|
+
const f = fixture('user');
|
|
67
|
+
const result = await executeMCPActivationCommand(
|
|
68
|
+
f.host,
|
|
69
|
+
'skill-inspect docs skill://docs/demo/SKILL.md',
|
|
70
|
+
);
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
expect(result.message).toContain(preview.content);
|
|
73
|
+
expect(result.message).toContain(
|
|
74
|
+
`/mcp skill-approve docs skill://docs/demo/SKILL.md ${preview.fingerprint}`,
|
|
75
|
+
);
|
|
76
|
+
expect(f.skills.approve).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it.each(['model', 'remote', undefined] as const)(
|
|
80
|
+
'refuses consent actions from %s before dispatch',
|
|
81
|
+
async (source) => {
|
|
82
|
+
const f = fixture(source);
|
|
83
|
+
for (const verb of ['skill-inspect', 'skill-approve', 'skill-withdraw']) {
|
|
84
|
+
const result = await executeMCPActivationCommand(
|
|
85
|
+
f.host,
|
|
86
|
+
`${verb} docs skill://docs/demo/SKILL.md ${preview.fingerprint}`,
|
|
87
|
+
);
|
|
88
|
+
expect(result.success).toBe(false);
|
|
89
|
+
expect(result.message).toContain('local user');
|
|
90
|
+
}
|
|
91
|
+
expect(f.skills.inspect).not.toHaveBeenCalled();
|
|
92
|
+
expect(f.skills.approve).not.toHaveBeenCalled();
|
|
93
|
+
expect(f.skills.withdraw).not.toHaveBeenCalled();
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
it('allows a proven local app user and refuses another-device transport', async () => {
|
|
98
|
+
const f = fixture('remote', 'local');
|
|
99
|
+
expect(
|
|
100
|
+
(
|
|
101
|
+
await executeMCPActivationCommand(
|
|
102
|
+
f.host,
|
|
103
|
+
`skill-approve docs ${preview.uri} ${preview.fingerprint}`,
|
|
104
|
+
)
|
|
105
|
+
).success,
|
|
106
|
+
).toBe(true);
|
|
107
|
+
expect(f.skills.approve).toHaveBeenCalledWith('docs', preview.uri, preview.fingerprint, 'user');
|
|
108
|
+
const remote = fixture('remote', 'remote');
|
|
109
|
+
expect(
|
|
110
|
+
(
|
|
111
|
+
await executeMCPActivationCommand(
|
|
112
|
+
remote.host,
|
|
113
|
+
`skill-approve docs ${preview.uri} ${preview.fingerprint}`,
|
|
114
|
+
)
|
|
115
|
+
).success,
|
|
116
|
+
).toBe(false);
|
|
117
|
+
expect(remote.skills.approve).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('allows only metadata listing through the model command executor', async () => {
|
|
121
|
+
const [command] = createMCPActivationCommandModule().systemCommands ?? [];
|
|
122
|
+
const execute = vi.fn();
|
|
123
|
+
const executor = new SystemCommandExecutor([{ ...command!, execute }]);
|
|
124
|
+
for (const verb of ['skill-inspect', 'skill-approve', 'skill-withdraw'])
|
|
125
|
+
expect(
|
|
126
|
+
(await executor.executeModelInvocable('mcp', createTestCommandHost(), `${verb} docs`))
|
|
127
|
+
?.success,
|
|
128
|
+
).toBe(false);
|
|
129
|
+
expect(execute).not.toHaveBeenCalled();
|
|
130
|
+
await executor.executeModelInvocable('mcp', createTestCommandHost(), 'skill-list docs');
|
|
131
|
+
expect(execute).toHaveBeenCalledOnce();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('accepts JSON argument arrays for opaque names and URIs without shell evaluation', async () => {
|
|
135
|
+
const f = fixture('user');
|
|
136
|
+
const args = ['docs with spaces', 'skill://host/demo/SKILL.md?label=a b'];
|
|
137
|
+
expect(
|
|
138
|
+
(await executeMCPActivationCommand(f.host, `skill-inspect ${JSON.stringify(args)}`)).success,
|
|
139
|
+
).toBe(true);
|
|
140
|
+
expect(f.skills.inspect).toHaveBeenCalledWith(...args);
|
|
141
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createMCPActivationCommandEntry,
|
|
3
|
+
createMCPActivationCommandModule,
|
|
4
|
+
MCPActivationCommandSource,
|
|
5
|
+
} from './mcp-activation-command-module.js';
|
|
6
|
+
export { executeMCPActivationCommand } from './mcp-activation-command.js';
|
|
7
|
+
export {
|
|
8
|
+
mcpUnavailableServersNotice,
|
|
9
|
+
mcpUserActionCommand,
|
|
10
|
+
mcpUserActionNotice,
|
|
11
|
+
} from './mcp-model-notice.js';
|
|
12
|
+
export type { TMCPUserAction, TMCPUserActionSurface } from './mcp-model-notice.js';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { executeMCPActivationCommand } from './mcp-activation-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
|
+
export function createMCPActivationCommandEntry(): ICommand {
|
|
7
|
+
return {
|
|
8
|
+
name: 'mcp',
|
|
9
|
+
displayName: 'MCP activation',
|
|
10
|
+
description:
|
|
11
|
+
"Show each MCP server's trust approval and OAuth sign-in state, or change one server: " +
|
|
12
|
+
'approve, reject or revoke its trust; sign in to it (login, which connects it in this ' +
|
|
13
|
+
'session) or out of it (logout). Use when an MCP server is not approved, needs a sign-in, or ' +
|
|
14
|
+
'should stop being used. List MCP Skills metadata, inspect exact instructions, approve their ' +
|
|
15
|
+
'content fingerprint or withdraw content consent separately from server trust. Returns the status list, verified preview, or what the action did.',
|
|
16
|
+
// Model-invocable for `status` only: when an MCP server's tools are missing or failing, the
|
|
17
|
+
// model can see why and name the command the user should run. Approving, rejecting and revoking
|
|
18
|
+
// a server are trust decisions and signing in or out is a credential action, so those stay
|
|
19
|
+
// user-only — the model's `status` view also omits everything but names and states.
|
|
20
|
+
modelDescription:
|
|
21
|
+
'Show which MCP servers are configured and why any of them is unavailable. Use it when an MCP ' +
|
|
22
|
+
'server or its tools are missing, a tool call reports that a server needs sign-in or approval, ' +
|
|
23
|
+
'or the user asks about MCP servers. Returns one line per server: its name, activation state, ' +
|
|
24
|
+
'sign-in state, and the command to suggest to the user when the server needs their approval, ' +
|
|
25
|
+
'sign-in or workspace trust. Use skill-list <server> to discover advertised skill names, URIs ' +
|
|
26
|
+
'and descriptions without loading instructions. Returns an invocation name for each supported ' +
|
|
27
|
+
'skill; use the session skill activation command with that exact name when its workflow fits ' +
|
|
28
|
+
'the task. If content consent is needed, ask the user to ' +
|
|
29
|
+
'run /mcp skill-inspect <server> <uri> and approve the reviewed fingerprint. You cannot approve, inspect instructions, sign in or sign out yourself.',
|
|
30
|
+
argumentHint:
|
|
31
|
+
'[status] | <approve|reject|revoke|logout> <server> | login <server> [--no-browser]',
|
|
32
|
+
source: 'mcp-activation',
|
|
33
|
+
modelInvocable: true,
|
|
34
|
+
// The bare `/mcp` shows the status view, so choosing it from a menu runs it.
|
|
35
|
+
runsBare: true,
|
|
36
|
+
subcommands: [
|
|
37
|
+
{
|
|
38
|
+
name: 'status',
|
|
39
|
+
description: 'Show every MCP server’s name, activation state and sign-in state',
|
|
40
|
+
source: 'mcp-activation',
|
|
41
|
+
modelInvocable: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'skill-list',
|
|
45
|
+
description:
|
|
46
|
+
'Discover MCP skill metadata and exact invocation names, or reasons they are unavailable, without activating instructions',
|
|
47
|
+
argumentHint: '<server>',
|
|
48
|
+
source: 'mcp-activation',
|
|
49
|
+
modelInvocable: true,
|
|
50
|
+
},
|
|
51
|
+
...(['skill-inspect', 'skill-approve', 'skill-withdraw'] as const).map((verb) => ({
|
|
52
|
+
name: verb,
|
|
53
|
+
description:
|
|
54
|
+
verb === 'skill-inspect'
|
|
55
|
+
? 'Review verified MCP skill instructions and frontmatter without activation'
|
|
56
|
+
: verb === 'skill-approve'
|
|
57
|
+
? 'Grant consent for an exactly reviewed MCP skill content fingerprint'
|
|
58
|
+
: 'Withdraw MCP skill content consent',
|
|
59
|
+
argumentHint: verb === 'skill-approve' ? '<server> <uri> <fingerprint>' : '<server> <uri>',
|
|
60
|
+
source: 'mcp-activation',
|
|
61
|
+
modelInvocable: false,
|
|
62
|
+
})),
|
|
63
|
+
{
|
|
64
|
+
name: 'reload',
|
|
65
|
+
description: 'Retry connecting every MCP server that is not currently connected',
|
|
66
|
+
source: 'mcp-activation',
|
|
67
|
+
modelInvocable: false,
|
|
68
|
+
},
|
|
69
|
+
...(['approve', 'reject', 'revoke', 'login', 'logout'] as const).map((verb) => ({
|
|
70
|
+
name: verb,
|
|
71
|
+
description: USER_ONLY_VERB_DESCRIPTION[verb],
|
|
72
|
+
argumentHint: verb === 'login' ? '<server> [--no-browser]' : '<server>',
|
|
73
|
+
source: 'mcp-activation',
|
|
74
|
+
modelInvocable: false,
|
|
75
|
+
})),
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const USER_ONLY_VERB_DESCRIPTION = {
|
|
81
|
+
approve: 'Trust and activate an MCP server',
|
|
82
|
+
reject: 'Refuse an MCP server',
|
|
83
|
+
revoke: 'Withdraw an earlier approval',
|
|
84
|
+
login: 'Sign in to an OAuth MCP server and connect it in this session',
|
|
85
|
+
logout: 'Sign out of an OAuth MCP server and revoke its tokens',
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
88
|
+
function createMCPActivationSystemCommand(): ISystemCommand {
|
|
89
|
+
const entry = createMCPActivationCommandEntry();
|
|
90
|
+
return {
|
|
91
|
+
name: entry.name,
|
|
92
|
+
displayName: entry.displayName,
|
|
93
|
+
description: entry.description,
|
|
94
|
+
...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
|
|
95
|
+
argumentHint: entry.argumentHint,
|
|
96
|
+
...(entry.subcommands !== undefined ? { subcommands: entry.subcommands } : {}),
|
|
97
|
+
// Approve/reject/revoke/login/logout change trust or credentials, so the command is never read-only
|
|
98
|
+
// for the user or a remote policy. The model can reach only `status` (its subcommand
|
|
99
|
+
// allowlist), a read-only view, so its call needs no prompt.
|
|
100
|
+
requiresPermission: true,
|
|
101
|
+
modelRequiresPermission: false,
|
|
102
|
+
userInvocable: true,
|
|
103
|
+
modelInvocable: true,
|
|
104
|
+
lifecycle: 'inline',
|
|
105
|
+
execute: executeMCPActivationCommand,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class MCPActivationCommandSource implements ICommandSource {
|
|
110
|
+
readonly name = 'mcp-activation';
|
|
111
|
+
|
|
112
|
+
getCommands(): ICommand[] {
|
|
113
|
+
return [createMCPActivationCommandEntry()];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createMCPActivationCommandModule(): ICommandModule {
|
|
118
|
+
return {
|
|
119
|
+
name: 'agent-command-mcp-activation',
|
|
120
|
+
commandSources: [new MCPActivationCommandSource()],
|
|
121
|
+
systemCommands: [createMCPActivationSystemCommand()],
|
|
122
|
+
};
|
|
123
|
+
}
|