@pellux/goodvibes-agent 1.0.31 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +72 -58
  3. package/dist/package/main.js +7572 -2430
  4. package/docs/README.md +25 -21
  5. package/docs/channels-remote-and-api.md +6 -2
  6. package/docs/connected-host.md +27 -6
  7. package/docs/getting-started.md +87 -66
  8. package/docs/knowledge-artifacts-and-multimodal.md +16 -4
  9. package/docs/project-planning.md +2 -2
  10. package/docs/providers-and-routing.md +3 -2
  11. package/docs/release-and-publishing.md +15 -11
  12. package/docs/tools-and-commands.md +150 -128
  13. package/docs/voice-and-live-tts.md +1 -1
  14. package/package.json +8 -3
  15. package/release/live-verification/live-verification.json +148 -0
  16. package/release/live-verification/live-verification.md +187 -0
  17. package/release/performance-snapshot.json +57 -0
  18. package/release/release-notes.md +19 -0
  19. package/release/release-readiness.json +581 -0
  20. package/src/agent/harness-control.ts +42 -3
  21. package/src/cli/agent-knowledge-command.ts +5 -5
  22. package/src/cli/agent-knowledge-format.ts +11 -0
  23. package/src/cli/agent-knowledge-runtime.ts +92 -13
  24. package/src/cli/bundle-command.ts +5 -4
  25. package/src/cli/entrypoint.ts +5 -2
  26. package/src/cli/external-runtime.ts +2 -15
  27. package/src/cli/management.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +1 -1
  29. package/src/input/commands/knowledge.ts +2 -2
  30. package/src/runtime/bootstrap.ts +10 -18
  31. package/src/runtime/connected-host-auth.ts +16 -0
  32. package/src/tools/agent-analysis-registry-policy.ts +2 -9
  33. package/src/tools/agent-channel-send-tool.ts +3 -9
  34. package/src/tools/agent-context-policy.ts +1 -5
  35. package/src/tools/agent-find-policy.ts +1 -4
  36. package/src/tools/agent-harness-channel-metadata.ts +177 -0
  37. package/src/tools/agent-harness-cli-metadata.ts +4 -1
  38. package/src/tools/agent-harness-command-catalog.ts +10 -3
  39. package/src/tools/agent-harness-connected-host-status.ts +9 -3
  40. package/src/tools/agent-harness-delegation-posture.ts +216 -0
  41. package/src/tools/agent-harness-keybinding-metadata.ts +57 -22
  42. package/src/tools/agent-harness-mcp-metadata.ts +248 -0
  43. package/src/tools/agent-harness-media-posture.ts +282 -0
  44. package/src/tools/agent-harness-metadata.ts +44 -9
  45. package/src/tools/agent-harness-model-routing.ts +501 -0
  46. package/src/tools/agent-harness-model-tool-catalog.ts +7 -2
  47. package/src/tools/agent-harness-notification-metadata.ts +217 -0
  48. package/src/tools/agent-harness-operator-methods.ts +285 -0
  49. package/src/tools/agent-harness-pairing-posture.ts +265 -0
  50. package/src/tools/agent-harness-panel-metadata.ts +26 -12
  51. package/src/tools/agent-harness-provider-account-metadata.ts +205 -0
  52. package/src/tools/agent-harness-release-evidence.ts +364 -0
  53. package/src/tools/agent-harness-release-readiness.ts +298 -0
  54. package/src/tools/agent-harness-security-posture.ts +648 -0
  55. package/src/tools/agent-harness-service-posture.ts +207 -0
  56. package/src/tools/agent-harness-session-metadata.ts +284 -0
  57. package/src/tools/agent-harness-setup-posture.ts +295 -0
  58. package/src/tools/agent-harness-tool-schema.ts +104 -27
  59. package/src/tools/agent-harness-tool.ts +251 -235
  60. package/src/tools/agent-harness-ui-surface-metadata.ts +20 -12
  61. package/src/tools/agent-harness-workspace-actions.ts +260 -0
  62. package/src/tools/agent-knowledge-ingest-tool.ts +4 -10
  63. package/src/tools/agent-knowledge-tool.ts +120 -25
  64. package/src/tools/agent-local-registry-tool.ts +3 -7
  65. package/src/tools/agent-media-generate-tool.ts +2 -8
  66. package/src/tools/agent-notify-tool.ts +3 -8
  67. package/src/tools/agent-operator-action-tool.ts +4 -10
  68. package/src/tools/agent-operator-briefing-tool.ts +1 -6
  69. package/src/tools/agent-read-policy.ts +1 -4
  70. package/src/tools/agent-reminder-schedule-tool.ts +4 -9
  71. package/src/tools/agent-tool-policy-guard.ts +15 -51
  72. package/src/tools/agent-web-search-policy.ts +1 -4
  73. package/src/tools/agent-work-plan-tool.ts +1 -6
  74. package/src/version.ts +2 -2
@@ -0,0 +1,265 @@
1
+ import { networkInterfaces } from 'node:os';
2
+ import type { CommandContext } from '../input/command-registry.ts';
3
+ import { GOODVIBES_AGENT_PAIRING_SURFACE } from '../config/surface.ts';
4
+ import { resolveRuntimeEndpointBinding } from '../cli/endpoints.ts';
5
+ import { connectedHostOperatorTokenFingerprint, readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
6
+ import { requirePlatform, requireShellPaths } from '../input/commands/runtime-services.ts';
7
+
8
+ export interface AgentHarnessPairingArgs {
9
+ readonly pairingRouteId?: unknown;
10
+ readonly target?: unknown;
11
+ readonly query?: unknown;
12
+ readonly includeParameters?: unknown;
13
+ readonly limit?: unknown;
14
+ }
15
+
16
+ type PairingResolution =
17
+ | { readonly status: 'found'; readonly route: Record<string, unknown> }
18
+ | { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
19
+ | { readonly status: 'missing_lookup'; readonly usage: string };
20
+
21
+ type PairingLookupSource = 'pairingRouteId' | 'target' | 'query';
22
+
23
+ interface PairingRoute {
24
+ readonly id: string;
25
+ readonly label: string;
26
+ readonly detail: string;
27
+ readonly effect: 'read-only' | 'visible-navigation' | 'external-network' | 'confirmation-gated-secret-display';
28
+ readonly command?: string;
29
+ readonly harnessRoute?: string;
30
+ readonly capabilityIds?: readonly string[];
31
+ readonly requiresConfirmation?: boolean;
32
+ }
33
+
34
+ function readString(value: unknown): string {
35
+ return typeof value === 'string' ? value.trim() : '';
36
+ }
37
+
38
+ function readLimit(value: unknown, fallback: number): number {
39
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
40
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
41
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
42
+ }
43
+
44
+ function localNetworkIp(): string {
45
+ try {
46
+ const nets = networkInterfaces();
47
+ for (const name of Object.keys(nets)) {
48
+ for (const netInfo of nets[name] ?? []) {
49
+ if (netInfo.family === 'IPv4' && !netInfo.internal) return netInfo.address;
50
+ }
51
+ }
52
+ } catch {
53
+ return '127.0.0.1';
54
+ }
55
+ return '127.0.0.1';
56
+ }
57
+
58
+ function urlHostForBindHost(host: string): string {
59
+ if (host === '0.0.0.0' || host === '::') return localNetworkIp();
60
+ return host || '127.0.0.1';
61
+ }
62
+
63
+ function lookupFromArgs(args: AgentHarnessPairingArgs): { readonly source: PairingLookupSource; readonly input: string } | null {
64
+ const pairingRouteId = readString(args.pairingRouteId);
65
+ if (pairingRouteId) return { source: 'pairingRouteId', input: pairingRouteId };
66
+ const target = readString(args.target);
67
+ if (target) return { source: 'target', input: target };
68
+ const query = readString(args.query);
69
+ return query ? { source: 'query', input: query } : null;
70
+ }
71
+
72
+ function pairingRoutes(): readonly PairingRoute[] {
73
+ return [
74
+ {
75
+ id: 'qr-pairing',
76
+ label: 'QR pairing',
77
+ detail: 'Visible companion pairing route that prints QR setup details in the Agent TUI without printing the raw token.',
78
+ effect: 'external-network',
79
+ command: '/pair',
80
+ harnessRoute: 'agent_harness mode:"run_command" command:"/pair" confirm:true explicitUserRequest:"..."',
81
+ requiresConfirmation: true,
82
+ },
83
+ {
84
+ id: 'manual-token-display',
85
+ label: 'Manual token display',
86
+ detail: 'Explicitly confirmed fallback route that prints the raw companion token only when the user asks for manual setup.',
87
+ effect: 'confirmation-gated-secret-display',
88
+ command: '/pair --show-token --yes',
89
+ harnessRoute: 'agent_harness mode:"run_command" command:"/pair --show-token --yes" confirm:true explicitUserRequest:"..."',
90
+ requiresConfirmation: true,
91
+ },
92
+ {
93
+ id: 'pairing-ui',
94
+ label: 'Pairing workspace route',
95
+ detail: 'Visible Agent workspace route for companion pairing and channel setup.',
96
+ effect: 'visible-navigation',
97
+ command: '/agent channels',
98
+ harnessRoute: 'agent_harness mode:"workspace_action" target:"pair"',
99
+ capabilityIds: ['companion-pairing', 'mobile-command-routing'],
100
+ },
101
+ {
102
+ id: 'connected-host-status',
103
+ label: 'Connected host live posture',
104
+ detail: 'Read-only reachability, token posture, SDK compatibility, and route readiness used before companion setup.',
105
+ effect: 'read-only',
106
+ harnessRoute: 'agent_harness mode:"connected_host_status"',
107
+ capabilityIds: ['connected-host-status'],
108
+ },
109
+ {
110
+ id: 'companion-capabilities',
111
+ label: 'Companion capability map',
112
+ detail: 'Allowed and blocked companion route families for pairing, shared sessions, tasks, approvals, provider/model changes, attachments, and mobile command surfaces.',
113
+ effect: 'read-only',
114
+ harnessRoute: 'agent_harness mode:"connected_host"',
115
+ capabilityIds: [
116
+ 'companion-pairing',
117
+ 'shared-session',
118
+ 'task-management',
119
+ 'approval-actions',
120
+ 'provider-model-routing',
121
+ 'attachment-upload',
122
+ 'mobile-command-routing',
123
+ ],
124
+ },
125
+ {
126
+ id: 'channels-readiness',
127
+ label: 'Channel readiness',
128
+ detail: 'Read-only channel setup and delivery posture used after pairing when messages or reminders need an explicit delivery target.',
129
+ effect: 'read-only',
130
+ command: '/channels',
131
+ harnessRoute: 'agent_harness mode:"channels"',
132
+ capabilityIds: ['channels', 'notifications'],
133
+ },
134
+ ];
135
+ }
136
+
137
+ function routeSearchText(route: PairingRoute): string {
138
+ return [
139
+ route.id,
140
+ route.label,
141
+ route.detail,
142
+ route.effect,
143
+ route.command ?? '',
144
+ route.harnessRoute ?? '',
145
+ ...(route.capabilityIds ?? []),
146
+ ].join('\n').toLowerCase();
147
+ }
148
+
149
+ function describeCandidate(route: PairingRoute): Record<string, unknown> {
150
+ return {
151
+ pairingRouteId: route.id,
152
+ label: route.label,
153
+ effect: route.effect,
154
+ requiresConfirmation: route.requiresConfirmation === true,
155
+ };
156
+ }
157
+
158
+ function describeRoute(route: PairingRoute, options: { readonly includeParameters?: boolean; readonly lookup?: Record<string, unknown> } = {}): Record<string, unknown> {
159
+ return {
160
+ pairingRouteId: route.id,
161
+ label: route.label,
162
+ detail: route.detail,
163
+ effect: route.effect,
164
+ ...(route.command ? { command: route.command } : {}),
165
+ ...(route.harnessRoute ? { harnessRoute: route.harnessRoute } : {}),
166
+ ...(route.capabilityIds ? { capabilityIds: route.capabilityIds } : {}),
167
+ requiresConfirmation: route.requiresConfirmation === true,
168
+ ...(options.lookup ? { lookup: options.lookup } : {}),
169
+ ...(options.includeParameters ? {
170
+ policy: {
171
+ effect: route.effect,
172
+ values: 'Pairing posture returns endpoint binding and token fingerprint only; raw companion tokens and QR payloads are never returned by this read-only mode.',
173
+ mutation: 'Pairing display, manual token display, companion connection, channel sends, provider/model changes, approval actions, and attachment flows stay explicit visible user flows.',
174
+ },
175
+ modelAccess: {
176
+ inspectPairing: 'agent_harness mode:"pairing_posture"',
177
+ inspectRoute: 'agent_harness mode:"pairing_route"',
178
+ connectedHostStatus: 'agent_harness mode:"connected_host_status"',
179
+ connectedHostCapabilities: 'agent_harness mode:"connected_host"',
180
+ channels: 'agent_harness mode:"channels"',
181
+ },
182
+ } : {}),
183
+ };
184
+ }
185
+
186
+ function pairingState(context: CommandContext): Record<string, unknown> {
187
+ const shellPaths = requireShellPaths(context);
188
+ const configManager = requirePlatform(context).configManager;
189
+ const tokenRecord = readConnectedHostOperatorToken(shellPaths.homeDirectory);
190
+ const binding = resolveRuntimeEndpointBinding(configManager, 'controlPlane');
191
+ const host = urlHostForBindHost(binding.host);
192
+ return {
193
+ surface: GOODVIBES_AGENT_PAIRING_SURFACE,
194
+ endpoint: {
195
+ endpointId: 'controlPlane',
196
+ bindHost: binding.host,
197
+ advertisedHost: host,
198
+ port: binding.port,
199
+ url: `http://${host}:${binding.port}`,
200
+ },
201
+ token: {
202
+ present: Boolean(tokenRecord.token),
203
+ path: tokenRecord.path,
204
+ fingerprint: tokenRecord.token ? `sha256:${connectedHostOperatorTokenFingerprint(tokenRecord.token)}` : null,
205
+ rawValueReturned: false,
206
+ },
207
+ companionPayloadReturned: false,
208
+ };
209
+ }
210
+
211
+ export function pairingPostureCatalogStatus(context: CommandContext): Record<string, unknown> {
212
+ const state = pairingState(context);
213
+ return {
214
+ modes: ['pairing_posture', 'pairing_route'],
215
+ routes: pairingRoutes().length,
216
+ tokenPresent: (state.token as { readonly present?: boolean }).present === true,
217
+ readOnly: true,
218
+ };
219
+ }
220
+
221
+ export function pairingPostureSummary(context: CommandContext, args: AgentHarnessPairingArgs): Record<string, unknown> {
222
+ const query = readString(args.query).toLowerCase();
223
+ const includeParameters = args.includeParameters === true;
224
+ const routes = pairingRoutes();
225
+ const filtered = routes
226
+ .filter((route) => !query || routeSearchText(route).includes(query))
227
+ .slice(0, readLimit(args.limit, 100));
228
+ return {
229
+ status: 'available',
230
+ pairing: pairingState(context),
231
+ routes: filtered.map((route) => describeRoute(route, { includeParameters })),
232
+ returned: filtered.length,
233
+ total: routes.length,
234
+ policy: 'Read-only pairing posture. QR display, manual token display, companion connection, channel delivery, task, approval, provider/model, and attachment actions remain explicit visible user flows.',
235
+ };
236
+ }
237
+
238
+ export function describeHarnessPairingRoute(_context: CommandContext, args: AgentHarnessPairingArgs): PairingResolution {
239
+ const lookup = lookupFromArgs(args);
240
+ if (!lookup) {
241
+ return {
242
+ status: 'missing_lookup',
243
+ usage: 'pairing_route requires pairingRouteId, target, or query. Use mode:"pairing_posture" to inspect pairing route ids.',
244
+ };
245
+ }
246
+ const routes = pairingRoutes();
247
+ const normalized = lookup.input.toLowerCase();
248
+ const exact = routes.find((route) => route.id === lookup.input);
249
+ if (exact) return { status: 'found', route: describeRoute(exact, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'id' } }) };
250
+ const insensitive = routes.find((route) => route.id.toLowerCase() === normalized);
251
+ if (insensitive) return { status: 'found', route: describeRoute(insensitive, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
252
+ const searched = routes.filter((route) => routeSearchText(route).includes(normalized));
253
+ if (searched.length === 1) return { status: 'found', route: describeRoute(searched[0]!, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'search' } }) };
254
+ if (searched.length > 1) {
255
+ return {
256
+ status: 'ambiguous',
257
+ input: lookup.input,
258
+ candidates: searched.slice(0, 8).map(describeCandidate),
259
+ };
260
+ }
261
+ return {
262
+ status: 'missing_lookup',
263
+ usage: `Unknown pairing route ${lookup.input}. Use mode:"pairing_posture" to inspect pairing route ids.`,
264
+ };
265
+ }
@@ -7,6 +7,7 @@ export interface AgentHarnessPanelArgs {
7
7
  readonly panelId?: unknown;
8
8
  readonly target?: unknown;
9
9
  readonly category?: unknown;
10
+ readonly includeParameters?: unknown;
10
11
  readonly limit?: unknown;
11
12
  readonly pane?: unknown;
12
13
  }
@@ -39,6 +40,11 @@ function readLimit(value: unknown, fallback: number): number {
39
40
  return Math.max(1, Math.min(500, Math.trunc(parsed)));
40
41
  }
41
42
 
43
+ function previewText(value: string, maxLength = 120): string {
44
+ const normalized = value.replace(/\s+/g, ' ').trim();
45
+ return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 1).trimEnd()}...`;
46
+ }
47
+
42
48
  function panelManager(context: CommandContext) {
43
49
  return context.workspace.panelManager ?? null;
44
50
  }
@@ -50,6 +56,7 @@ function panelMatches(panel: Record<string, unknown>, query: string): boolean {
50
56
  panel.name,
51
57
  panel.category,
52
58
  panel.description,
59
+ panel.summary,
53
60
  panel.workspaceRoute,
54
61
  ].map((value) => typeof value === 'object' ? JSON.stringify(value) : String(value ?? '')).join('\n').toLowerCase().includes(query.toLowerCase());
55
62
  }
@@ -69,7 +76,7 @@ function panelCandidate(registration: PanelRegistration): Record<string, unknown
69
76
  id: registration.id,
70
77
  name: registration.name,
71
78
  category: registration.category,
72
- description: registration.description,
79
+ summary: previewText(registration.description),
73
80
  workspaceRoute: {
74
81
  categoryId: agentWorkspaceCategoryForPanel(registration.id),
75
82
  command: agentWorkspaceCommandForPanel(registration.id),
@@ -103,7 +110,11 @@ function resolveHarnessPanel(context: CommandContext, args: AgentHarnessPanelArg
103
110
  return null;
104
111
  }
105
112
 
106
- function describePanelRegistration(context: CommandContext, registration: PanelRegistration, lookup?: PanelLookup): Record<string, unknown> {
113
+ function describePanelRegistration(
114
+ context: CommandContext,
115
+ registration: PanelRegistration,
116
+ options: { readonly includeParameters?: boolean; readonly lookup?: PanelLookup } = {},
117
+ ): Record<string, unknown> {
107
118
  const manager = panelManager(context);
108
119
  const openPanel = manager?.getPanel(registration.id) ?? null;
109
120
  const pane = manager?.getPaneOf(registration.id) ?? null;
@@ -113,8 +124,8 @@ function describePanelRegistration(context: CommandContext, registration: PanelR
113
124
  name: registration.name,
114
125
  icon: registration.icon,
115
126
  category: registration.category,
116
- description: registration.description,
117
- ...(lookup ? { lookup } : {}),
127
+ ...(options.includeParameters ? { description: registration.description } : { summary: previewText(registration.description) }),
128
+ ...(options.lookup ? { lookup: options.lookup } : {}),
118
129
  preload: registration.preload === true,
119
130
  open: openPanel !== null,
120
131
  pane,
@@ -124,11 +135,13 @@ function describePanelRegistration(context: CommandContext, registration: PanelR
124
135
  categoryId: agentWorkspaceCategoryForPanel(registration.id),
125
136
  command: agentWorkspaceCommandForPanel(registration.id),
126
137
  },
127
- policy: {
128
- effect: 'ui-navigation',
129
- confirmation: 'agent_harness mode:"open_panel" requires confirm:true and explicitUserRequest.',
130
- boundary: 'Panels are Agent/TUI operator views. The model can inspect panel catalog/open state; panel routing uses the existing Agent workspace bridge and does not mutate connected-host lifecycle.',
131
- },
138
+ ...(options.includeParameters ? {
139
+ policy: {
140
+ effect: 'ui-navigation',
141
+ confirmation: 'agent_harness mode:"open_panel" requires confirm:true and explicitUserRequest.',
142
+ boundary: 'Panels are Agent/TUI operator views. The model can inspect panel catalog/open state; panel routing uses the existing Agent workspace bridge and does not mutate connected-host lifecycle.',
143
+ },
144
+ } : {}),
132
145
  };
133
146
  }
134
147
 
@@ -142,8 +155,9 @@ export function listHarnessPanels(context: CommandContext, args: AgentHarnessPan
142
155
  const query = readString(args.query);
143
156
  const category = readString(args.category);
144
157
  const limit = readLimit(args.limit, 200);
158
+ const includeParameters = args.includeParameters === true;
145
159
  return manager.getRegisteredTypes()
146
- .map((registration) => describePanelRegistration(context, registration))
160
+ .map((registration) => describePanelRegistration(context, registration, { includeParameters }))
147
161
  .filter((panel) => !category || panel.category === category)
148
162
  .filter((panel) => panelMatches(panel, query))
149
163
  .sort((a, b) => String(a.id).localeCompare(String(b.id)))
@@ -152,7 +166,7 @@ export function listHarnessPanels(context: CommandContext, args: AgentHarnessPan
152
166
 
153
167
  export function describeHarnessPanel(context: CommandContext, args: AgentHarnessPanelArgs): Record<string, unknown> | null {
154
168
  const resolved = resolveHarnessPanel(context, args);
155
- if (resolved?.status === 'found') return describePanelRegistration(context, resolved.registration, resolved.lookup);
169
+ if (resolved?.status === 'found') return describePanelRegistration(context, resolved.registration, { includeParameters: true, lookup: resolved.lookup });
156
170
  if (resolved?.status === 'ambiguous') {
157
171
  return { status: 'ambiguous', input: resolved.input, candidates: resolved.candidates };
158
172
  }
@@ -171,7 +185,7 @@ export function openHarnessPanel(context: CommandContext, args: AgentHarnessPane
171
185
  availablePanels: listHarnessPanels(context, { limit: 50 }).map((entry) => entry.id),
172
186
  };
173
187
  }
174
- const panel = describePanelRegistration(context, resolved.registration, resolved.lookup);
188
+ const panel = describePanelRegistration(context, resolved.registration, { includeParameters: true, lookup: resolved.lookup });
175
189
  const requestedPane = readString(args.pane);
176
190
  const pane = requestedPane === 'bottom' || requestedPane === 'top' ? requestedPane : undefined;
177
191
  if (!context.showPanel) {
@@ -0,0 +1,205 @@
1
+ import type { CommandContext } from '../input/command-registry.ts';
2
+ import type { ProviderAccountRecord, ProviderAccountSnapshot } from '../panels/provider-account-snapshot.ts';
3
+ import { buildProviderAccountSnapshot } from '../panels/provider-account-snapshot.ts';
4
+ import { requireProvider, requireServiceRegistry, requireSubscriptionManager } from '../input/commands/runtime-services.ts';
5
+
6
+ export interface AgentHarnessProviderAccountArgs {
7
+ readonly providerId?: unknown;
8
+ readonly target?: unknown;
9
+ readonly query?: unknown;
10
+ readonly includeParameters?: unknown;
11
+ readonly limit?: unknown;
12
+ }
13
+
14
+ type ProviderAccountLookupSource = 'providerId' | 'target' | 'query';
15
+
16
+ type ProviderAccountResolution =
17
+ | { readonly status: 'found'; readonly account: Record<string, unknown> }
18
+ | { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
19
+ | { readonly status: 'missing_lookup'; readonly usage: string };
20
+
21
+ function readString(value: unknown): string {
22
+ return typeof value === 'string' ? value.trim() : '';
23
+ }
24
+
25
+ function readLimit(value: unknown, fallback: number): number {
26
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
27
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
28
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
29
+ }
30
+
31
+ async function loadSnapshot(context: CommandContext): Promise<ProviderAccountSnapshot> {
32
+ return await buildProviderAccountSnapshot({
33
+ providerModels: requireProvider(context).providerRegistry,
34
+ services: requireServiceRegistry(context),
35
+ subscriptions: requireSubscriptionManager(context),
36
+ environment: {
37
+ hasEnvironmentVariable: (name: string) => Boolean(process.env[name]),
38
+ },
39
+ });
40
+ }
41
+
42
+ function lookupFromArgs(args: AgentHarnessProviderAccountArgs): { readonly source: ProviderAccountLookupSource; readonly input: string } | null {
43
+ const providerId = readString(args.providerId);
44
+ if (providerId) return { source: 'providerId', input: providerId };
45
+ const target = readString(args.target);
46
+ if (target) return { source: 'target', input: target };
47
+ const query = readString(args.query);
48
+ return query ? { source: 'query', input: query } : null;
49
+ }
50
+
51
+ function accountSearchText(account: ProviderAccountRecord): string {
52
+ return [
53
+ account.providerId,
54
+ account.authFreshness,
55
+ account.activeRoute,
56
+ account.preferredRoute,
57
+ account.activeRouteReason,
58
+ account.fallbackRoute ?? '',
59
+ account.fallbackRisk ?? '',
60
+ ...account.availableRoutes,
61
+ ...account.issues,
62
+ ...account.notes,
63
+ ...account.recommendedActions,
64
+ ...account.usageWindows.flatMap((window) => [window.label, window.detail]),
65
+ ...account.routeRecords.flatMap((route) => [route.route, route.freshness, route.detail, ...route.issues]),
66
+ ].join('\n').toLowerCase();
67
+ }
68
+
69
+ function describeCandidate(account: ProviderAccountRecord): Record<string, unknown> {
70
+ return {
71
+ providerId: account.providerId,
72
+ activeRoute: account.activeRoute,
73
+ preferredRoute: account.preferredRoute,
74
+ authFreshness: account.authFreshness,
75
+ configured: account.configured,
76
+ issues: account.issues.length,
77
+ };
78
+ }
79
+
80
+ function describeAccount(
81
+ account: ProviderAccountRecord,
82
+ options: { readonly includeParameters?: boolean; readonly lookup?: Record<string, unknown> } = {},
83
+ ): Record<string, unknown> {
84
+ return {
85
+ providerId: account.providerId,
86
+ active: account.active,
87
+ configured: account.configured,
88
+ modelCount: account.modelCount,
89
+ oauthReady: account.oauthReady,
90
+ pendingLogin: account.pendingLogin,
91
+ availableRoutes: account.availableRoutes,
92
+ preferredRoute: account.preferredRoute,
93
+ activeRoute: account.activeRoute,
94
+ activeRouteReason: account.activeRouteReason,
95
+ authFreshness: account.authFreshness,
96
+ ...(account.fallbackRoute ? { fallbackRoute: account.fallbackRoute } : {}),
97
+ ...(account.fallbackRisk ? { fallbackRisk: account.fallbackRisk } : {}),
98
+ ...(account.expiresAt ? { expiresAt: new Date(account.expiresAt).toISOString() } : {}),
99
+ ...(account.tokenType ? { tokenType: account.tokenType } : {}),
100
+ issueCount: account.issues.length,
101
+ recommendedActionCount: account.recommendedActions.length,
102
+ ...(options.lookup ? { lookup: options.lookup } : {}),
103
+ ...(options.includeParameters ? {
104
+ routeRecords: account.routeRecords.map((route) => ({
105
+ route: route.route,
106
+ usable: route.usable,
107
+ freshness: route.freshness,
108
+ detail: route.detail,
109
+ issues: route.issues,
110
+ })),
111
+ usageWindows: account.usageWindows,
112
+ issues: account.issues,
113
+ notes: account.notes,
114
+ recommendedActions: account.recommendedActions,
115
+ policy: {
116
+ effect: 'read-only',
117
+ values: 'Provider account posture reports route and freshness metadata only; raw tokens, authorization codes, and secret values are never returned.',
118
+ mutation: 'Provider login, logout, subscription bundle export, and account repair actions stay explicit confirmation-gated workspace or slash-command flows.',
119
+ },
120
+ modelAccess: {
121
+ reviewCommand: '/accounts review',
122
+ showCommand: `/accounts show ${account.providerId}`,
123
+ routesCommand: `/accounts routes ${account.providerId}`,
124
+ repairCommand: `/accounts repair ${account.providerId}`,
125
+ subscriptionInspectCommand: `/subscription inspect ${account.providerId}`,
126
+ workspaceActions: [
127
+ 'provider-accounts',
128
+ 'provider-account-repair',
129
+ 'subscription-review',
130
+ 'subscription-inspect',
131
+ ],
132
+ loginStartCommand: `/subscription login ${account.providerId} start --yes`,
133
+ loginFinishCommand: `/subscription login ${account.providerId} finish <code-or-url> --yes`,
134
+ logoutCommand: `/subscription logout ${account.providerId} --yes`,
135
+ confirmationRequired: true,
136
+ },
137
+ } : {}),
138
+ };
139
+ }
140
+
141
+ export async function providerAccountCatalogStatus(context: CommandContext): Promise<Record<string, unknown>> {
142
+ const snapshot = await loadSnapshot(context);
143
+ return {
144
+ modes: ['provider_accounts', 'provider_account'],
145
+ providers: snapshot.providers.length,
146
+ configured: snapshot.configuredCount,
147
+ issues: snapshot.issueCount,
148
+ readOnly: true,
149
+ };
150
+ }
151
+
152
+ export async function providerAccountSummary(
153
+ context: CommandContext,
154
+ args: AgentHarnessProviderAccountArgs,
155
+ ): Promise<Record<string, unknown>> {
156
+ const snapshot = await loadSnapshot(context);
157
+ const query = readString(args.query).toLowerCase();
158
+ const includeParameters = args.includeParameters === true;
159
+ const accounts = snapshot.providers
160
+ .filter((account) => !query || accountSearchText(account).includes(query))
161
+ .slice(0, readLimit(args.limit, 100));
162
+ return {
163
+ capturedAt: new Date(snapshot.capturedAt).toISOString(),
164
+ providers: accounts.map((account) => describeAccount(account, { includeParameters })),
165
+ returned: accounts.length,
166
+ total: snapshot.providers.length,
167
+ configured: snapshot.configuredCount,
168
+ issues: snapshot.issueCount,
169
+ policy: 'Read-only provider account posture. Use confirmed workspace actions or slash-command mirrors for login/logout/bundle mutations.',
170
+ };
171
+ }
172
+
173
+ export async function describeHarnessProviderAccount(
174
+ context: CommandContext,
175
+ args: AgentHarnessProviderAccountArgs,
176
+ ): Promise<ProviderAccountResolution> {
177
+ const lookup = lookupFromArgs(args);
178
+ if (!lookup) {
179
+ return {
180
+ status: 'missing_lookup',
181
+ usage: 'provider_account requires providerId, target, or query. Use mode:"provider_accounts" to inspect provider ids.',
182
+ };
183
+ }
184
+ const snapshot = await loadSnapshot(context);
185
+ const normalized = lookup.input.toLowerCase();
186
+ const exact = snapshot.providers.find((account) => account.providerId === lookup.input);
187
+ if (exact) return { status: 'found', account: describeAccount(exact, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'id' } }) };
188
+ const insensitive = snapshot.providers.find((account) => account.providerId.toLowerCase() === normalized);
189
+ if (insensitive) return { status: 'found', account: describeAccount(insensitive, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
190
+ const searched = snapshot.providers.filter((account) => accountSearchText(account).includes(normalized));
191
+ if (searched.length === 1) {
192
+ return { status: 'found', account: describeAccount(searched[0]!, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'search' } }) };
193
+ }
194
+ if (searched.length > 1) {
195
+ return {
196
+ status: 'ambiguous',
197
+ input: lookup.input,
198
+ candidates: searched.slice(0, 8).map(describeCandidate),
199
+ };
200
+ }
201
+ return {
202
+ status: 'missing_lookup',
203
+ usage: `Unknown provider account ${lookup.input}. Use mode:"provider_accounts" to inspect provider ids.`,
204
+ };
205
+ }