@pellux/goodvibes-agent 1.0.30 → 1.0.33
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 +42 -1
- package/README.md +7 -5
- package/dist/package/main.js +7323 -2268
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +6 -2
- package/docs/connected-host.md +26 -5
- package/docs/getting-started.md +30 -10
- package/docs/knowledge-artifacts-and-multimodal.md +16 -4
- package/docs/project-planning.md +2 -2
- package/docs/providers-and-routing.md +3 -2
- package/docs/release-and-publishing.md +15 -11
- package/docs/tools-and-commands.md +20 -8
- package/package.json +8 -3
- package/release/live-verification/live-verification.json +148 -0
- package/release/live-verification/live-verification.md +187 -0
- package/release/performance-snapshot.json +57 -0
- package/release/release-notes.md +28 -0
- package/release/release-readiness.json +581 -0
- package/src/cli/agent-knowledge-command.ts +5 -5
- package/src/cli/agent-knowledge-format.ts +11 -0
- package/src/cli/agent-knowledge-runtime.ts +92 -13
- package/src/cli/bundle-command.ts +5 -4
- package/src/cli/entrypoint.ts +5 -2
- package/src/cli/external-runtime.ts +2 -15
- package/src/cli/management.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/knowledge.ts +2 -2
- package/src/runtime/bootstrap.ts +2 -2
- package/src/runtime/connected-host-auth.ts +16 -0
- package/src/tools/agent-channel-send-tool.ts +5 -7
- package/src/tools/agent-harness-channel-metadata.ts +177 -0
- package/src/tools/agent-harness-connected-host-status.ts +1 -1
- package/src/tools/agent-harness-delegation-posture.ts +216 -0
- package/src/tools/agent-harness-keybinding-metadata.ts +57 -22
- package/src/tools/agent-harness-mcp-metadata.ts +246 -0
- package/src/tools/agent-harness-media-posture.ts +282 -0
- package/src/tools/agent-harness-metadata.ts +21 -4
- package/src/tools/agent-harness-model-routing.ts +501 -0
- package/src/tools/agent-harness-notification-metadata.ts +217 -0
- package/src/tools/agent-harness-operator-methods.ts +285 -0
- package/src/tools/agent-harness-pairing-posture.ts +265 -0
- package/src/tools/agent-harness-provider-account-metadata.ts +203 -0
- package/src/tools/agent-harness-release-evidence.ts +364 -0
- package/src/tools/agent-harness-release-readiness.ts +298 -0
- package/src/tools/agent-harness-security-posture.ts +646 -0
- package/src/tools/agent-harness-service-posture.ts +201 -0
- package/src/tools/agent-harness-session-metadata.ts +282 -0
- package/src/tools/agent-harness-setup-posture.ts +295 -0
- package/src/tools/agent-harness-tool-schema.ts +103 -27
- package/src/tools/agent-harness-tool.ts +209 -236
- package/src/tools/agent-harness-ui-surface-metadata.ts +1 -1
- package/src/tools/agent-harness-workspace-actions.ts +232 -0
- package/src/tools/agent-knowledge-ingest-tool.ts +6 -8
- package/src/tools/agent-knowledge-tool.ts +122 -23
- package/src/tools/agent-local-registry-tool.ts +4 -5
- package/src/tools/agent-media-generate-tool.ts +4 -6
- package/src/tools/agent-notify-tool.ts +5 -6
- package/src/tools/agent-operator-action-tool.ts +6 -8
- package/src/tools/agent-operator-briefing-tool.ts +3 -4
- package/src/tools/agent-reminder-schedule-tool.ts +6 -7
- package/src/tools/agent-tool-policy-guard.ts +8 -17
- package/src/tools/agent-work-plan-tool.ts +3 -4
- package/src/version.ts +2 -2
|
@@ -135,7 +135,7 @@ export async function connectedHostStatusSummary(
|
|
|
135
135
|
],
|
|
136
136
|
findings: connectedHostFindings(runtime, tokenUsable),
|
|
137
137
|
modelAccess: {
|
|
138
|
-
diagnostics: 'Use mode:"connected_host_status" for live read-only host readiness
|
|
138
|
+
diagnostics: 'Use mode:"connected_host_status" for live read-only host readiness, mode:"service_posture" for endpoint posture, mode:"service_endpoint" for one endpoint, and mode:"connected_host" for capability and boundary inventory.',
|
|
139
139
|
cliMirrors: ['goodvibes-agent status --json', 'goodvibes-agent doctor', 'goodvibes-agent compat'],
|
|
140
140
|
tuiMirrors: ['Agent Workspace -> Home -> Host compatibility', 'Agent Workspace -> Home -> Doctor diagnostics', 'Agent Workspace -> Home -> Review health'],
|
|
141
141
|
},
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
2
|
+
|
|
3
|
+
export interface AgentHarnessDelegationArgs {
|
|
4
|
+
readonly delegationRouteId?: unknown;
|
|
5
|
+
readonly target?: unknown;
|
|
6
|
+
readonly query?: unknown;
|
|
7
|
+
readonly includeParameters?: unknown;
|
|
8
|
+
readonly limit?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type DelegationResolution =
|
|
12
|
+
| { readonly status: 'found'; readonly route: Record<string, unknown> }
|
|
13
|
+
| { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
|
|
14
|
+
| { readonly status: 'missing_lookup'; readonly usage: string };
|
|
15
|
+
|
|
16
|
+
type DelegationLookupSource = 'delegationRouteId' | 'target' | 'query';
|
|
17
|
+
|
|
18
|
+
interface DelegationRoute {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
readonly effect: 'read-only' | 'main-conversation' | 'delegated-work' | 'blocked';
|
|
23
|
+
readonly command?: string;
|
|
24
|
+
readonly commandTemplate?: string;
|
|
25
|
+
readonly workspaceActionId?: string;
|
|
26
|
+
readonly confirmationRequired?: boolean;
|
|
27
|
+
readonly reviewPolicy?: 'explicit-only' | 'not-applicable';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readString(value: unknown): string {
|
|
31
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLimit(value: unknown, fallback: number): number {
|
|
35
|
+
const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
|
|
36
|
+
if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
|
|
37
|
+
return Math.max(1, Math.min(500, Math.trunc(parsed)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function lookupFromArgs(args: AgentHarnessDelegationArgs): { readonly source: DelegationLookupSource; readonly input: string } | null {
|
|
41
|
+
const delegationRouteId = readString(args.delegationRouteId);
|
|
42
|
+
if (delegationRouteId) return { source: 'delegationRouteId', input: delegationRouteId };
|
|
43
|
+
const target = readString(args.target);
|
|
44
|
+
if (target) return { source: 'target', input: target };
|
|
45
|
+
const query = readString(args.query);
|
|
46
|
+
return query ? { source: 'query', input: query } : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function routes(): readonly DelegationRoute[] {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
id: 'delegate-build-task',
|
|
53
|
+
label: 'Delegate build/fix/review task',
|
|
54
|
+
detail: 'Send one explicit build, fix, implementation, patch, or review task to GoodVibes TUI/shared-session routes with the full original ask.',
|
|
55
|
+
effect: 'delegated-work',
|
|
56
|
+
commandTemplate: '/delegate <full original user ask>',
|
|
57
|
+
workspaceActionId: 'delegate-task',
|
|
58
|
+
confirmationRequired: true,
|
|
59
|
+
reviewPolicy: 'explicit-only',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'delegate-build-task-with-review',
|
|
63
|
+
label: 'Delegate with requested review',
|
|
64
|
+
detail: 'Request delegated review only when the user explicitly asks for review or explicitly requests review as part of a build/fix/review handoff.',
|
|
65
|
+
effect: 'delegated-work',
|
|
66
|
+
commandTemplate: '/delegate --review <full original user ask>',
|
|
67
|
+
workspaceActionId: 'delegate-task',
|
|
68
|
+
confirmationRequired: true,
|
|
69
|
+
reviewPolicy: 'explicit-only',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'delegation-status',
|
|
73
|
+
label: 'Delegation status',
|
|
74
|
+
detail: 'Inspect build-delegation receipts and shared-session status without starting coding work.',
|
|
75
|
+
effect: 'read-only',
|
|
76
|
+
command: '/delegate status',
|
|
77
|
+
workspaceActionId: 'delegation-status',
|
|
78
|
+
reviewPolicy: 'not-applicable',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'ordinary-agent-work',
|
|
82
|
+
label: 'Ordinary Agent work',
|
|
83
|
+
detail: 'Planning, research, operations, memory, configuration, approvals, automation observability, media generation, and ordinary assistant work stay in the main Agent conversation or first-class Agent tools.',
|
|
84
|
+
effect: 'main-conversation',
|
|
85
|
+
reviewPolicy: 'not-applicable',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'local-coding-blocked',
|
|
89
|
+
label: 'Local coding execution blocked in Agent',
|
|
90
|
+
detail: 'Agent does not own file edits, git/worktree workflows, execution isolation UX, coding panels, or local delegated review chains.',
|
|
91
|
+
effect: 'blocked',
|
|
92
|
+
reviewPolicy: 'not-applicable',
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function routeSearchText(route: DelegationRoute): string {
|
|
98
|
+
return [
|
|
99
|
+
route.id,
|
|
100
|
+
route.label,
|
|
101
|
+
route.detail,
|
|
102
|
+
route.effect,
|
|
103
|
+
route.command ?? '',
|
|
104
|
+
route.commandTemplate ?? '',
|
|
105
|
+
route.workspaceActionId ?? '',
|
|
106
|
+
route.reviewPolicy ?? '',
|
|
107
|
+
].join('\n').toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeCandidate(route: DelegationRoute): Record<string, unknown> {
|
|
111
|
+
return {
|
|
112
|
+
delegationRouteId: route.id,
|
|
113
|
+
label: route.label,
|
|
114
|
+
effect: route.effect,
|
|
115
|
+
confirmationRequired: route.confirmationRequired === true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function describeRoute(
|
|
120
|
+
context: CommandContext,
|
|
121
|
+
route: DelegationRoute,
|
|
122
|
+
options: { readonly includeParameters?: boolean; readonly lookup?: Record<string, unknown> } = {},
|
|
123
|
+
): Record<string, unknown> {
|
|
124
|
+
return {
|
|
125
|
+
delegationRouteId: route.id,
|
|
126
|
+
label: route.label,
|
|
127
|
+
detail: route.detail,
|
|
128
|
+
effect: route.effect,
|
|
129
|
+
...(route.command ? { command: route.command } : {}),
|
|
130
|
+
...(route.commandTemplate ? { commandTemplate: route.commandTemplate } : {}),
|
|
131
|
+
...(route.workspaceActionId ? { workspaceActionId: route.workspaceActionId } : {}),
|
|
132
|
+
confirmationRequired: route.confirmationRequired === true,
|
|
133
|
+
reviewPolicy: route.reviewPolicy ?? 'not-applicable',
|
|
134
|
+
runtime: {
|
|
135
|
+
sessionId: context.session.runtime.sessionId,
|
|
136
|
+
operatorClientAttached: Boolean(context.clients?.operator),
|
|
137
|
+
},
|
|
138
|
+
...(options.lookup ? { lookup: options.lookup } : {}),
|
|
139
|
+
policy: {
|
|
140
|
+
effect: route.effect,
|
|
141
|
+
values: 'Delegation posture returns policy, route, confirmation, and runtime availability metadata only; it does not submit delegated work.',
|
|
142
|
+
mutation: 'Delegated work submission stays a visible confirmed workspace or slash-command flow and must preserve the full original user ask.',
|
|
143
|
+
},
|
|
144
|
+
...(options.includeParameters ? {
|
|
145
|
+
modelAccess: {
|
|
146
|
+
inspectDelegation: 'agent_harness mode:"delegation_posture"',
|
|
147
|
+
inspectRoute: 'agent_harness mode:"delegation_route"',
|
|
148
|
+
workspaceAction: route.workspaceActionId ? `agent_harness mode:"workspace_action" actionId:"${route.workspaceActionId}"` : null,
|
|
149
|
+
runWorkspaceAction: route.workspaceActionId ? `agent_harness mode:"run_workspace_action" actionId:"${route.workspaceActionId}" confirm:true explicitUserRequest:"..."` : null,
|
|
150
|
+
runCommand: route.command ? `agent_harness mode:"run_command" command:"${route.command}" confirm:true explicitUserRequest:"..."` : null,
|
|
151
|
+
runCommandTemplate: route.commandTemplate ? `agent_harness mode:"run_command" command:"${route.commandTemplate}" confirm:true explicitUserRequest:"..."` : null,
|
|
152
|
+
},
|
|
153
|
+
} : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function delegationPostureCatalogStatus(context: CommandContext): Record<string, unknown> {
|
|
158
|
+
return {
|
|
159
|
+
modes: ['delegation_posture', 'delegation_route'],
|
|
160
|
+
routes: routes().length,
|
|
161
|
+
operatorClientAttached: Boolean(context.clients?.operator),
|
|
162
|
+
readOnly: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function delegationPostureSummary(context: CommandContext, args: AgentHarnessDelegationArgs): Record<string, unknown> {
|
|
167
|
+
const query = readString(args.query).toLowerCase();
|
|
168
|
+
const includeParameters = args.includeParameters === true;
|
|
169
|
+
const filtered = routes()
|
|
170
|
+
.filter((route) => !query || routeSearchText(route).includes(query))
|
|
171
|
+
.slice(0, readLimit(args.limit, 100));
|
|
172
|
+
return {
|
|
173
|
+
status: 'available',
|
|
174
|
+
summary: {
|
|
175
|
+
routes: routes().length,
|
|
176
|
+
operatorClientAttached: Boolean(context.clients?.operator),
|
|
177
|
+
sessionId: context.session.runtime.sessionId,
|
|
178
|
+
delegatedReviewPolicy: 'explicit-build-delegation-only',
|
|
179
|
+
normalChatPolicy: 'ordinary assistant work stays in the main Agent conversation',
|
|
180
|
+
codingOwnership: 'GoodVibes TUI owns file edits, git/worktree workflows, execution isolation UX, coding panels, and delegated review coordination',
|
|
181
|
+
},
|
|
182
|
+
routes: filtered.map((route) => describeRoute(context, route, { includeParameters })),
|
|
183
|
+
returned: filtered.length,
|
|
184
|
+
total: routes().length,
|
|
185
|
+
policy: 'Read-only delegation posture. Delegation submission requires an explicit build/fix/review/implementation user request, visible confirmation, and preservation of the full original ask.',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function describeHarnessDelegationRoute(context: CommandContext, args: AgentHarnessDelegationArgs): DelegationResolution {
|
|
190
|
+
const lookup = lookupFromArgs(args);
|
|
191
|
+
if (!lookup) {
|
|
192
|
+
return {
|
|
193
|
+
status: 'missing_lookup',
|
|
194
|
+
usage: 'delegation_route requires delegationRouteId, target, or query. Use mode:"delegation_posture" to inspect delegation route ids.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const all = routes();
|
|
198
|
+
const normalized = lookup.input.toLowerCase();
|
|
199
|
+
const exact = all.find((route) => route.id === lookup.input);
|
|
200
|
+
if (exact) return { status: 'found', route: describeRoute(context, exact, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'id' } }) };
|
|
201
|
+
const insensitive = all.find((route) => route.id.toLowerCase() === normalized);
|
|
202
|
+
if (insensitive) return { status: 'found', route: describeRoute(context, insensitive, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
|
|
203
|
+
const searched = all.filter((route) => routeSearchText(route).includes(normalized));
|
|
204
|
+
if (searched.length === 1) return { status: 'found', route: describeRoute(context, searched[0]!, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'search' } }) };
|
|
205
|
+
if (searched.length > 1) {
|
|
206
|
+
return {
|
|
207
|
+
status: 'ambiguous',
|
|
208
|
+
input: lookup.input,
|
|
209
|
+
candidates: searched.slice(0, 8).map(describeCandidate),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
status: 'missing_lookup',
|
|
214
|
+
usage: `Unknown delegation route ${lookup.input}. Use mode:"delegation_posture" to inspect delegation route ids.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -53,6 +53,7 @@ interface KeybindingOperationRoute {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
type KeybindingsOverrideFile = Record<string, unknown>;
|
|
56
|
+
type KeybindingEntry = { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] };
|
|
56
57
|
|
|
57
58
|
interface KeybindingLookup {
|
|
58
59
|
readonly source: 'actionId' | 'target' | 'key' | 'query';
|
|
@@ -93,6 +94,19 @@ function requireKeybindingsManager(context: CommandContext): KeybindingsManager
|
|
|
93
94
|
return manager;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
function readKeybindingsManager(context: CommandContext): KeybindingsManager | null {
|
|
98
|
+
return context.workspace.keybindingsManager ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function defaultKeybindingEntries(): KeybindingEntry[] {
|
|
102
|
+
return (Object.entries(DEFAULT_KEYBINDINGS) as [KeyAction, KeyCombo[]][])
|
|
103
|
+
.map(([action, combos]) => ({
|
|
104
|
+
action,
|
|
105
|
+
description: ACTION_DESCRIPTIONS[action],
|
|
106
|
+
combos: combos.map((combo) => ({ ...combo })),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
function isKeyAction(action: string): action is KeyAction {
|
|
97
111
|
return Object.hasOwn(ACTION_DESCRIPTIONS, action);
|
|
98
112
|
}
|
|
@@ -162,13 +176,23 @@ function combosEqual(left: readonly KeyCombo[], right: readonly KeyCombo[]): boo
|
|
|
162
176
|
return left.every((combo, index) => comboFingerprint(combo) === comboFingerprint(right[index]));
|
|
163
177
|
}
|
|
164
178
|
|
|
165
|
-
function
|
|
179
|
+
function formatCombo(manager: KeybindingsManager | null, combo: KeyCombo): string {
|
|
180
|
+
if (manager) return manager.formatCombo(combo);
|
|
181
|
+
const parts: string[] = [];
|
|
182
|
+
if (combo.ctrl) parts.push('Ctrl');
|
|
183
|
+
if (combo.alt) parts.push('Alt');
|
|
184
|
+
if (combo.shift) parts.push('Shift');
|
|
185
|
+
parts.push(combo.key.length === 1 ? combo.key.toUpperCase() : combo.key);
|
|
186
|
+
return parts.join('+');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function describeCombo(manager: KeybindingsManager | null, combo: KeyCombo): Record<string, unknown> {
|
|
166
190
|
return {
|
|
167
191
|
key: combo.key,
|
|
168
192
|
ctrl: combo.ctrl === true,
|
|
169
193
|
shift: combo.shift === true,
|
|
170
194
|
alt: combo.alt === true,
|
|
171
|
-
label:
|
|
195
|
+
label: formatCombo(manager, combo),
|
|
172
196
|
};
|
|
173
197
|
}
|
|
174
198
|
|
|
@@ -184,33 +208,33 @@ function keybindingLookupFromArgs(args: HarnessKeybindingArgs): { readonly sourc
|
|
|
184
208
|
return null;
|
|
185
209
|
}
|
|
186
210
|
|
|
187
|
-
function bindingSearchText(manager: KeybindingsManager, entry:
|
|
211
|
+
function bindingSearchText(manager: KeybindingsManager | null, entry: KeybindingEntry): string {
|
|
188
212
|
return [
|
|
189
213
|
entry.action,
|
|
190
214
|
entry.description,
|
|
191
215
|
...entry.combos.map((combo) => comboFingerprint(combo)),
|
|
192
|
-
...entry.combos.map((combo) =>
|
|
216
|
+
...entry.combos.map((combo) => formatCombo(manager, combo)),
|
|
193
217
|
].join('\n').toLowerCase();
|
|
194
218
|
}
|
|
195
219
|
|
|
196
|
-
function bindingCandidate(manager: KeybindingsManager, entry:
|
|
220
|
+
function bindingCandidate(manager: KeybindingsManager | null, entry: KeybindingEntry): Record<string, unknown> {
|
|
197
221
|
return {
|
|
198
222
|
action: entry.action,
|
|
199
223
|
description: entry.description,
|
|
200
|
-
labels: entry.combos.map((combo) =>
|
|
224
|
+
labels: entry.combos.map((combo) => formatCombo(manager, combo)),
|
|
201
225
|
};
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
function bindingMatches(manager: KeybindingsManager, entry:
|
|
228
|
+
function bindingMatches(manager: KeybindingsManager | null, entry: KeybindingEntry, query: string): boolean {
|
|
205
229
|
if (!query) return true;
|
|
206
230
|
return bindingSearchText(manager, entry).includes(query.toLowerCase());
|
|
207
231
|
}
|
|
208
232
|
|
|
209
233
|
function resolveHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): KeybindingResolution | null {
|
|
210
|
-
const manager =
|
|
234
|
+
const manager = readKeybindingsManager(context);
|
|
211
235
|
const lookup = keybindingLookupFromArgs(args);
|
|
212
236
|
if (!lookup) return null;
|
|
213
|
-
const entries = manager
|
|
237
|
+
const entries = manager?.getAll() ?? defaultKeybindingEntries();
|
|
214
238
|
if (isKeyAction(lookup.input)) return { status: 'found', action: lookup.input, lookup: { ...lookup, resolvedBy: 'action' } };
|
|
215
239
|
const inputLower = lookup.input.toLowerCase();
|
|
216
240
|
const ciActions = entries.filter((entry) => entry.action.toLowerCase() === inputLower);
|
|
@@ -222,7 +246,7 @@ function resolveHarnessKeybinding(context: CommandContext, args: HarnessKeybindi
|
|
|
222
246
|
return null;
|
|
223
247
|
}
|
|
224
248
|
|
|
225
|
-
function describeBinding(manager: KeybindingsManager, action: KeyAction, combos: KeyCombo[], lookup?: KeybindingLookup): Record<string, unknown> {
|
|
249
|
+
function describeBinding(manager: KeybindingsManager | null, action: KeyAction, combos: KeyCombo[], lookup?: KeybindingLookup): Record<string, unknown> {
|
|
226
250
|
const defaults = DEFAULT_KEYBINDINGS[action];
|
|
227
251
|
const customized = !combosEqual(combos, defaults);
|
|
228
252
|
return {
|
|
@@ -230,10 +254,10 @@ function describeBinding(manager: KeybindingsManager, action: KeyAction, combos:
|
|
|
230
254
|
description: ACTION_DESCRIPTIONS[action],
|
|
231
255
|
...(lookup ? { lookup } : {}),
|
|
232
256
|
bindings: combos.map((combo) => describeCombo(manager, combo)),
|
|
233
|
-
labels: combos.map((combo) =>
|
|
257
|
+
labels: combos.map((combo) => formatCombo(manager, combo)),
|
|
234
258
|
defaultBindings: defaults.map((combo) => describeCombo(manager, combo)),
|
|
235
259
|
customized,
|
|
236
|
-
source: customized ? 'custom' : 'default',
|
|
260
|
+
source: manager ? (customized ? 'custom' : 'default') : 'default-fallback',
|
|
237
261
|
modelOperation: keybindingOperationRoute(action),
|
|
238
262
|
};
|
|
239
263
|
}
|
|
@@ -377,7 +401,7 @@ function writeOverrideFile(configPath: string, overrides: KeybindingsOverrideFil
|
|
|
377
401
|
}
|
|
378
402
|
|
|
379
403
|
export function totalHarnessKeybindings(context: CommandContext): number {
|
|
380
|
-
return context.workspace.keybindingsManager?.getAll().length ??
|
|
404
|
+
return context.workspace.keybindingsManager?.getAll().length ?? Object.keys(DEFAULT_KEYBINDINGS).length;
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
export function totalHarnessShortcuts(context: CommandContext): number {
|
|
@@ -391,41 +415,52 @@ export function listHarnessShortcuts(context: CommandContext, args: HarnessKeybi
|
|
|
391
415
|
.filter((shortcut) => !query || `${shortcut.key}\n${shortcut.description}`.toLowerCase().includes(query))
|
|
392
416
|
.slice(0, readLimit(args.limit, 200))
|
|
393
417
|
.map((shortcut) => ({ ...shortcut, source: 'fixed', userEditable: false }));
|
|
418
|
+
const degraded = keybindings.status === 'degraded';
|
|
394
419
|
return {
|
|
420
|
+
status: degraded ? 'degraded' : 'available',
|
|
395
421
|
configPath: keybindings.configPath,
|
|
396
422
|
fixedShortcuts: fixed,
|
|
397
423
|
configurableKeybindings: keybindings.keybindings,
|
|
398
424
|
returned: fixed.length + Number(keybindings.returned ?? 0),
|
|
399
425
|
total: totalHarnessShortcuts(context),
|
|
400
|
-
policy:
|
|
426
|
+
policy: degraded
|
|
427
|
+
? 'Fixed shortcuts and default keybindings are available for discovery. Live keybinding execution and mutation require the runtime keybinding manager.'
|
|
428
|
+
: 'Fixed shortcuts are runtime/editor controls. Configurable keybindings can be inspected, supported shell-safe actions can be run with run_keybinding, and bindings can be changed with set_keybinding/reset_keybinding.',
|
|
401
429
|
};
|
|
402
430
|
}
|
|
403
431
|
|
|
404
432
|
export function listHarnessKeybindings(context: CommandContext, args: HarnessKeybindingArgs): Record<string, unknown> {
|
|
405
|
-
const manager =
|
|
433
|
+
const manager = readKeybindingsManager(context);
|
|
406
434
|
const query = readString(args.query);
|
|
407
|
-
const
|
|
435
|
+
const allEntries = manager?.getAll() ?? defaultKeybindingEntries();
|
|
436
|
+
const entries = allEntries
|
|
408
437
|
.filter((entry) => bindingMatches(manager, entry, query))
|
|
409
438
|
.slice(0, readLimit(args.limit, 200))
|
|
410
439
|
.map((entry) => describeBinding(manager, entry.action, entry.combos));
|
|
411
440
|
return {
|
|
412
|
-
|
|
441
|
+
status: manager ? 'available' : 'degraded',
|
|
442
|
+
configPath: manager?.getConfigPath() ?? null,
|
|
413
443
|
keybindings: entries,
|
|
414
444
|
returned: entries.length,
|
|
415
|
-
total:
|
|
416
|
-
policy:
|
|
445
|
+
total: allEntries.length,
|
|
446
|
+
policy: manager
|
|
447
|
+
? 'Reads the live resolved keybindings, including modelOperation route metadata. run_keybinding executes only supported shell-safe actions. set_keybinding/reset_keybinding write the same keybindings.json file the user can edit and reload the runtime manager.'
|
|
448
|
+
: 'Live keybinding manager is unavailable; default keybindings are shown for discovery only. run_keybinding, set_keybinding, and reset_keybinding require a live manager.',
|
|
417
449
|
};
|
|
418
450
|
}
|
|
419
451
|
|
|
420
452
|
export function describeHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): Record<string, unknown> | null {
|
|
421
|
-
const manager =
|
|
453
|
+
const manager = readKeybindingsManager(context);
|
|
422
454
|
const resolved = resolveHarnessKeybinding(context, args);
|
|
423
455
|
if (resolved?.status === 'ambiguous') return { status: 'ambiguous', input: resolved.input, candidates: resolved.candidates };
|
|
424
456
|
if (!resolved) return null;
|
|
425
|
-
const
|
|
457
|
+
const entries = manager?.getAll() ?? defaultKeybindingEntries();
|
|
458
|
+
const entry = entries.find((candidate) => candidate.action === resolved.action);
|
|
426
459
|
return entry ? {
|
|
427
|
-
|
|
460
|
+
status: manager ? 'available' : 'degraded',
|
|
461
|
+
configPath: manager?.getConfigPath() ?? null,
|
|
428
462
|
...describeBinding(manager, entry.action, entry.combos, resolved.lookup),
|
|
463
|
+
...(manager ? {} : { note: 'Default keybinding descriptor only; live run/set/reset operations require the runtime keybinding manager.' }),
|
|
429
464
|
} : null;
|
|
430
465
|
}
|
|
431
466
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
2
|
+
|
|
3
|
+
export interface AgentHarnessMcpArgs {
|
|
4
|
+
readonly mcpServerId?: unknown;
|
|
5
|
+
readonly target?: unknown;
|
|
6
|
+
readonly query?: unknown;
|
|
7
|
+
readonly includeParameters?: unknown;
|
|
8
|
+
readonly limit?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type McpServerLookupSource = 'mcpServerId' | 'target' | 'query';
|
|
12
|
+
|
|
13
|
+
type McpServerResolution =
|
|
14
|
+
| { readonly status: 'found'; readonly server: Record<string, unknown> }
|
|
15
|
+
| { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
|
|
16
|
+
| { readonly status: 'missing_lookup'; readonly usage: string };
|
|
17
|
+
|
|
18
|
+
type McpServerRecord = ReturnType<NonNullable<NonNullable<CommandContext['clients']>['mcpApi']>['listServerSecurity']>[number];
|
|
19
|
+
|
|
20
|
+
interface McpToolRecord {
|
|
21
|
+
readonly serverName: string;
|
|
22
|
+
readonly toolName: string;
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readString(value: unknown): string {
|
|
27
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readLimit(value: unknown, fallback: number): number {
|
|
31
|
+
const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
|
|
32
|
+
if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
|
|
33
|
+
return Math.max(1, Math.min(500, Math.trunc(parsed)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveMcpApi(context: CommandContext): {
|
|
37
|
+
readonly listServerSecurity: () => readonly McpServerRecord[];
|
|
38
|
+
readonly listAllTools?: (() => Promise<readonly McpToolRecord[]>) | undefined;
|
|
39
|
+
} | null {
|
|
40
|
+
const api = context.clients?.mcpApi ?? context.extensions?.mcpRegistry;
|
|
41
|
+
if (!api || typeof api.listServerSecurity !== 'function') return null;
|
|
42
|
+
return {
|
|
43
|
+
listServerSecurity: () => api.listServerSecurity(),
|
|
44
|
+
...(typeof (api as { readonly listAllTools?: unknown }).listAllTools === 'function'
|
|
45
|
+
? { listAllTools: () => (api as { listAllTools: () => Promise<readonly McpToolRecord[]> }).listAllTools() }
|
|
46
|
+
: {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function lookupFromArgs(args: AgentHarnessMcpArgs): { readonly source: McpServerLookupSource; readonly input: string } | null {
|
|
51
|
+
const mcpServerId = readString(args.mcpServerId);
|
|
52
|
+
if (mcpServerId) return { source: 'mcpServerId', input: mcpServerId };
|
|
53
|
+
const target = readString(args.target);
|
|
54
|
+
if (target) return { source: 'target', input: target };
|
|
55
|
+
const query = readString(args.query);
|
|
56
|
+
return query ? { source: 'query', input: query } : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function serverSearchText(server: McpServerRecord): string {
|
|
60
|
+
return [
|
|
61
|
+
server.name,
|
|
62
|
+
server.connected ? 'connected' : 'disconnected',
|
|
63
|
+
server.trustMode,
|
|
64
|
+
server.role,
|
|
65
|
+
server.schemaFreshness,
|
|
66
|
+
server.quarantineReason ?? '',
|
|
67
|
+
server.quarantineDetail ?? '',
|
|
68
|
+
...server.allowedHosts,
|
|
69
|
+
].join('\n').toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function describeCandidate(server: McpServerRecord): Record<string, unknown> {
|
|
73
|
+
return {
|
|
74
|
+
mcpServerId: server.name,
|
|
75
|
+
connected: server.connected,
|
|
76
|
+
trustMode: server.trustMode,
|
|
77
|
+
role: server.role,
|
|
78
|
+
schemaFreshness: server.schemaFreshness,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toolsByServer(tools: readonly McpToolRecord[]): ReadonlyMap<string, readonly McpToolRecord[]> {
|
|
83
|
+
const grouped = new Map<string, McpToolRecord[]>();
|
|
84
|
+
for (const tool of tools) {
|
|
85
|
+
const entries = grouped.get(tool.serverName) ?? [];
|
|
86
|
+
entries.push(tool);
|
|
87
|
+
grouped.set(tool.serverName, entries);
|
|
88
|
+
}
|
|
89
|
+
return grouped;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function describeServer(
|
|
93
|
+
server: McpServerRecord,
|
|
94
|
+
options: {
|
|
95
|
+
readonly includeParameters?: boolean;
|
|
96
|
+
readonly tools?: readonly McpToolRecord[];
|
|
97
|
+
readonly lookup?: Record<string, unknown>;
|
|
98
|
+
} = {},
|
|
99
|
+
): Record<string, unknown> {
|
|
100
|
+
const tools = options.tools ?? [];
|
|
101
|
+
return {
|
|
102
|
+
name: server.name,
|
|
103
|
+
connected: server.connected,
|
|
104
|
+
trustMode: server.trustMode,
|
|
105
|
+
role: server.role,
|
|
106
|
+
schemaFreshness: server.schemaFreshness,
|
|
107
|
+
allowedPathCount: server.allowedPaths.length,
|
|
108
|
+
allowedHostCount: server.allowedHosts.length,
|
|
109
|
+
...(server.quarantineReason ? { quarantineReason: server.quarantineReason } : {}),
|
|
110
|
+
...(server.quarantineDetail ? { quarantineDetail: server.quarantineDetail } : {}),
|
|
111
|
+
...(options.lookup ? { lookup: options.lookup } : {}),
|
|
112
|
+
...(options.includeParameters ? {
|
|
113
|
+
tools: tools.map((tool) => ({
|
|
114
|
+
name: tool.toolName,
|
|
115
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
116
|
+
})),
|
|
117
|
+
toolCount: tools.length,
|
|
118
|
+
} : { toolCount: tools.length }),
|
|
119
|
+
policy: {
|
|
120
|
+
effect: 'read-only',
|
|
121
|
+
values: 'Server posture returns trust, role, connection, quarantine, and tool metadata; env values and secret config values are never returned.',
|
|
122
|
+
mutation: 'MCP add/remove/reload/trust/role/quarantine operations stay explicit confirmation-gated workspace or slash-command flows.',
|
|
123
|
+
allowAll: 'allow-all trust decisions remain routed through Settings -> MCP, not direct command escalation.',
|
|
124
|
+
},
|
|
125
|
+
modelAccess: {
|
|
126
|
+
reviewCommand: '/mcp review',
|
|
127
|
+
serversCommand: '/mcp servers',
|
|
128
|
+
toolsCommand: `/mcp tools ${server.name}`,
|
|
129
|
+
repairCommand: `/mcp repair ${server.name}`,
|
|
130
|
+
authReviewCommand: '/mcp auth-review',
|
|
131
|
+
configCommand: '/mcp config',
|
|
132
|
+
workspaceActionIds: [
|
|
133
|
+
'mcp-review',
|
|
134
|
+
'mcp-tools-server',
|
|
135
|
+
'mcp-repair',
|
|
136
|
+
'mcp-config',
|
|
137
|
+
'mcp-add-server',
|
|
138
|
+
'mcp-settings',
|
|
139
|
+
],
|
|
140
|
+
confirmationGatedCommands: [
|
|
141
|
+
`/mcp trust ${server.name} <constrained|ask-on-risk|blocked> --yes`,
|
|
142
|
+
`/mcp role ${server.name} <role> --yes`,
|
|
143
|
+
`/mcp quarantine ${server.name} approve <operatorId> --yes`,
|
|
144
|
+
`/mcp remove ${server.name} --yes`,
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function readMcpTools(api: ReturnType<typeof resolveMcpApi>, includeParameters: boolean): Promise<readonly McpToolRecord[]> {
|
|
151
|
+
if (!includeParameters || !api?.listAllTools) return [];
|
|
152
|
+
try {
|
|
153
|
+
return await api.listAllTools();
|
|
154
|
+
} catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function mcpServerCatalogStatus(context: CommandContext): Record<string, unknown> {
|
|
160
|
+
const api = resolveMcpApi(context);
|
|
161
|
+
if (!api) {
|
|
162
|
+
return {
|
|
163
|
+
modes: ['mcp_servers', 'mcp_server'],
|
|
164
|
+
status: 'unavailable',
|
|
165
|
+
readOnly: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const servers = api.listServerSecurity();
|
|
169
|
+
return {
|
|
170
|
+
modes: ['mcp_servers', 'mcp_server'],
|
|
171
|
+
servers: servers.length,
|
|
172
|
+
connected: servers.filter((server) => server.connected).length,
|
|
173
|
+
attention: servers.filter((server) => !server.connected || server.schemaFreshness === 'quarantined').length,
|
|
174
|
+
readOnly: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function mcpServerSummary(context: CommandContext, args: AgentHarnessMcpArgs): Promise<Record<string, unknown>> {
|
|
179
|
+
const api = resolveMcpApi(context);
|
|
180
|
+
if (!api) {
|
|
181
|
+
return {
|
|
182
|
+
status: 'unavailable',
|
|
183
|
+
servers: [],
|
|
184
|
+
returned: 0,
|
|
185
|
+
total: 0,
|
|
186
|
+
policy: 'MCP runtime API is unavailable in this Agent context.',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const query = readString(args.query).toLowerCase();
|
|
190
|
+
const includeParameters = args.includeParameters === true;
|
|
191
|
+
const servers = api.listServerSecurity();
|
|
192
|
+
const tools = toolsByServer(await readMcpTools(api, includeParameters));
|
|
193
|
+
const filtered = servers
|
|
194
|
+
.filter((server) => !query || serverSearchText(server).includes(query))
|
|
195
|
+
.slice(0, readLimit(args.limit, 100));
|
|
196
|
+
return {
|
|
197
|
+
servers: filtered.map((server) => describeServer(server, {
|
|
198
|
+
includeParameters,
|
|
199
|
+
tools: tools.get(server.name) ?? [],
|
|
200
|
+
})),
|
|
201
|
+
returned: filtered.length,
|
|
202
|
+
total: servers.length,
|
|
203
|
+
connected: servers.filter((server) => server.connected).length,
|
|
204
|
+
attention: servers.filter((server) => !server.connected || server.schemaFreshness === 'quarantined').length,
|
|
205
|
+
policy: 'Read-only MCP server posture. Use confirmed workspace actions or slash-command mirrors for add/remove/reload/trust/role/quarantine mutations.',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function describeHarnessMcpServer(context: CommandContext, args: AgentHarnessMcpArgs): Promise<McpServerResolution> {
|
|
210
|
+
const lookup = lookupFromArgs(args);
|
|
211
|
+
if (!lookup) {
|
|
212
|
+
return {
|
|
213
|
+
status: 'missing_lookup',
|
|
214
|
+
usage: 'mcp_server requires mcpServerId, target, or query. Use mode:"mcp_servers" to inspect MCP server ids.',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const api = resolveMcpApi(context);
|
|
218
|
+
if (!api) {
|
|
219
|
+
return {
|
|
220
|
+
status: 'missing_lookup',
|
|
221
|
+
usage: 'MCP runtime API is unavailable in this Agent context.',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const servers = api.listServerSecurity();
|
|
225
|
+
const normalized = lookup.input.toLowerCase();
|
|
226
|
+
const exact = servers.find((server) => server.name === lookup.input);
|
|
227
|
+
const tools = toolsByServer(await readMcpTools(api, true));
|
|
228
|
+
if (exact) return { status: 'found', server: describeServer(exact, { includeParameters: true, tools: tools.get(exact.name) ?? [], lookup: { ...lookup, resolvedBy: 'id' } }) };
|
|
229
|
+
const insensitive = servers.find((server) => server.name.toLowerCase() === normalized);
|
|
230
|
+
if (insensitive) return { status: 'found', server: describeServer(insensitive, { includeParameters: true, tools: tools.get(insensitive.name) ?? [], lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
|
|
231
|
+
const searched = servers.filter((server) => serverSearchText(server).includes(normalized));
|
|
232
|
+
if (searched.length === 1) {
|
|
233
|
+
return { status: 'found', server: describeServer(searched[0]!, { includeParameters: true, tools: tools.get(searched[0]!.name) ?? [], lookup: { ...lookup, resolvedBy: 'search' } }) };
|
|
234
|
+
}
|
|
235
|
+
if (searched.length > 1) {
|
|
236
|
+
return {
|
|
237
|
+
status: 'ambiguous',
|
|
238
|
+
input: lookup.input,
|
|
239
|
+
candidates: searched.slice(0, 8).map(describeCandidate),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
status: 'missing_lookup',
|
|
244
|
+
usage: `Unknown MCP server ${lookup.input}. Use mode:"mcp_servers" to inspect MCP server ids.`,
|
|
245
|
+
};
|
|
246
|
+
}
|