@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,648 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { buildMcpAttackPathReview } from '@/runtime/index.ts';
3
+ import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
4
+ import type { CommandContext } from '../input/command-registry.ts';
5
+
6
+ export interface AgentHarnessSecurityArgs {
7
+ readonly query?: unknown;
8
+ readonly target?: unknown;
9
+ readonly findingId?: unknown;
10
+ readonly bundlePath?: unknown;
11
+ readonly includeParameters?: unknown;
12
+ readonly limit?: unknown;
13
+ }
14
+
15
+ type SecurityFindingSeverity = 'info' | 'warn' | 'blocker';
16
+ type SecurityFindingSource = 'token' | 'policy' | 'mcp' | 'plugin' | 'incident';
17
+
18
+ interface SecurityFinding {
19
+ readonly findingId: string;
20
+ readonly source: SecurityFindingSource;
21
+ readonly severity: SecurityFindingSeverity;
22
+ readonly title: string;
23
+ readonly summary: string;
24
+ readonly detail?: Record<string, unknown>;
25
+ readonly route: string;
26
+ }
27
+
28
+ interface SupportBundleRoute {
29
+ readonly bundleType: 'support' | 'auth' | 'trust' | 'subscription' | 'voice';
30
+ readonly defaultPath: string;
31
+ readonly inspectCommand: string;
32
+ readonly exportCommand: string;
33
+ readonly importCommand?: string;
34
+ readonly workspaceActionIds: readonly string[];
35
+ }
36
+
37
+ type SecurityFindingResolution =
38
+ | { readonly status: 'found'; readonly finding: Record<string, unknown> }
39
+ | { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
40
+ | { readonly status: 'missing_lookup'; readonly usage: string };
41
+
42
+ type SupportBundleResolution =
43
+ | { readonly status: 'found'; readonly bundle: Record<string, unknown> }
44
+ | { readonly status: 'missing_lookup'; readonly usage: string };
45
+
46
+ const SUPPORT_BUNDLE_ROUTES: readonly SupportBundleRoute[] = [
47
+ {
48
+ bundleType: 'support',
49
+ defaultPath: 'goodvibes-agent-bundle.json',
50
+ inspectCommand: '/bundle inspect <path>',
51
+ exportCommand: '/bundle export [path] --yes',
52
+ importCommand: '/bundle import <path> --yes',
53
+ workspaceActionIds: ['support-bundle-export', 'support-bundle-inspect', 'support-bundle-import'],
54
+ },
55
+ {
56
+ bundleType: 'auth',
57
+ defaultPath: 'auth-review-bundle.json',
58
+ inspectCommand: '/auth bundle inspect <path>',
59
+ exportCommand: '/auth bundle export <path> --yes',
60
+ workspaceActionIds: ['auth-bundle-export', 'auth-bundle-inspect'],
61
+ },
62
+ {
63
+ bundleType: 'trust',
64
+ defaultPath: 'trust-review-bundle.json',
65
+ inspectCommand: '/trust bundle inspect <path>',
66
+ exportCommand: '/trust bundle export <path> --yes',
67
+ workspaceActionIds: ['trust-bundle-export', 'trust-bundle-inspect'],
68
+ },
69
+ {
70
+ bundleType: 'subscription',
71
+ defaultPath: 'subscription-bundle.json',
72
+ inspectCommand: '/subscription bundle inspect <path>',
73
+ exportCommand: '/subscription bundle export <path> --yes',
74
+ workspaceActionIds: ['subscription-bundle-export', 'subscription-bundle-inspect'],
75
+ },
76
+ {
77
+ bundleType: 'voice',
78
+ defaultPath: 'voice-bundle.json',
79
+ inspectCommand: '/voice bundle inspect <path>',
80
+ exportCommand: '/voice bundle export <path> --yes',
81
+ workspaceActionIds: ['voice-bundle-export', 'voice-bundle-inspect'],
82
+ },
83
+ ];
84
+
85
+ function readString(value: unknown): string {
86
+ return typeof value === 'string' ? value.trim() : '';
87
+ }
88
+
89
+ function readLimit(value: unknown, fallback: number): number {
90
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
91
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
92
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
93
+ }
94
+
95
+ function isRecord(value: unknown): value is Record<string, unknown> {
96
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
97
+ }
98
+
99
+ function readRecordArray(value: unknown): readonly Record<string, unknown>[] {
100
+ return Array.isArray(value) ? value.filter(isRecord) : [];
101
+ }
102
+
103
+ function readArray(value: unknown): readonly unknown[] {
104
+ return Array.isArray(value) ? value : [];
105
+ }
106
+
107
+ function readNestedValue(source: unknown, path: string): unknown {
108
+ let cursor = source;
109
+ for (const part of path.split('.')) {
110
+ if (!isRecord(cursor)) return undefined;
111
+ cursor = cursor[part];
112
+ }
113
+ return cursor;
114
+ }
115
+
116
+ function countNestedLeaves(value: unknown): number {
117
+ if (value === null || value === undefined) return 0;
118
+ if (Array.isArray(value)) return value.reduce((total, entry) => total + countNestedLeaves(entry), 0);
119
+ if (typeof value === 'object') return Object.values(value).reduce((total, entry) => total + countNestedLeaves(entry), 0);
120
+ return 1;
121
+ }
122
+
123
+ function slug(input: string): string {
124
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'item';
125
+ }
126
+
127
+ function safeText(value: unknown, fallback = 'n/a'): string {
128
+ if (typeof value === 'string' && value.trim()) return value.trim();
129
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
130
+ return fallback;
131
+ }
132
+
133
+ function severityRank(severity: SecurityFindingSeverity): number {
134
+ if (severity === 'blocker') return 0;
135
+ if (severity === 'warn') return 1;
136
+ return 2;
137
+ }
138
+
139
+ function findingSearchText(finding: SecurityFinding): string {
140
+ return [
141
+ finding.findingId,
142
+ finding.source,
143
+ finding.severity,
144
+ finding.title,
145
+ finding.summary,
146
+ JSON.stringify(finding.detail ?? {}),
147
+ ].join('\n').toLowerCase();
148
+ }
149
+
150
+ function describeFindingCandidate(finding: SecurityFinding): Record<string, unknown> {
151
+ return {
152
+ findingId: finding.findingId,
153
+ source: finding.source,
154
+ severity: finding.severity,
155
+ title: finding.title,
156
+ };
157
+ }
158
+
159
+ function describeFinding(finding: SecurityFinding, includeParameters: boolean, lookup?: Record<string, unknown>): Record<string, unknown> {
160
+ return {
161
+ findingId: finding.findingId,
162
+ source: finding.source,
163
+ severity: finding.severity,
164
+ title: finding.title,
165
+ summary: finding.summary,
166
+ route: finding.route,
167
+ ...(lookup ? { lookup } : {}),
168
+ ...(includeParameters && finding.detail ? { detail: finding.detail } : {}),
169
+ ...(includeParameters ? {
170
+ policy: {
171
+ effect: 'read-only',
172
+ values: 'Security posture returns counts, labels, policy ids, route names, and redacted bundle summaries only; token values, secret values, and raw config values are never returned.',
173
+ mutation: 'Token rotation, policy changes, MCP trust changes, bundle export/import, auth repair, and voice enable/disable stay explicit confirmation-gated workspace or slash-command flows.',
174
+ },
175
+ } : {}),
176
+ };
177
+ }
178
+
179
+ function buildTokenFindings(securitySnapshot: Record<string, unknown> | null): readonly SecurityFinding[] {
180
+ const audit = isRecord(securitySnapshot?.audit) ? securitySnapshot.audit : null;
181
+ const results = readRecordArray(audit?.results);
182
+ return results
183
+ .map((result, index): SecurityFinding | null => {
184
+ const scope = isRecord(result.scope) ? result.scope : {};
185
+ const rotation = isRecord(result.rotation) ? result.rotation : {};
186
+ const label = safeText(result.label, `token-${index + 1}`);
187
+ const scopeOutcome = safeText(scope.outcome);
188
+ const rotationOutcome = safeText(rotation.outcome);
189
+ const blocked = result.blocked === true;
190
+ if (!blocked && scopeOutcome === 'ok' && rotationOutcome === 'ok') return null;
191
+ const severity: SecurityFindingSeverity = blocked || rotationOutcome === 'overdue' ? 'blocker' : 'warn';
192
+ return {
193
+ findingId: `token-${slug(label)}-${index + 1}`,
194
+ source: 'token',
195
+ severity,
196
+ title: `Token ${label}`,
197
+ summary: `Scope ${scopeOutcome}; rotation ${rotationOutcome}; blocked ${blocked ? 'yes' : 'no'}.`,
198
+ route: '/security tokens',
199
+ detail: {
200
+ tokenLabel: label,
201
+ policyId: safeText(scope.policyId),
202
+ scopeOutcome,
203
+ rotationOutcome,
204
+ blocked,
205
+ ...(typeof rotation.dueAt === 'number' ? { dueAt: new Date(rotation.dueAt).toISOString() } : {}),
206
+ },
207
+ };
208
+ })
209
+ .filter((finding): finding is SecurityFinding => finding !== null);
210
+ }
211
+
212
+ function buildPolicyFindings(context: CommandContext): readonly SecurityFinding[] {
213
+ const policySnapshot = context.extensions.policyRuntimeState?.getSnapshot();
214
+ if (!policySnapshot) return [];
215
+ const lintFindings = readRecordArray(policySnapshot.lintFindings);
216
+ const findings = lintFindings.map((finding, index): SecurityFinding => {
217
+ const level = safeText(finding.level ?? finding.severity, 'warn').toLowerCase();
218
+ const severity: SecurityFindingSeverity = level === 'error' || level === 'blocker' ? 'blocker' : level === 'info' ? 'info' : 'warn';
219
+ const rule = safeText(finding.ruleId ?? finding.rule ?? finding.code, `policy-${index + 1}`);
220
+ const message = safeText(finding.message ?? finding.summary ?? finding.reason, 'Policy lint finding');
221
+ return {
222
+ findingId: `policy-${slug(rule)}-${index + 1}`,
223
+ source: 'policy',
224
+ severity,
225
+ title: `Policy ${rule}`,
226
+ summary: message,
227
+ route: '/security review',
228
+ detail: {
229
+ rule,
230
+ message,
231
+ level,
232
+ },
233
+ };
234
+ });
235
+ const preflight = isRecord(policySnapshot.lastPreflightReview) ? policySnapshot.lastPreflightReview : null;
236
+ if (!preflight || safeText(preflight.status, 'ok') === 'ok') return findings;
237
+ return [
238
+ ...findings,
239
+ {
240
+ findingId: 'policy-preflight',
241
+ source: 'policy',
242
+ severity: safeText(preflight.status) === 'blocked' ? 'blocker' : 'warn',
243
+ title: 'Policy preflight',
244
+ summary: `Preflight ${safeText(preflight.status)} with ${safeText(preflight.issueCount, '0')} issue(s).`,
245
+ route: '/security review',
246
+ detail: {
247
+ status: safeText(preflight.status),
248
+ issueCount: typeof preflight.issueCount === 'number' ? preflight.issueCount : 0,
249
+ },
250
+ },
251
+ ];
252
+ }
253
+
254
+ function buildMcpFindings(securitySnapshot: Record<string, unknown> | null): readonly SecurityFinding[] {
255
+ const mcpServers = readRecordArray(securitySnapshot?.mcpServers);
256
+ const recentMcpDecisions = readArray(securitySnapshot?.recentMcpDecisions);
257
+ const attackPathReview = isRecord(securitySnapshot?.attackPathReview)
258
+ ? securitySnapshot.attackPathReview
259
+ : buildMcpAttackPathReview({ servers: mcpServers as never, recentDecisions: recentMcpDecisions as never });
260
+ const attackFindings = readRecordArray(attackPathReview.findings).map((finding, index): SecurityFinding => {
261
+ const serverName = safeText(finding.serverName, `server-${index + 1}`);
262
+ const severityRaw = safeText(finding.severity, 'warn').toLowerCase();
263
+ const severity: SecurityFindingSeverity = severityRaw === 'critical' || severityRaw === 'high' || severityRaw === 'blocker' ? 'blocker' : severityRaw === 'info' ? 'info' : 'warn';
264
+ return {
265
+ findingId: `mcp-attack-${slug(serverName)}-${index + 1}`,
266
+ source: 'mcp',
267
+ severity,
268
+ title: `MCP attack path ${serverName}`,
269
+ summary: safeText(finding.reason, 'MCP attack-path finding'),
270
+ route: '/security attack-paths',
271
+ detail: {
272
+ serverName,
273
+ route: safeText(finding.route),
274
+ reason: safeText(finding.reason),
275
+ severity: severityRaw,
276
+ },
277
+ };
278
+ });
279
+ const postureFindings = mcpServers.flatMap((server, index): readonly SecurityFinding[] => {
280
+ const serverName = safeText(server.name, `server-${index + 1}`);
281
+ const findings: SecurityFinding[] = [];
282
+ if (server.schemaFreshness === 'quarantined') {
283
+ findings.push({
284
+ findingId: `mcp-quarantined-${slug(serverName)}`,
285
+ source: 'mcp',
286
+ severity: 'blocker',
287
+ title: `MCP quarantined ${serverName}`,
288
+ summary: safeText(server.quarantineReason, 'Server schema is quarantined.'),
289
+ route: `/mcp repair ${serverName}`,
290
+ detail: {
291
+ serverName,
292
+ trustMode: safeText(server.trustMode),
293
+ role: safeText(server.role),
294
+ quarantineReason: safeText(server.quarantineReason),
295
+ quarantineDetail: safeText(server.quarantineDetail),
296
+ },
297
+ });
298
+ }
299
+ if (server.trustMode === 'allow-all') {
300
+ findings.push({
301
+ findingId: `mcp-allow-all-${slug(serverName)}`,
302
+ source: 'mcp',
303
+ severity: 'warn',
304
+ title: `MCP elevated trust ${serverName}`,
305
+ summary: 'Server is configured with allow-all trust.',
306
+ route: `/mcp review ${serverName}`,
307
+ detail: {
308
+ serverName,
309
+ trustMode: 'allow-all',
310
+ role: safeText(server.role),
311
+ },
312
+ });
313
+ }
314
+ return findings;
315
+ });
316
+ return [...attackFindings, ...postureFindings];
317
+ }
318
+
319
+ function buildPluginFindings(securitySnapshot: Record<string, unknown> | null): readonly SecurityFinding[] {
320
+ const plugins = readRecordArray(securitySnapshot?.plugins);
321
+ return plugins
322
+ .filter((plugin) => plugin.quarantined === true || plugin.trustTier === 'untrusted')
323
+ .map((plugin, index): SecurityFinding => {
324
+ const name = safeText(plugin.name, `plugin-${index + 1}`);
325
+ const quarantined = plugin.quarantined === true;
326
+ return {
327
+ findingId: `plugin-${quarantined ? 'quarantined' : 'untrusted'}-${slug(name)}`,
328
+ source: 'plugin',
329
+ severity: quarantined ? 'blocker' : 'warn',
330
+ title: `Plugin ${name}`,
331
+ summary: quarantined ? 'Plugin is quarantined.' : 'Plugin is untrusted.',
332
+ route: '/trust review',
333
+ detail: {
334
+ name,
335
+ version: safeText(plugin.version),
336
+ enabled: plugin.enabled === true,
337
+ active: plugin.active === true,
338
+ trustTier: safeText(plugin.trustTier),
339
+ quarantined,
340
+ },
341
+ };
342
+ });
343
+ }
344
+
345
+ function buildIncidentFindings(securitySnapshot: Record<string, unknown> | null): readonly SecurityFinding[] {
346
+ const incidents = readRecordArray(securitySnapshot?.incidents);
347
+ return incidents.slice(0, 20).map((incident, index): SecurityFinding => {
348
+ const id = safeText(incident.id, `incident-${index + 1}`);
349
+ return {
350
+ findingId: `incident-${slug(id)}`,
351
+ source: 'incident',
352
+ severity: 'warn',
353
+ title: `Security incident ${id}`,
354
+ summary: safeText(incident.title ?? incident.summary ?? incident.kind, 'Security incident recorded.'),
355
+ route: '/security review',
356
+ detail: {
357
+ id,
358
+ kind: safeText(incident.kind),
359
+ createdAt: safeText(incident.createdAt ?? incident.capturedAt),
360
+ },
361
+ };
362
+ });
363
+ }
364
+
365
+ function readSecuritySnapshot(context: CommandContext): Record<string, unknown> | null {
366
+ try {
367
+ const snapshot = context.platform.readModels?.security.getSnapshot();
368
+ return isRecord(snapshot) ? snapshot : null;
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+
374
+ function buildSecurityFindings(context: CommandContext): readonly SecurityFinding[] {
375
+ const securitySnapshot = readSecuritySnapshot(context);
376
+ return [
377
+ ...buildTokenFindings(securitySnapshot),
378
+ ...buildPolicyFindings(context),
379
+ ...buildMcpFindings(securitySnapshot),
380
+ ...buildPluginFindings(securitySnapshot),
381
+ ...buildIncidentFindings(securitySnapshot),
382
+ ].sort((a, b) => severityRank(a.severity) - severityRank(b.severity) || a.findingId.localeCompare(b.findingId));
383
+ }
384
+
385
+ function countByString(records: readonly Record<string, unknown>[], key: string, value: string): number {
386
+ return records.filter((record) => record[key] === value).length;
387
+ }
388
+
389
+ export function securityPostureCatalogStatus(context: CommandContext): Record<string, unknown> {
390
+ const securitySnapshot = readSecuritySnapshot(context);
391
+ return {
392
+ modes: ['security_posture', 'security_finding'],
393
+ status: securitySnapshot ? 'available' : 'unavailable',
394
+ findings: securitySnapshot ? buildSecurityFindings(context).length : 0,
395
+ readOnly: true,
396
+ };
397
+ }
398
+
399
+ export async function securityPostureSummary(context: CommandContext, args: AgentHarnessSecurityArgs): Promise<Record<string, unknown>> {
400
+ const securitySnapshot = readSecuritySnapshot(context);
401
+ if (!securitySnapshot) {
402
+ return {
403
+ status: 'unavailable',
404
+ findings: [],
405
+ returned: 0,
406
+ total: 0,
407
+ policy: 'Security read models are unavailable in this Agent context.',
408
+ };
409
+ }
410
+ const audit = isRecord(securitySnapshot.audit) ? securitySnapshot.audit : {};
411
+ const policy = isRecord(securitySnapshot.policy) ? securitySnapshot.policy : {};
412
+ const mcpServers = readRecordArray(securitySnapshot.mcpServers);
413
+ const plugins = readRecordArray(securitySnapshot.plugins);
414
+ const subscriptions = context.platform.subscriptionManager;
415
+ const services = context.platform.serviceRegistry;
416
+ const query = readString(args.query).toLowerCase();
417
+ const findings = buildSecurityFindings(context);
418
+ const filtered = findings
419
+ .filter((finding) => !query || findingSearchText(finding).includes(query))
420
+ .slice(0, readLimit(args.limit, 100));
421
+ let secretRefCount: number | null = null;
422
+ if (args.includeParameters === true && context.platform.secretsManager) {
423
+ try {
424
+ secretRefCount = (await context.platform.secretsManager.list()).length;
425
+ } catch {
426
+ secretRefCount = null;
427
+ }
428
+ }
429
+ return {
430
+ status: 'available',
431
+ summary: {
432
+ tokens: {
433
+ total: typeof audit.totalTokens === 'number' ? audit.totalTokens : readRecordArray(audit.results).length,
434
+ blocked: readArray(audit.blocked).length,
435
+ scopeViolations: readArray(audit.scopeViolations).length,
436
+ rotationWarnings: readArray(audit.rotationWarnings).length,
437
+ rotationOverdue: readArray(audit.rotationOverdue).length,
438
+ },
439
+ policy: {
440
+ preflightStatus: safeText(policy.preflightStatus),
441
+ preflightIssueCount: typeof policy.preflightIssueCount === 'number' ? policy.preflightIssueCount : 0,
442
+ lintFindings: typeof policy.lintFindingCount === 'number' ? policy.lintFindingCount : 0,
443
+ deniedPermissions: typeof securitySnapshot.deniedPermissions === 'number' ? securitySnapshot.deniedPermissions : 0,
444
+ },
445
+ mcp: {
446
+ servers: mcpServers.length,
447
+ quarantined: countByString(mcpServers, 'schemaFreshness', 'quarantined'),
448
+ elevated: countByString(mcpServers, 'trustMode', 'allow-all'),
449
+ attackPathFindings: readRecordArray(isRecord(securitySnapshot.attackPathReview) ? securitySnapshot.attackPathReview.findings : []).length,
450
+ },
451
+ plugins: {
452
+ total: plugins.length,
453
+ quarantined: plugins.filter((plugin) => plugin.quarantined === true).length,
454
+ untrusted: countByString(plugins, 'trustTier', 'untrusted'),
455
+ },
456
+ auth: {
457
+ builtInProviders: listBuiltinSubscriptionProviders().length,
458
+ activeSubscriptions: subscriptions?.list().length ?? 0,
459
+ pendingSubscriptions: subscriptions?.listPending().length ?? 0,
460
+ configuredServices: services ? Object.keys(services.getAll()).length : 0,
461
+ ...(secretRefCount !== null ? { secretRefs: secretRefCount } : {}),
462
+ },
463
+ },
464
+ findings: filtered.map((finding) => describeFinding(finding, args.includeParameters === true)),
465
+ returned: filtered.length,
466
+ total: findings.length,
467
+ policy: 'Read-only security posture. Token rotation, policy changes, MCP trust changes, bundle export/import, auth repair, and voice enable/disable remain confirmation-gated workspace or slash-command flows.',
468
+ ...(args.includeParameters === true ? { modelAccess: {
469
+ reviewCommand: '/security review',
470
+ tokensCommand: '/security tokens',
471
+ attackPathsCommand: '/security attack-paths',
472
+ trustReviewCommand: '/trust review',
473
+ authReviewCommand: '/auth review',
474
+ bundleCatalogMode: 'support_bundles',
475
+ singleFindingMode: 'security_finding',
476
+ } } : {}),
477
+ };
478
+ }
479
+
480
+ export function describeHarnessSecurityFinding(context: CommandContext, args: AgentHarnessSecurityArgs): SecurityFindingResolution {
481
+ const findingId = readString(args.findingId);
482
+ const target = readString(args.target);
483
+ const query = readString(args.query);
484
+ const input = findingId || target || query;
485
+ if (!input) {
486
+ return {
487
+ status: 'missing_lookup',
488
+ usage: 'security_finding requires findingId, target, or query. Use mode:"security_posture" to inspect current finding ids.',
489
+ };
490
+ }
491
+ const findings = buildSecurityFindings(context);
492
+ const exact = findings.find((finding) => finding.findingId === input);
493
+ if (exact) return { status: 'found', finding: describeFinding(exact, true, { source: findingId ? 'findingId' : target ? 'target' : 'query', input, resolvedBy: 'id' }) };
494
+ const normalized = input.toLowerCase();
495
+ const insensitive = findings.find((finding) => finding.findingId.toLowerCase() === normalized);
496
+ if (insensitive) return { status: 'found', finding: describeFinding(insensitive, true, { source: findingId ? 'findingId' : target ? 'target' : 'query', input, resolvedBy: 'case-insensitive-id' }) };
497
+ const searched = findings.filter((finding) => findingSearchText(finding).includes(normalized));
498
+ if (searched.length === 1) return { status: 'found', finding: describeFinding(searched[0]!, true, { source: findingId ? 'findingId' : target ? 'target' : 'query', input, resolvedBy: 'search' }) };
499
+ if (searched.length > 1) {
500
+ return {
501
+ status: 'ambiguous',
502
+ input,
503
+ candidates: searched.slice(0, 8).map(describeFindingCandidate),
504
+ };
505
+ }
506
+ return {
507
+ status: 'missing_lookup',
508
+ usage: `Unknown security finding ${input}. Use mode:"security_posture" to inspect current finding ids.`,
509
+ };
510
+ }
511
+
512
+ function bundleRouteSearchText(route: typeof SUPPORT_BUNDLE_ROUTES[number]): string {
513
+ return [
514
+ route.bundleType,
515
+ route.defaultPath,
516
+ route.inspectCommand,
517
+ route.exportCommand,
518
+ route.importCommand ?? '',
519
+ ...route.workspaceActionIds,
520
+ ].join('\n').toLowerCase();
521
+ }
522
+
523
+ export function supportBundleCatalogStatus(): Record<string, unknown> {
524
+ return {
525
+ modes: ['support_bundles', 'support_bundle'],
526
+ bundleTypes: SUPPORT_BUNDLE_ROUTES.length,
527
+ readOnlyInspection: true,
528
+ };
529
+ }
530
+
531
+ export function supportBundleSummary(args: AgentHarnessSecurityArgs): Record<string, unknown> {
532
+ const query = readString(args.query).toLowerCase();
533
+ const routes = SUPPORT_BUNDLE_ROUTES
534
+ .filter((route) => !query || bundleRouteSearchText(route).includes(query))
535
+ .slice(0, readLimit(args.limit, 100))
536
+ .map((route) => ({
537
+ bundleType: route.bundleType,
538
+ defaultPath: route.defaultPath,
539
+ inspectCommand: route.inspectCommand,
540
+ exportCommand: route.exportCommand,
541
+ ...(route.importCommand ? { importCommand: route.importCommand } : {}),
542
+ workspaceActionIds: route.workspaceActionIds,
543
+ ...(args.includeParameters === true ? {
544
+ parameters: {
545
+ bundlePath: 'Workspace-relative path for support_bundle inspection.',
546
+ confirm: 'Required for export/import routes through run_workspace_action or run_command.',
547
+ },
548
+ } : {}),
549
+ }));
550
+ return {
551
+ bundles: routes,
552
+ returned: routes.length,
553
+ total: SUPPORT_BUNDLE_ROUTES.length,
554
+ policy: 'Bundle catalog is read-only. Existing bundle inspection returns counts and redaction metadata only; bundle export/import stays confirmation-gated through workspace actions or slash-command mirrors.',
555
+ };
556
+ }
557
+
558
+ function resolveBundlePath(context: CommandContext, args: AgentHarnessSecurityArgs): string {
559
+ const raw = readString(args.bundlePath) || readString(args.target) || readString(args.query);
560
+ if (!raw) return '';
561
+ return context.workspace.shellPaths?.resolveWorkspacePath(raw) ?? raw;
562
+ }
563
+
564
+ function summarizeBundleType(bundle: Record<string, unknown>): string {
565
+ const type = readString(bundle.type);
566
+ if (type) return type;
567
+ if ('permissionMode' in bundle && 'mcpSummary' in bundle) return 'trust-review';
568
+ if ('externalRuntimeAuth' in bundle) return 'auth-review';
569
+ if ('subscriptions' in bundle && 'provider' in bundle) return 'subscription';
570
+ if ('voice' in bundle || 'tts' in bundle) return 'voice';
571
+ return 'unknown';
572
+ }
573
+
574
+ function timestampForBundle(bundle: Record<string, unknown>): number | null {
575
+ const value = bundle.capturedAt ?? bundle.exportedAt ?? bundle.createdAt;
576
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
577
+ }
578
+
579
+ function summarizeSupportBundle(path: string, bundle: Record<string, unknown>): Record<string, unknown> {
580
+ const redaction = isRecord(bundle.redaction) ? bundle.redaction : {};
581
+ const diagnostics = isRecord(bundle.diagnostics) ? bundle.diagnostics : {};
582
+ const timestamp = timestampForBundle(bundle);
583
+ const mcpSummary = isRecord(bundle.mcpSummary) ? bundle.mcpSummary : {};
584
+ const pluginSummary = isRecord(bundle.pluginSummary) ? bundle.pluginSummary : {};
585
+ return {
586
+ path,
587
+ type: summarizeBundleType(bundle),
588
+ version: safeText(bundle.version),
589
+ capturedAt: timestamp === null ? null : new Date(timestamp).toISOString(),
590
+ topLevelSections: Object.keys(bundle).filter((key) => key !== 'config' && key !== 'secrets'),
591
+ counts: {
592
+ configValues: countNestedLeaves(bundle.config),
593
+ redactedConfigPaths: readArray(redaction.redactedConfigPaths).length,
594
+ diagnosticSections: Object.keys(diagnostics).length,
595
+ secretRefs: readArray(bundle.secretKeys).length,
596
+ serviceNames: readArray(bundle.serviceNames).length,
597
+ activeSubscriptions: readArray(bundle.activeSubscriptions).length,
598
+ pendingSubscriptions: readArray(bundle.pendingSubscriptions).length,
599
+ providerSnapshots: readArray(readNestedValue(bundle, 'diagnostics.providers')).length,
600
+ mcpServers: typeof mcpSummary.total === 'number' ? mcpSummary.total : 0,
601
+ plugins: typeof pluginSummary.total === 'number' ? pluginSummary.total : 0,
602
+ },
603
+ redaction: {
604
+ sentinelPresent: typeof redaction.sentinel === 'string',
605
+ hasRedactedConfigPaths: readArray(redaction.redactedConfigPaths).length > 0,
606
+ },
607
+ policy: {
608
+ effect: 'read-only',
609
+ values: 'Bundle inspection returns structure, counts, timestamps, and redaction metadata only. Raw config values, token values, and secret payloads are not returned.',
610
+ mutation: 'Bundle export/import remains confirmation-gated through run_workspace_action or run_command.',
611
+ },
612
+ };
613
+ }
614
+
615
+ export function describeHarnessSupportBundle(context: CommandContext, args: AgentHarnessSecurityArgs): SupportBundleResolution {
616
+ const path = resolveBundlePath(context, args);
617
+ if (!path) {
618
+ return {
619
+ status: 'missing_lookup',
620
+ usage: 'support_bundle requires bundlePath, target, or query containing a workspace-relative bundle path. Use mode:"support_bundles" to inspect bundle routes.',
621
+ };
622
+ }
623
+ if (!existsSync(path)) {
624
+ return {
625
+ status: 'missing_lookup',
626
+ usage: `Bundle file not found: ${path}. Use mode:"support_bundles" to inspect bundle export and inspect routes.`,
627
+ };
628
+ }
629
+ let parsed: unknown;
630
+ try {
631
+ parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
632
+ } catch (error) {
633
+ return {
634
+ status: 'missing_lookup',
635
+ usage: `Invalid bundle JSON at ${path}: ${error instanceof Error ? error.message : String(error)}`,
636
+ };
637
+ }
638
+ if (!isRecord(parsed)) {
639
+ return {
640
+ status: 'missing_lookup',
641
+ usage: `Invalid bundle JSON at ${path}: expected a JSON object.`,
642
+ };
643
+ }
644
+ return {
645
+ status: 'found',
646
+ bundle: summarizeSupportBundle(path, parsed),
647
+ };
648
+ }