@pellux/goodvibes-agent 0.1.81 → 0.1.83
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 +8 -0
- package/package.json +1 -1
- package/src/cli/help.ts +4 -4
- package/src/cli/management.ts +54 -136
- package/src/cli/status.ts +10 -10
- package/src/input/commands/health-runtime.ts +12 -14
- package/src/input/commands/platform-access-runtime.ts +8 -4
- package/src/input/commands.ts +0 -2
- package/src/input/onboarding/onboarding-wizard-helpers.ts +1 -1
- package/src/input/settings-modal-agent-policy.ts +17 -1
- package/src/input/settings-modal-types.ts +3 -5
- package/src/panels/builtin/operations.ts +0 -10
- package/src/panels/provider-health-domains.ts +9 -8
- package/src/runtime/onboarding/apply.ts +6 -154
- package/src/runtime/onboarding/derivation.ts +4 -4
- package/src/runtime/onboarding/types.ts +1 -11
- package/src/runtime/onboarding/verify.ts +3 -25
- package/src/version.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +0 -128
- package/src/panels/local-auth-panel.ts +0 -130
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.83 - 2026-06-01
|
|
6
|
+
|
|
7
|
+
- f5099c1 Hide runtime-owned settings from Agent workspace
|
|
8
|
+
|
|
9
|
+
## 0.1.82 - 2026-06-01
|
|
10
|
+
|
|
11
|
+
- 3185531 Remove Agent local auth ownership paths
|
|
12
|
+
|
|
5
13
|
## 0.1.81 - 2026-06-01
|
|
6
14
|
|
|
7
15
|
- Added local Agent skill bundles so users can group related local skills, enable or disable the bundle, and inject the bundle's member procedures into the same serial assistant conversation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.83",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
|
|
6
6
|
"type": "module",
|
package/src/cli/help.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
|
|
|
38
38
|
' providers List/inspect/use provider config/auth posture',
|
|
39
39
|
' profiles Manage isolated Agent profile homes',
|
|
40
40
|
' routines Inspect local routines and explicitly promote one to an external schedule',
|
|
41
|
-
' auth Inspect
|
|
41
|
+
' auth Inspect Agent auth posture and external runtime token state',
|
|
42
42
|
' compat Inspect Agent SDK pin, runtime version, and Agent knowledge route readiness',
|
|
43
43
|
' knowledge Use isolated Agent Knowledge/Wiki routes',
|
|
44
44
|
' ask|search Shortcuts for isolated Agent Knowledge ask/search',
|
|
@@ -170,9 +170,9 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
170
170
|
examples: ['models current', 'models openai', 'models use openai:gpt-5.4'],
|
|
171
171
|
},
|
|
172
172
|
auth: {
|
|
173
|
-
usage: ['auth
|
|
174
|
-
summary: 'Inspect and
|
|
175
|
-
examples: ['auth', 'auth
|
|
173
|
+
usage: ['auth', 'auth status', 'auth review', 'auth users', 'auth sessions'],
|
|
174
|
+
summary: 'Inspect Agent auth posture and external runtime token state. Runtime user/session administration belongs to the runtime-owning TUI or host tooling.',
|
|
175
|
+
examples: ['auth', 'auth status', 'auth users'],
|
|
176
176
|
},
|
|
177
177
|
compat: {
|
|
178
178
|
usage: ['compat', 'compat --json'],
|
package/src/cli/management.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
@@ -21,7 +21,6 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
|
|
|
21
21
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
22
22
|
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
23
23
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
24
|
-
import { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
25
24
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
26
25
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
27
26
|
import { classifyProviderSetup } from './provider-classification.ts';
|
|
@@ -60,7 +59,7 @@ export function formatJsonOrText(cli: GoodVibesCliParseResult): Formatter {
|
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
function exitCodeForText(output: string): number {
|
|
63
|
-
if (output.startsWith('Usage:') || output.startsWith('Invalid ')) return 2;
|
|
62
|
+
if (output.startsWith('Usage:') || output.startsWith('Invalid ') || output.startsWith('Unsupported:')) return 2;
|
|
64
63
|
if (output.startsWith('Session not found:') || output.startsWith('Unknown task:') || output.startsWith('Task submit failed ')) return 1;
|
|
65
64
|
if (output.startsWith('No stored ') || output.startsWith('No pending ') || output.startsWith('No model ') || output.startsWith('No provider ') || output.startsWith('No auth ')) return 1;
|
|
66
65
|
if (output.startsWith('Unknown ')) return 1;
|
|
@@ -74,59 +73,10 @@ function splitCommandOption(token: string): { readonly name: string; readonly va
|
|
|
74
73
|
return { name: token.slice(0, index), value: token.slice(index + 1) };
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function readOptionValue(args: readonly string[], name: string): string | undefined {
|
|
78
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
79
|
-
const token = args[index]!;
|
|
80
|
-
const split = splitCommandOption(token);
|
|
81
|
-
if (split.name !== name) continue;
|
|
82
|
-
if (split.value !== undefined) return split.value;
|
|
83
|
-
const next = args[index + 1];
|
|
84
|
-
if (next === undefined || next.startsWith('--')) return undefined;
|
|
85
|
-
return next;
|
|
86
|
-
}
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function readOptionValues(args: readonly string[], name: string): string[] {
|
|
91
|
-
const values: string[] = [];
|
|
92
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
93
|
-
const token = args[index]!;
|
|
94
|
-
const split = splitCommandOption(token);
|
|
95
|
-
if (split.name !== name) continue;
|
|
96
|
-
if (split.value !== undefined) {
|
|
97
|
-
values.push(split.value);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const next = args[index + 1];
|
|
101
|
-
if (next !== undefined && !next.startsWith('--')) values.push(next);
|
|
102
|
-
}
|
|
103
|
-
return values;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
76
|
export function hasCommandFlag(args: readonly string[], name: string): boolean {
|
|
107
77
|
return args.some((arg) => splitCommandOption(arg).name === name);
|
|
108
78
|
}
|
|
109
79
|
|
|
110
|
-
function commandValues(args: readonly string[]): string[] {
|
|
111
|
-
const values: string[] = [];
|
|
112
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
113
|
-
const token = args[index]!;
|
|
114
|
-
if (!token.startsWith('--')) {
|
|
115
|
-
values.push(token);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (!token.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1;
|
|
119
|
-
}
|
|
120
|
-
return values;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function readPassword(args: readonly string[]): string | null {
|
|
124
|
-
const explicit = readOptionValue(args, '--password');
|
|
125
|
-
if (explicit !== undefined) return explicit;
|
|
126
|
-
if (hasCommandFlag(args, '--password-stdin')) return readFileSync(0, 'utf-8').trimEnd();
|
|
127
|
-
return process.env.GOODVIBES_AUTH_PASSWORD ?? null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
80
|
export function extractAuthorizationCode(input: string): string {
|
|
131
81
|
try {
|
|
132
82
|
const url = new URL(input);
|
|
@@ -291,14 +241,6 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
|
291
241
|
};
|
|
292
242
|
}
|
|
293
243
|
|
|
294
|
-
function createCliLocalUserAuthManager(runtime: CliCommandRuntime): UserAuthManager {
|
|
295
|
-
const paths = readAuthPaths(runtime);
|
|
296
|
-
return new UserAuthManager({
|
|
297
|
-
bootstrapFilePath: paths.userStorePath,
|
|
298
|
-
bootstrapCredentialPath: paths.bootstrapCredentialPath,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
244
|
export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
|
|
303
245
|
const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
|
|
304
246
|
if (!prompt) {
|
|
@@ -584,84 +526,60 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
584
526
|
}
|
|
585
527
|
|
|
586
528
|
async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (sub === 'revoke-sessions') {
|
|
622
|
-
const username = rest[0];
|
|
623
|
-
if (!username) return 'Usage: goodvibes auth revoke-sessions <username>';
|
|
624
|
-
const count = localUserAuthManager.revokeSessionsForUser(username);
|
|
625
|
-
return `Auth sessions revoked for ${username}: ${count}`;
|
|
626
|
-
}
|
|
627
|
-
if (sub === 'clear-bootstrap') {
|
|
628
|
-
return localUserAuthManager.clearBootstrapCredentialFile()
|
|
629
|
-
? 'Bootstrap credential file removed.'
|
|
630
|
-
: 'Bootstrap credential file was already absent.';
|
|
631
|
-
}
|
|
632
|
-
if (sub !== 'status' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
|
|
633
|
-
return 'Usage: goodvibes auth [status|users|sessions|add-user|delete-user|rotate-password|revoke-session|revoke-sessions|clear-bootstrap]';
|
|
634
|
-
}
|
|
635
|
-
const snapshot = localUserAuthManager.inspect();
|
|
636
|
-
const paths = readAuthPaths(runtime);
|
|
637
|
-
const value = {
|
|
638
|
-
...paths,
|
|
639
|
-
users: snapshot.users.map((user) => ({ username: user.username, roles: user.roles })),
|
|
640
|
-
sessions: snapshot.sessions.length,
|
|
641
|
-
permissionMode: runtime.configManager.get('permissions.mode'),
|
|
642
|
-
};
|
|
643
|
-
if (sub === 'users') {
|
|
644
|
-
return formatJsonOrText(runtime.cli)(value.users, [
|
|
645
|
-
`GoodVibes auth users (${value.users.length})`,
|
|
646
|
-
...value.users.map((user) => ` ${user.username} (${user.roles.join(', ') || 'no roles'})`),
|
|
647
|
-
].join('\n'));
|
|
648
|
-
}
|
|
649
|
-
if (sub === 'sessions') {
|
|
650
|
-
return formatJsonOrText(runtime.cli)(snapshot.sessions, [
|
|
651
|
-
`GoodVibes auth sessions (${snapshot.sessions.length})`,
|
|
652
|
-
...snapshot.sessions.map((session) => ` ${session.username} expires=${new Date(session.expiresAt).toISOString()} fingerprint=${session.tokenFingerprint}`),
|
|
653
|
-
].join('\n'));
|
|
654
|
-
}
|
|
529
|
+
const [sub = 'status'] = runtime.cli.commandArgs;
|
|
530
|
+
const blocked = new Set([
|
|
531
|
+
'add-user',
|
|
532
|
+
'add',
|
|
533
|
+
'delete-user',
|
|
534
|
+
'remove-user',
|
|
535
|
+
'rotate-password',
|
|
536
|
+
'passwd',
|
|
537
|
+
'revoke-session',
|
|
538
|
+
'revoke-sessions',
|
|
539
|
+
'clear-bootstrap',
|
|
540
|
+
]);
|
|
541
|
+
if (blocked.has(sub)) {
|
|
542
|
+
return [
|
|
543
|
+
'Unsupported: runtime auth user/session administration is external to GoodVibes Agent.',
|
|
544
|
+
'GoodVibes Agent connects to an already-running runtime and does not create, delete, rotate, revoke, or clear runtime users, sessions, or bootstrap credentials.',
|
|
545
|
+
'Use the runtime-owning GoodVibes TUI or host tooling for runtime auth administration.',
|
|
546
|
+
].join('\n');
|
|
547
|
+
}
|
|
548
|
+
if (sub !== 'status' && sub !== 'review' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
|
|
549
|
+
return 'Usage: goodvibes-agent auth [status|review|users|sessions]';
|
|
550
|
+
}
|
|
551
|
+
const paths = readAuthPaths(runtime);
|
|
552
|
+
const value = {
|
|
553
|
+
authOwner: 'external-runtime',
|
|
554
|
+
operatorTokenPresent: paths.operatorTokenPresent,
|
|
555
|
+
operatorTokenPath: paths.operatorTokenPath,
|
|
556
|
+
compatibilityUserStorePresent: paths.userStorePresent,
|
|
557
|
+
compatibilityUserStorePath: paths.userStorePath,
|
|
558
|
+
compatibilityBootstrapCredentialPresent: paths.bootstrapCredentialPresent,
|
|
559
|
+
compatibilityBootstrapCredentialPath: paths.bootstrapCredentialPath,
|
|
560
|
+
permissionMode: runtime.configManager.get('permissions.mode'),
|
|
561
|
+
};
|
|
562
|
+
if (sub === 'users' || sub === 'sessions') {
|
|
655
563
|
return formatJsonOrText(runtime.cli)(value, [
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
`
|
|
659
|
-
|
|
660
|
-
`
|
|
661
|
-
|
|
662
|
-
` bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
|
|
663
|
-
` operator tokens: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
|
|
564
|
+
`GoodVibes Agent auth ${sub}`,
|
|
565
|
+
' owner: external GoodVibes runtime',
|
|
566
|
+
` operator token: ${paths.operatorTokenPresent ? 'present' : 'missing'}`,
|
|
567
|
+
` operator token path: ${paths.operatorTokenPath}`,
|
|
568
|
+
` ${sub}: managed by the runtime-owning TUI or host tooling`,
|
|
569
|
+
' Agent does not enumerate or mutate runtime users/sessions from the local CLI.',
|
|
664
570
|
].join('\n'));
|
|
571
|
+
}
|
|
572
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
573
|
+
'GoodVibes Agent auth',
|
|
574
|
+
' owner: external GoodVibes runtime',
|
|
575
|
+
` permission mode: ${String(value.permissionMode)}`,
|
|
576
|
+
` operator token: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
|
|
577
|
+
` compatibility user store: ${paths.userStorePresent ? 'present' : 'missing'} (${paths.userStorePath})`,
|
|
578
|
+
` compatibility bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
|
|
579
|
+
' runtime user/session administration: external',
|
|
580
|
+
' next: goodvibes-agent providers',
|
|
581
|
+
' next: goodvibes-agent subscription providers',
|
|
582
|
+
].join('\n'));
|
|
665
583
|
}
|
|
666
584
|
|
|
667
585
|
|
package/src/cli/status.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface CliStatusSnapshot {
|
|
|
50
50
|
readonly permissionLabel: string;
|
|
51
51
|
readonly secretPolicy: unknown;
|
|
52
52
|
readonly secretPolicyLabel: string;
|
|
53
|
-
readonly
|
|
53
|
+
readonly runtimeAuthSignal: CliAuthStatus | null;
|
|
54
54
|
};
|
|
55
55
|
readonly runtimeConnection: {
|
|
56
56
|
readonly enabled: unknown;
|
|
@@ -182,13 +182,13 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
182
182
|
|
|
183
183
|
if (networkFacingSurfaces.length > 0 && options.auth?.userStorePresent !== true) {
|
|
184
184
|
findings.push({
|
|
185
|
-
id: 'network-endpoint-without-
|
|
185
|
+
id: 'network-endpoint-without-runtime-auth-signal',
|
|
186
186
|
area: 'auth',
|
|
187
187
|
severity: 'risk',
|
|
188
|
-
summary: 'Network-facing runtime endpoints are enabled
|
|
189
|
-
cause: `${networkFacingSurfaces.map(([name]) => name).join(', ')} are LAN/custom-bound, but
|
|
190
|
-
impact: 'Remote access paths may be unusable or unsafe
|
|
191
|
-
action: '
|
|
188
|
+
summary: 'Network-facing runtime endpoints are enabled without a visible runtime auth signal.',
|
|
189
|
+
cause: `${networkFacingSurfaces.map(([name]) => name).join(', ')} are LAN/custom-bound, but Agent cannot see runtime auth state from its local compatibility files.`,
|
|
190
|
+
impact: 'Remote access paths may be unusable or unsafe unless the external runtime owner configured auth.',
|
|
191
|
+
action: 'Review runtime auth from the owning GoodVibes TUI or host tooling; Agent will not create local runtime users.',
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -200,7 +200,7 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
200
200
|
summary: 'A bootstrap credential is still present while network-facing surfaces are enabled.',
|
|
201
201
|
cause: `${networkFacingSurfaces.map(([name]) => name).join(', ')} are LAN/custom-bound and auth-bootstrap.txt exists.`,
|
|
202
202
|
impact: 'Bootstrap credentials should be treated as temporary setup material, not long-lived network access credentials.',
|
|
203
|
-
action: '
|
|
203
|
+
action: 'Use the runtime-owning GoodVibes TUI or host tooling to replace bootstrap auth and retire the bootstrap credential.',
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -264,7 +264,7 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
|
|
|
264
264
|
permissionLabel: permissionModeLabel(config.get('permissions.mode')),
|
|
265
265
|
secretPolicy: config.get('storage.secretPolicy'),
|
|
266
266
|
secretPolicyLabel: secretPolicyLabel(config.get('storage.secretPolicy')),
|
|
267
|
-
|
|
267
|
+
runtimeAuthSignal: options.auth ?? null,
|
|
268
268
|
},
|
|
269
269
|
runtimeConnection: {
|
|
270
270
|
enabled: config.get('service.enabled'),
|
|
@@ -317,8 +317,8 @@ export function renderCliStatus(options: CliStatusOptions): string {
|
|
|
317
317
|
` permissions: ${permissionModeLabel(config.get('permissions.mode'))} (${String(config.get('permissions.mode'))})`,
|
|
318
318
|
` secretPolicy: ${secretPolicyLabel(config.get('storage.secretPolicy'))} (${String(config.get('storage.secretPolicy'))})`,
|
|
319
319
|
options.auth
|
|
320
|
-
? `
|
|
321
|
-
: '
|
|
320
|
+
? ` runtimeAuthSignal: ${options.auth.userStorePresent ? 'present' : 'missing'} (${options.auth.userStorePath})`
|
|
321
|
+
: ' runtimeAuthSignal: unknown',
|
|
322
322
|
options.auth
|
|
323
323
|
? ` bootstrapCredential: ${options.auth.bootstrapCredentialPresent ? 'present' : 'missing'} (${options.auth.bootstrapCredentialPath})`
|
|
324
324
|
: ' bootstrapCredential: unknown',
|
|
@@ -8,7 +8,6 @@ import { getSettingsControlPlaneSnapshot } from '@/runtime/index.ts';
|
|
|
8
8
|
import { checkRecoveryFile, readLastSessionPointer } from '@/runtime/index.ts';
|
|
9
9
|
import {
|
|
10
10
|
openCommandPanel,
|
|
11
|
-
requireLocalUserAuthManager,
|
|
12
11
|
requireOperatorClient,
|
|
13
12
|
requireProviderApi,
|
|
14
13
|
requireReadModels,
|
|
@@ -78,12 +77,13 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
78
77
|
if (sub === 'auth') {
|
|
79
78
|
const auth = readModels.localAuth.getSnapshot();
|
|
80
79
|
ctx.print([
|
|
81
|
-
'Health Review:
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
`
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
'Health Review: Runtime Auth',
|
|
81
|
+
' owner: external GoodVibes runtime host',
|
|
82
|
+
` compatibility users visible: ${auth.userCount}`,
|
|
83
|
+
` compatibility sessions visible: ${auth.sessionCount}`,
|
|
84
|
+
` bootstrap file signal: ${auth.bootstrapCredentialPresent ? 'present' : 'cleared'}`,
|
|
85
|
+
' Agent action: review provider/subscription auth only; do not mutate runtime auth users or bootstrap credentials.',
|
|
86
|
+
...(auth.bootstrapCredentialPresent ? [' issue: bootstrap cleanup belongs to the runtime-owning TUI or host tooling'] : []),
|
|
87
87
|
].join('\n'));
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
@@ -222,13 +222,11 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
222
222
|
));
|
|
223
223
|
lines.push(' verify: /health settings');
|
|
224
224
|
} else if (domain === 'auth') {
|
|
225
|
-
const auth = requireLocalUserAuthManager(ctx).inspect();
|
|
226
225
|
lines.push(' domain: auth');
|
|
227
|
-
lines.push(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
));
|
|
226
|
+
lines.push(' /auth review');
|
|
227
|
+
lines.push(' /providers');
|
|
228
|
+
lines.push(' /subscription providers');
|
|
229
|
+
lines.push(' runtime auth users/bootstrap cleanup: use the runtime-owning GoodVibes TUI or host tooling');
|
|
232
230
|
lines.push(' verify: /health auth');
|
|
233
231
|
} else if (domain === 'accounts') {
|
|
234
232
|
lines.push(' domain: accounts');
|
|
@@ -316,7 +314,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
316
314
|
` account issues: ${accountSnapshot.issueCount}`,
|
|
317
315
|
` settings conflicts: ${settingsSnapshot.conflicts.length}`,
|
|
318
316
|
` managed locks: ${settingsSnapshot.managedLockCount}`,
|
|
319
|
-
`
|
|
317
|
+
` runtime auth owner: external`,
|
|
320
318
|
` remote workers: ${snapshot.remoteRunnerCount}`,
|
|
321
319
|
...formatSessionMaintenanceLines(maintenance, 'guided').map((line) => ` ${line}`),
|
|
322
320
|
...(snapshot.issues.length > 0 ? ['', ...snapshot.issues.map((issue) => ` [${issue.severity.toUpperCase()}] ${issue.area}: ${issue.message}`)] : []),
|
|
@@ -2,7 +2,6 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
4
4
|
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
5
|
-
import { handleLocalAuthCommand } from './local-auth-runtime.ts';
|
|
6
5
|
import { buildAuthInspectionSnapshot, inspectProviderAuth } from '@/runtime/index.ts';
|
|
7
6
|
import { requireSecretsManager, requireServiceRegistry, requireShellPaths, requireSubscriptionManager } from './runtime-services.ts';
|
|
8
7
|
import { requireYesFlag, stripYesFlag } from './confirmation.ts';
|
|
@@ -113,7 +112,7 @@ export function registerPlatformAccessRuntimeCommands(registry: CommandRegistry)
|
|
|
113
112
|
registry.register({
|
|
114
113
|
name: 'auth',
|
|
115
114
|
description: 'Review auth posture and exchange session login tokens with local services',
|
|
116
|
-
usage: '[review|show <provider>|repair <provider>|bundle export <path> --yes|bundle inspect <path>|login <runtime|listener> <baseUrl> <username> <password> [secretKey] --yes
|
|
115
|
+
usage: '[review|show <provider>|repair <provider>|bundle export <path> --yes|bundle inspect <path>|login <runtime|listener> <baseUrl> <username> <password> [secretKey] --yes]',
|
|
117
116
|
async handler(args, ctx) {
|
|
118
117
|
const parsed = stripYesFlag(args);
|
|
119
118
|
const commandArgs = [...parsed.rest];
|
|
@@ -123,7 +122,12 @@ export function registerPlatformAccessRuntimeCommands(registry: CommandRegistry)
|
|
|
123
122
|
const serviceRegistry = requireServiceRegistry(ctx);
|
|
124
123
|
const secretsManager = requireSecretsManager(ctx);
|
|
125
124
|
if (sub === 'local') {
|
|
126
|
-
|
|
125
|
+
ctx.print([
|
|
126
|
+
'Local runtime auth management is external to GoodVibes Agent.',
|
|
127
|
+
'Agent connects to an already-running GoodVibes runtime and does not create, delete, rotate, revoke, or clear runtime auth users, sessions, or bootstrap credentials.',
|
|
128
|
+
'Use the runtime-owning GoodVibes TUI or host tooling for runtime auth administration.',
|
|
129
|
+
'Agent auth commands available here: /auth review, /auth show <provider>, /auth repair <provider>, /auth login <runtime|listener> ... --yes.',
|
|
130
|
+
].join('\n'));
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
if (sub === 'review') {
|
|
@@ -270,7 +274,7 @@ export function registerPlatformAccessRuntimeCommands(registry: CommandRegistry)
|
|
|
270
274
|
return;
|
|
271
275
|
}
|
|
272
276
|
|
|
273
|
-
ctx.print('Usage: /auth [review|show <provider>|bundle export <path> --yes|bundle inspect <path>|login <runtime|listener> <baseUrl> <username> <password> [secretKey] --yes
|
|
277
|
+
ctx.print('Usage: /auth [review|show <provider>|bundle export <path> --yes|bundle inspect <path>|login <runtime|listener> <baseUrl> <username> <password> [secretKey] --yes]');
|
|
274
278
|
},
|
|
275
279
|
});
|
|
276
280
|
}
|
package/src/input/commands.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { registerTasksRuntimeCommands } from './commands/tasks-runtime.ts';
|
|
|
23
23
|
import { registerLocalProviderRuntimeCommands } from './commands/local-provider-runtime.ts';
|
|
24
24
|
import { registerHealthRuntimeCommands } from './commands/health-runtime.ts';
|
|
25
25
|
import { registerProviderAccountsRuntimeCommands } from './commands/provider-accounts-runtime.ts';
|
|
26
|
-
import { registerLocalAuthRuntimeCommands } from './commands/local-auth-runtime.ts';
|
|
27
26
|
import { registerConversationRuntimeCommands } from './commands/conversation-runtime.ts';
|
|
28
27
|
import { registerQrcodeRuntimeCommands } from './commands/qrcode-runtime.ts';
|
|
29
28
|
import { registerOnboardingRuntimeCommands } from './commands/onboarding-runtime.ts';
|
|
@@ -62,7 +61,6 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|
|
62
61
|
registerLocalProviderRuntimeCommands(registry);
|
|
63
62
|
registerHealthRuntimeCommands(registry);
|
|
64
63
|
registerProviderAccountsRuntimeCommands(registry);
|
|
65
|
-
registerLocalAuthRuntimeCommands(registry);
|
|
66
64
|
registerConversationRuntimeCommands(registry);
|
|
67
65
|
registerQrcodeRuntimeCommands(registry);
|
|
68
66
|
registerOnboardingRuntimeCommands(registry);
|
|
@@ -49,7 +49,7 @@ export function buildDefaultDerivedState(): OnboardingStepDerivationState {
|
|
|
49
49
|
required: false,
|
|
50
50
|
accepted: false,
|
|
51
51
|
reason: 'not-needed',
|
|
52
|
-
detail: 'No
|
|
52
|
+
detail: 'No external runtime auth signal needs confirmation.',
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
};
|
|
@@ -3,9 +3,24 @@ export const AGENT_EXTERNAL_DAEMON_SETTING_LOCK_REASON = 'GoodVibes Agent connec
|
|
|
3
3
|
const AGENT_HIDDEN_SETTING_PREFIXES = [
|
|
4
4
|
['cloud', 'flare.'].join(''),
|
|
5
5
|
['surfaces.', 'home', 'assistant.'].join(''),
|
|
6
|
+
'batch.',
|
|
7
|
+
'controlPlane.',
|
|
8
|
+
'danger.',
|
|
9
|
+
'httpListener.',
|
|
10
|
+
'network.',
|
|
11
|
+
'orchestration.',
|
|
12
|
+
'runtime.',
|
|
13
|
+
'service.',
|
|
6
14
|
'sandbox.',
|
|
15
|
+
'web.',
|
|
16
|
+
'watchers.',
|
|
17
|
+
'wrfc.',
|
|
7
18
|
] as const;
|
|
8
19
|
|
|
20
|
+
const AGENT_HIDDEN_SETTING_KEYS = new Set<string>([
|
|
21
|
+
'ui.wrfcMessages',
|
|
22
|
+
]);
|
|
23
|
+
|
|
9
24
|
const EXTERNAL_DAEMON_SETTING_PREFIXES = [
|
|
10
25
|
'service.',
|
|
11
26
|
'controlPlane.',
|
|
@@ -24,5 +39,6 @@ export function isExternalDaemonOwnedSettingKey(key: string): boolean {
|
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
export function isAgentHiddenSettingKey(key: string): boolean {
|
|
27
|
-
return
|
|
42
|
+
return AGENT_HIDDEN_SETTING_KEYS.has(key)
|
|
43
|
+
|| AGENT_HIDDEN_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
28
44
|
}
|
|
@@ -57,11 +57,9 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
|
57
57
|
}> = [
|
|
58
58
|
{ label: 'Agent Experience', categories: ['display', 'ui', 'behavior', 'permissions'] },
|
|
59
59
|
{ label: 'Models and Providers', categories: ['provider', 'subscriptions', 'helper', 'tools', 'tts'] },
|
|
60
|
-
{ label: 'Local Agent State', categories: ['storage', '
|
|
61
|
-
{ label: '
|
|
62
|
-
{ label: '
|
|
63
|
-
{ label: 'Delegation Compatibility', categories: ['orchestration', 'wrfc'] },
|
|
64
|
-
{ label: 'Advanced', categories: ['flags', 'release', 'danger'] },
|
|
60
|
+
{ label: 'Local Agent State', categories: ['storage', 'cache', 'telemetry'] },
|
|
61
|
+
{ label: 'Channels and Tools', categories: ['surfaces', 'mcp', 'automation'] },
|
|
62
|
+
{ label: 'Advanced', categories: ['flags', 'release'] },
|
|
65
63
|
];
|
|
66
64
|
|
|
67
65
|
export const SETTINGS_CATEGORIES: SettingsCategory[] = SETTINGS_CATEGORY_GROUPS.flatMap(group => group.categories);
|
|
@@ -2,7 +2,6 @@ import type { PanelManager } from '../panel-manager.ts';
|
|
|
2
2
|
import { ApprovalPanel } from '../approval-panel.ts';
|
|
3
3
|
import { AutomationControlPanel } from '../automation-control-panel.ts';
|
|
4
4
|
import { SubscriptionPanel } from '../subscription-panel.ts';
|
|
5
|
-
import { LocalAuthPanel } from '../local-auth-panel.ts';
|
|
6
5
|
import { ProviderAccountsPanel } from '../provider-accounts-panel.ts';
|
|
7
6
|
import { SecurityPanel } from '../security-panel.ts';
|
|
8
7
|
import { TasksPanel } from '../tasks-panel.ts';
|
|
@@ -59,15 +58,6 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
|
|
|
59
58
|
factory: () => new SubscriptionPanel(deps.serviceRegistry, deps.subscriptionManager),
|
|
60
59
|
});
|
|
61
60
|
|
|
62
|
-
manager.registerType({
|
|
63
|
-
id: 'local-auth',
|
|
64
|
-
name: 'Local Auth',
|
|
65
|
-
icon: 'U',
|
|
66
|
-
category: 'monitoring',
|
|
67
|
-
description: 'Local runtime auth users, bootstrap posture, and active sessions',
|
|
68
|
-
factory: () => new LocalAuthPanel(deps.localUserAuthManager),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
61
|
manager.registerType({
|
|
72
62
|
id: 'accounts',
|
|
73
63
|
name: 'Accounts',
|
|
@@ -44,18 +44,19 @@ export function buildProviderHealthDomainSummaries(
|
|
|
44
44
|
|
|
45
45
|
summaries.push({
|
|
46
46
|
name: 'auth',
|
|
47
|
-
level: auth.bootstrapCredentialPresent
|
|
47
|
+
level: auth.bootstrapCredentialPresent ? 'warn' : 'info',
|
|
48
48
|
summary: auth.bootstrapCredentialPresent
|
|
49
|
-
? 'bootstrap credential
|
|
50
|
-
:
|
|
51
|
-
next:
|
|
49
|
+
? 'external runtime bootstrap credential visible in local compatibility state'
|
|
50
|
+
: 'runtime auth administration belongs to the external runtime owner',
|
|
51
|
+
next: '/auth review',
|
|
52
52
|
details: [
|
|
53
|
-
|
|
54
|
-
auth.userCount
|
|
53
|
+
'GoodVibes Agent does not create, delete, rotate, revoke, or clear runtime auth users or sessions.',
|
|
54
|
+
`${auth.userCount} compatibility user record(s) and ${auth.sessionCount} session record(s) are visible for diagnostics only.`,
|
|
55
|
+
auth.bootstrapCredentialPresent ? 'Runtime bootstrap cleanup must be done from the runtime-owning TUI or host tooling.' : '',
|
|
55
56
|
].filter(Boolean),
|
|
56
57
|
nextSteps: auth.bootstrapCredentialPresent
|
|
57
|
-
? ['/auth
|
|
58
|
-
: ['/auth
|
|
58
|
+
? ['/auth review', '/providers', '/subscription providers']
|
|
59
|
+
: ['/auth review', '/providers'],
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
const settingIssueCount = settings.conflictCount + settings.recentFailureCount + (settings.hasStagedManagedBundle ? 1 : 0);
|
|
@@ -62,22 +62,6 @@ function setNestedValue(root: Record<string, unknown>, key: string, value: unkno
|
|
|
62
62
|
|
|
63
63
|
type RollbackAction = () => Promise<void> | void;
|
|
64
64
|
|
|
65
|
-
interface BootstrapCredential {
|
|
66
|
-
readonly username: string;
|
|
67
|
-
readonly password: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface PersistedAuthUser {
|
|
71
|
-
readonly username: string;
|
|
72
|
-
readonly passwordHash: string;
|
|
73
|
-
readonly roles?: readonly string[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface MutableAuthManager {
|
|
77
|
-
readonly users?: Map<string, PersistedAuthUser>;
|
|
78
|
-
readonly sessions?: Map<string, { readonly token: string; readonly username: string; readonly expiresAt: number }>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
65
|
function restoreFile(path: string, previous: string | null, reload?: () => void): void {
|
|
82
66
|
if (previous === null) {
|
|
83
67
|
if (existsSync(path)) unlinkSync(path);
|
|
@@ -88,31 +72,6 @@ function restoreFile(path: string, previous: string | null, reload?: () => void)
|
|
|
88
72
|
reload?.();
|
|
89
73
|
}
|
|
90
74
|
|
|
91
|
-
function parseBootstrapCredential(content: string | null): BootstrapCredential | null {
|
|
92
|
-
if (content === null) return null;
|
|
93
|
-
let username = '';
|
|
94
|
-
let password = '';
|
|
95
|
-
for (const rawLine of content.split('\n')) {
|
|
96
|
-
const line = rawLine.trim();
|
|
97
|
-
if (line.startsWith('username=')) username = line.slice('username='.length);
|
|
98
|
-
if (line.startsWith('password=')) password = line.slice('password='.length);
|
|
99
|
-
}
|
|
100
|
-
return username.length > 0 && password.length > 0 ? { username, password } : null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function parsePersistedAuthUsers(content: string): readonly PersistedAuthUser[] {
|
|
104
|
-
const parsed = JSON.parse(content) as unknown;
|
|
105
|
-
if (!isPlainObject(parsed) || parsed.version !== 1 || !Array.isArray(parsed.users)) {
|
|
106
|
-
throw new Error('Expected a version 1 local auth user store.');
|
|
107
|
-
}
|
|
108
|
-
return parsed.users.filter((user): user is PersistedAuthUser => (
|
|
109
|
-
isPlainObject(user)
|
|
110
|
-
&& typeof user.username === 'string'
|
|
111
|
-
&& typeof user.passwordHash === 'string'
|
|
112
|
-
&& (user.roles === undefined || (Array.isArray(user.roles) && user.roles.every((role) => typeof role === 'string')))
|
|
113
|
-
));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
75
|
function snapshotFileRollback(path: string, reload?: () => void): RollbackAction {
|
|
117
76
|
const previous = existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
118
77
|
return () => restoreFile(path, previous, reload);
|
|
@@ -227,18 +186,10 @@ function validateSecretOperation(
|
|
|
227
186
|
}
|
|
228
187
|
|
|
229
188
|
function validateAuthOperation(
|
|
230
|
-
|
|
231
|
-
|
|
189
|
+
_deps: OnboardingApplyDependencies,
|
|
190
|
+
_operation: Extract<OnboardingApplyOperation, { kind: 'ensure-auth-user' }>,
|
|
232
191
|
): void {
|
|
233
|
-
|
|
234
|
-
if (operation.username.trim().length === 0) throw new Error('Local auth username is required.');
|
|
235
|
-
if (operation.password.length === 0) throw new Error(`Local auth password for ${operation.username} is required.`);
|
|
236
|
-
const username = operation.username.trim();
|
|
237
|
-
const existing = deps.auth.inspect().users.find((user) => user.username === username);
|
|
238
|
-
const requiredRoles = operation.roles ?? ['admin'];
|
|
239
|
-
if (existing && !requiredRoles.every((role) => existing.roles.includes(role))) {
|
|
240
|
-
throw new Error(`Existing local auth user ${username} is missing required role(s): ${requiredRoles.join(', ')}.`);
|
|
241
|
-
}
|
|
192
|
+
throw new Error('Runtime auth user/session administration is external to GoodVibes Agent onboarding.');
|
|
242
193
|
}
|
|
243
194
|
|
|
244
195
|
function validateAcknowledgementOperation(
|
|
@@ -313,44 +264,6 @@ async function applySecretOperation(
|
|
|
313
264
|
};
|
|
314
265
|
}
|
|
315
266
|
|
|
316
|
-
function applyAuthOperation(
|
|
317
|
-
deps: OnboardingApplyDependencies,
|
|
318
|
-
operation: Extract<OnboardingApplyOperation, { kind: 'ensure-auth-user' }>,
|
|
319
|
-
): OnboardingAppliedOperation {
|
|
320
|
-
validateAuthOperation(deps, operation);
|
|
321
|
-
const auth = deps.auth!;
|
|
322
|
-
const username = operation.username.trim();
|
|
323
|
-
const before = auth.inspect();
|
|
324
|
-
const existing = before.users.find((user) => user.username === username);
|
|
325
|
-
const bootstrapCredential = before.bootstrapCredentialPresent
|
|
326
|
-
? parseBootstrapCredential(readFileSync(before.bootstrapCredentialPath, 'utf-8'))
|
|
327
|
-
: null;
|
|
328
|
-
|
|
329
|
-
if (existing) {
|
|
330
|
-
auth.rotatePassword(username, operation.password);
|
|
331
|
-
} else {
|
|
332
|
-
auth.addUser(username, operation.password, operation.roles ?? ['admin']);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (operation.retireBootstrapCredential) {
|
|
336
|
-
if (bootstrapCredential && bootstrapCredential.username !== username && auth.getUser(bootstrapCredential.username)) {
|
|
337
|
-
auth.deleteUser(bootstrapCredential.username);
|
|
338
|
-
}
|
|
339
|
-
auth.clearBootstrapCredentialFile();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (operation.createSession ?? true) {
|
|
343
|
-
auth.createSession(username);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
kind: operation.kind,
|
|
348
|
-
summary: existing
|
|
349
|
-
? `Updated local auth user ${username}.`
|
|
350
|
-
: `Created local auth user ${username}.`,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
267
|
async function buildSecretRollbackAction(
|
|
355
268
|
deps: OnboardingApplyDependencies,
|
|
356
269
|
operation: Extract<OnboardingApplyOperation, { kind: 'set-secret' }>,
|
|
@@ -369,67 +282,6 @@ async function buildSecretRollbackAction(
|
|
|
369
282
|
};
|
|
370
283
|
}
|
|
371
284
|
|
|
372
|
-
function buildAuthRollbackAction(
|
|
373
|
-
deps: OnboardingApplyDependencies,
|
|
374
|
-
operation: Extract<OnboardingApplyOperation, { kind: 'ensure-auth-user' }>,
|
|
375
|
-
): RollbackAction {
|
|
376
|
-
validateAuthOperation(deps, operation);
|
|
377
|
-
const auth = deps.auth!;
|
|
378
|
-
const mutable = auth as unknown as MutableAuthManager;
|
|
379
|
-
const username = operation.username.trim();
|
|
380
|
-
const before = auth.inspect();
|
|
381
|
-
const existingUser = before.users.find((user) => user.username === username);
|
|
382
|
-
const existingSessionFingerprints = new Set(before.sessions
|
|
383
|
-
.filter((session) => session.username === username)
|
|
384
|
-
.map((session) => session.tokenFingerprint));
|
|
385
|
-
const userStoreSnapshot = existsSync(before.userStorePath) ? readFileSync(before.userStorePath, 'utf-8') : null;
|
|
386
|
-
const bootstrapCredentialSnapshot = existsSync(before.bootstrapCredentialPath)
|
|
387
|
-
? readFileSync(before.bootstrapCredentialPath, 'utf-8')
|
|
388
|
-
: null;
|
|
389
|
-
const bootstrapCredential = parseBootstrapCredential(bootstrapCredentialSnapshot);
|
|
390
|
-
const beforeSessions = mutable.sessions instanceof Map
|
|
391
|
-
? [...mutable.sessions.entries()].map(([token, session]) => [token, { ...session }] as const)
|
|
392
|
-
: [];
|
|
393
|
-
|
|
394
|
-
return () => {
|
|
395
|
-
for (const session of auth.inspect().sessions) {
|
|
396
|
-
if (session.username === username && !existingSessionFingerprints.has(session.tokenFingerprint)) {
|
|
397
|
-
auth.revokeSession(session.tokenFingerprint);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (bootstrapCredential && !auth.getUser(bootstrapCredential.username)) {
|
|
402
|
-
auth.addUser(bootstrapCredential.username, bootstrapCredential.password, ['admin']);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (!existingUser && auth.getUser(username)) {
|
|
406
|
-
try {
|
|
407
|
-
auth.deleteUser(username);
|
|
408
|
-
} catch (error) {
|
|
409
|
-
if (mutable.users instanceof Map) mutable.users.delete(username);
|
|
410
|
-
else throw error;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
restoreFile(before.bootstrapCredentialPath, bootstrapCredentialSnapshot);
|
|
415
|
-
restoreFile(before.userStorePath, userStoreSnapshot);
|
|
416
|
-
|
|
417
|
-
if (mutable.users instanceof Map) {
|
|
418
|
-
if (userStoreSnapshot === null) {
|
|
419
|
-
if (before.users.length === 0) mutable.users.clear();
|
|
420
|
-
} else {
|
|
421
|
-
mutable.users.clear();
|
|
422
|
-
for (const user of parsePersistedAuthUsers(userStoreSnapshot)) mutable.users.set(user.username, user);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (mutable.sessions instanceof Map) {
|
|
427
|
-
mutable.sessions.clear();
|
|
428
|
-
for (const [token, session] of beforeSessions) mutable.sessions.set(token, session);
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
285
|
async function buildRollbackAction(
|
|
434
286
|
deps: OnboardingApplyDependencies,
|
|
435
287
|
operation: OnboardingApplyOperation,
|
|
@@ -453,7 +305,8 @@ async function buildRollbackAction(
|
|
|
453
305
|
}
|
|
454
306
|
|
|
455
307
|
if (operation.kind === 'ensure-auth-user') {
|
|
456
|
-
|
|
308
|
+
validateAuthOperation(deps, operation);
|
|
309
|
+
return () => {};
|
|
457
310
|
}
|
|
458
311
|
|
|
459
312
|
if (operation.kind === 'acknowledge') {
|
|
@@ -633,8 +486,7 @@ export async function applyOnboardingRequest(
|
|
|
633
486
|
}
|
|
634
487
|
|
|
635
488
|
if (operation.kind === 'ensure-auth-user') {
|
|
636
|
-
|
|
637
|
-
rollbacks.push(rollback);
|
|
489
|
+
validateAuthOperation(deps, operation);
|
|
638
490
|
continue;
|
|
639
491
|
}
|
|
640
492
|
|
|
@@ -500,23 +500,23 @@ export function deriveReopenEditAcknowledgementState(
|
|
|
500
500
|
snapshot,
|
|
501
501
|
'auth',
|
|
502
502
|
'bootstrap-credential',
|
|
503
|
-
'
|
|
503
|
+
'An external runtime bootstrap credential signal is still visible to Agent.',
|
|
504
504
|
)
|
|
505
505
|
: authSessionCount > 0
|
|
506
506
|
? buildRequiredAcknowledgement(
|
|
507
507
|
snapshot,
|
|
508
508
|
'auth',
|
|
509
509
|
'active-sessions',
|
|
510
|
-
`${authSessionCount}
|
|
510
|
+
`${authSessionCount} external runtime auth session signal(s) are currently visible.`,
|
|
511
511
|
)
|
|
512
512
|
: authUserCount > 0
|
|
513
513
|
? buildRequiredAcknowledgement(
|
|
514
514
|
snapshot,
|
|
515
515
|
'auth',
|
|
516
516
|
'auth-state',
|
|
517
|
-
`${authUserCount}
|
|
517
|
+
`${authUserCount} external runtime auth user signal(s) are already visible.`,
|
|
518
518
|
)
|
|
519
|
-
: buildNotNeededAcknowledgement(snapshot, 'auth', 'No
|
|
519
|
+
: buildNotNeededAcknowledgement(snapshot, 'auth', 'No external runtime auth signal needs confirmation.');
|
|
520
520
|
|
|
521
521
|
return {
|
|
522
522
|
providers,
|
|
@@ -383,17 +383,7 @@ export interface OnboardingApplyDependencies {
|
|
|
383
383
|
readonly clock?: () => number;
|
|
384
384
|
readonly config: Pick<ConfigManager, 'get' | 'getRaw' | 'load' | 'setDynamic'>;
|
|
385
385
|
readonly secrets?: Pick<SecretsManager, 'delete' | 'get' | 'inspect' | 'set'>;
|
|
386
|
-
readonly auth?: Pick<
|
|
387
|
-
UserAuthManager,
|
|
388
|
-
'addUser'
|
|
389
|
-
| 'clearBootstrapCredentialFile'
|
|
390
|
-
| 'createSession'
|
|
391
|
-
| 'deleteUser'
|
|
392
|
-
| 'getUser'
|
|
393
|
-
| 'inspect'
|
|
394
|
-
| 'revokeSession'
|
|
395
|
-
| 'rotatePassword'
|
|
396
|
-
>;
|
|
386
|
+
readonly auth?: Pick<UserAuthManager, 'inspect'>;
|
|
397
387
|
readonly shellPaths: OnboardingShellPaths;
|
|
398
388
|
readonly acknowledgementScope?: OnboardingStateScope;
|
|
399
389
|
}
|
|
@@ -116,36 +116,14 @@ async function verifySecretOperation(
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function verifyAuthOperation(
|
|
119
|
-
|
|
119
|
+
_deps: OnboardingVerificationDependencies,
|
|
120
120
|
operation: Extract<OnboardingApplyOperation, { kind: 'ensure-auth-user' }>,
|
|
121
121
|
): OnboardingVerificationItem {
|
|
122
|
-
if (!deps.auth) {
|
|
123
|
-
return {
|
|
124
|
-
id: `auth:${operation.username}`,
|
|
125
|
-
status: 'fail',
|
|
126
|
-
message: 'Local auth manager is unavailable.',
|
|
127
|
-
target: operation.username,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const snapshot = deps.auth.inspect();
|
|
132
122
|
const username = operation.username.trim();
|
|
133
|
-
const user = snapshot.users.find((entry) => entry.username === username);
|
|
134
|
-
const requiredRoles = operation.roles ?? ['admin'];
|
|
135
|
-
const userExists = Boolean(user) && requiredRoles.every((role) => user!.roles.includes(role));
|
|
136
|
-
const sessionExists = operation.createSession === false
|
|
137
|
-
? true
|
|
138
|
-
: snapshot.sessions.some((session) => session.username === username);
|
|
139
|
-
const bootstrapRetired = operation.retireBootstrapCredential
|
|
140
|
-
? snapshot.bootstrapCredentialPresent === false
|
|
141
|
-
: true;
|
|
142
|
-
const ok = userExists && sessionExists && bootstrapRetired;
|
|
143
123
|
return {
|
|
144
124
|
id: `auth:${username}`,
|
|
145
|
-
status:
|
|
146
|
-
message:
|
|
147
|
-
? `${username} local auth user has required role(s) and session state.`
|
|
148
|
-
: `${username} local auth user/session/role/bootstrap state was not created.`,
|
|
125
|
+
status: 'fail',
|
|
126
|
+
message: 'Runtime auth user/session administration is external to GoodVibes Agent onboarding.',
|
|
149
127
|
target: username,
|
|
150
128
|
};
|
|
151
129
|
}
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.1.
|
|
9
|
+
let _version = '0.1.83';
|
|
10
10
|
let _sdkVersion = '0.33.35';
|
|
11
11
|
try {
|
|
12
12
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
2
|
-
import { openCommandPanel, requireLocalUserAuthManager } from './runtime-services.ts';
|
|
3
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
|
-
import { requireYesFlag, stripYesFlag } from './confirmation.ts';
|
|
5
|
-
|
|
6
|
-
function formatRoles(roles: readonly string[]): string {
|
|
7
|
-
return roles.length > 0 ? roles.join(', ') : '(none)';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function handleLocalAuthCommand(args: string[], ctx: CommandContext): void {
|
|
11
|
-
const parsed = stripYesFlag(args);
|
|
12
|
-
const commandArgs = [...parsed.rest];
|
|
13
|
-
const sub = (commandArgs[0] ?? 'review').toLowerCase();
|
|
14
|
-
const auth = requireLocalUserAuthManager(ctx);
|
|
15
|
-
if (sub === 'panel' || sub === 'open') {
|
|
16
|
-
openCommandPanel(ctx, 'local-auth');
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (sub === 'add-user') {
|
|
21
|
-
const username = commandArgs[1];
|
|
22
|
-
const password = commandArgs[2];
|
|
23
|
-
const roles = commandArgs[3]?.split(',').map((value) => value.trim()).filter(Boolean) ?? ['admin'];
|
|
24
|
-
if (!username || !password) {
|
|
25
|
-
ctx.print('Usage: /auth local add-user <username> <password> [roles] --yes');
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (!parsed.yes) {
|
|
29
|
-
requireYesFlag(ctx, `add local auth user ${username}`, '/auth local add-user <username> <password> [roles] --yes');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const added = auth.addUser(username, password, roles);
|
|
34
|
-
ctx.print(`Added local auth user ${added.username} (${formatRoles(added.roles)}).`);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
ctx.print(summarizeError(error));
|
|
37
|
-
}
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (sub === 'delete-user') {
|
|
42
|
-
const username = commandArgs[1];
|
|
43
|
-
if (!username) {
|
|
44
|
-
ctx.print('Usage: /auth local delete-user <username> --yes');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (!parsed.yes) {
|
|
48
|
-
requireYesFlag(ctx, `delete local auth user ${username}`, '/auth local delete-user <username> --yes');
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const deleted = auth.deleteUser(username);
|
|
53
|
-
ctx.print(deleted ? `Deleted local auth user ${username}.` : `Unknown local auth user: ${username}`);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
ctx.print(summarizeError(error));
|
|
56
|
-
}
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (sub === 'rotate-password') {
|
|
61
|
-
const username = commandArgs[1];
|
|
62
|
-
const password = commandArgs[2];
|
|
63
|
-
if (!username || !password) {
|
|
64
|
-
ctx.print('Usage: /auth local rotate-password <username> <password> --yes');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (!parsed.yes) {
|
|
68
|
-
requireYesFlag(ctx, `rotate password for local auth user ${username}`, '/auth local rotate-password <username> <password> --yes');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
auth.rotatePassword(username, password);
|
|
73
|
-
ctx.print(`Rotated password for ${username}. Existing sessions were revoked.`);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
ctx.print(summarizeError(error));
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (sub === 'revoke-session') {
|
|
81
|
-
const token = commandArgs[1];
|
|
82
|
-
if (!token) {
|
|
83
|
-
ctx.print('Usage: /auth local revoke-session <token-or-fingerprint> --yes');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (!parsed.yes) {
|
|
87
|
-
requireYesFlag(ctx, 'revoke local auth session', '/auth local revoke-session <token-or-fingerprint> --yes');
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
ctx.print(auth.revokeSession(token) ? `Revoked session ${token.slice(0, 12)}…` : `Unknown session token or fingerprint: ${token}`);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (sub === 'clear-bootstrap-file') {
|
|
95
|
-
if (!parsed.yes) {
|
|
96
|
-
requireYesFlag(ctx, 'clear the local auth bootstrap credential file', '/auth local clear-bootstrap-file --yes');
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
ctx.print(auth.clearBootstrapCredentialFile()
|
|
100
|
-
? 'Removed bootstrap credential file.'
|
|
101
|
-
: 'No bootstrap credential file was present.');
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const snapshot = auth.inspect();
|
|
106
|
-
ctx.print([
|
|
107
|
-
'Local Auth Review',
|
|
108
|
-
` user store: ${snapshot.userStorePath}`,
|
|
109
|
-
` bootstrap file: ${snapshot.bootstrapCredentialPath}`,
|
|
110
|
-
` bootstrap credentials: ${snapshot.bootstrapCredentialPresent ? 'present' : 'cleared'}`,
|
|
111
|
-
` users: ${snapshot.userCount}`,
|
|
112
|
-
` sessions: ${snapshot.sessionCount}`,
|
|
113
|
-
...snapshot.users.map((user) => ` user: ${user.username} roles=${formatRoles(user.roles)}`),
|
|
114
|
-
...snapshot.sessions.map((session) => ` session: ${session.username} expires=${new Date(session.expiresAt).toISOString()} fingerprint=${session.tokenFingerprint}`),
|
|
115
|
-
].join('\n'));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function registerLocalAuthRuntimeCommands(registry: CommandRegistry): void {
|
|
119
|
-
registry.register({
|
|
120
|
-
name: 'local-auth',
|
|
121
|
-
aliases: ['auth-local'],
|
|
122
|
-
description: 'Inspect and manage local runtime auth users, sessions, and bootstrap credentials',
|
|
123
|
-
usage: '[review|panel|add-user <username> <password> [roles] --yes|delete-user <username> --yes|rotate-password <username> <password> --yes|revoke-session <token-or-fingerprint> --yes|clear-bootstrap-file --yes]',
|
|
124
|
-
handler(args, ctx) {
|
|
125
|
-
handleLocalAuthCommand(args, ctx);
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
-
import {
|
|
5
|
-
buildDetailBlock,
|
|
6
|
-
buildGuidanceLine,
|
|
7
|
-
buildPanelListRow,
|
|
8
|
-
buildPanelLine,
|
|
9
|
-
buildSummaryBlock,
|
|
10
|
-
buildPanelWorkspace,
|
|
11
|
-
DEFAULT_PANEL_PALETTE,
|
|
12
|
-
type PanelPalette,
|
|
13
|
-
} from './polish.ts';
|
|
14
|
-
import type { LocalAuthSnapshot } from '@pellux/goodvibes-sdk/platform/security';
|
|
15
|
-
import type { LocalAuthInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
16
|
-
|
|
17
|
-
const C = {
|
|
18
|
-
...DEFAULT_PANEL_PALETTE,
|
|
19
|
-
info: '#38bdf8',
|
|
20
|
-
warn: '#eab308',
|
|
21
|
-
error: '#ef4444',
|
|
22
|
-
selectBg: '#1e293b',
|
|
23
|
-
} as const;
|
|
24
|
-
|
|
25
|
-
function formatRoles(roles: readonly string[]): string {
|
|
26
|
-
return roles.length > 0 ? roles.join(', ') : '(none)';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type LocalAuthUser = LocalAuthSnapshot['users'][number];
|
|
30
|
-
|
|
31
|
-
export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
32
|
-
private readonly authManager: LocalAuthInspectionQuery;
|
|
33
|
-
|
|
34
|
-
public constructor(authManager: LocalAuthInspectionQuery) {
|
|
35
|
-
super('local-auth', 'Local Auth', 'U', 'monitoring');
|
|
36
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
37
|
-
this.authManager = authManager;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
protected override getPalette(): PanelPalette {
|
|
41
|
-
return C;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
protected getItems(): readonly LocalAuthUser[] {
|
|
45
|
-
return this.authManager.inspect().users;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
|
|
49
|
-
return buildPanelListRow(width, [
|
|
50
|
-
{ text: user.username.padEnd(20), fg: C.value },
|
|
51
|
-
{ text: ` roles=${formatRoles(user.roles)}`.slice(0, Math.max(0, width - 24)), fg: C.info },
|
|
52
|
-
], C, { selected });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
protected override getEmptyStateMessage(): string {
|
|
56
|
-
return ' No local auth users configured.';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
public render(width: number, height: number): Line[] {
|
|
60
|
-
const intro = 'Review local runtime auth users, bootstrap state, and active sessions.';
|
|
61
|
-
const snapshot = this.authManager.inspect();
|
|
62
|
-
const users = this.getItems();
|
|
63
|
-
|
|
64
|
-
const issueMessages: string[] = [];
|
|
65
|
-
if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists and should be cleared after password rotation.');
|
|
66
|
-
if (snapshot.userCount <= 1) issueMessages.push('Only one local auth user is configured.');
|
|
67
|
-
if (snapshot.sessionCount === 0) issueMessages.push('No active local auth sessions are currently tracked.');
|
|
68
|
-
|
|
69
|
-
const headerLines: Line[] = [
|
|
70
|
-
...buildSummaryBlock(width, 'Local auth posture', [
|
|
71
|
-
buildPanelLine(width, [
|
|
72
|
-
[' users ', C.label],
|
|
73
|
-
[String(snapshot.userCount), C.value],
|
|
74
|
-
[' sessions ', C.label],
|
|
75
|
-
[String(snapshot.sessionCount), snapshot.sessionCount > 0 ? C.info : C.dim],
|
|
76
|
-
[' bootstrap ', C.label],
|
|
77
|
-
[snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
|
|
78
|
-
]),
|
|
79
|
-
buildPanelLine(width, [[' user store ', C.label], [snapshot.userStorePath.slice(0, Math.max(0, width - 13)), C.dim]]),
|
|
80
|
-
buildPanelLine(width, [[' bootstrap file ', C.label], [snapshot.bootstrapCredentialPath.slice(0, Math.max(0, width - 18)), C.dim]]),
|
|
81
|
-
...(issueMessages.length > 0
|
|
82
|
-
? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}`.slice(0, Math.max(0, width)), C.warn]]))
|
|
83
|
-
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
84
|
-
buildGuidanceLine(width, '/auth local rotate-password <user> <password> --yes', 'rotate bootstrap/default credentials and revoke older sessions as needed', C),
|
|
85
|
-
], C),
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
if (users.length === 0) {
|
|
89
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
90
|
-
title: 'Local Auth Control Room',
|
|
91
|
-
intro,
|
|
92
|
-
sections: [{ lines: headerLines }],
|
|
93
|
-
palette: C,
|
|
94
|
-
});
|
|
95
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
96
|
-
return workspace;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
this.clampSelection();
|
|
100
|
-
const selected = users[this.selectedIndex];
|
|
101
|
-
|
|
102
|
-
const footerLines: Line[] = [];
|
|
103
|
-
if (selected) {
|
|
104
|
-
footerLines.push(
|
|
105
|
-
...buildDetailBlock(width, 'Selected user', [
|
|
106
|
-
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
|
|
107
|
-
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username} <password> --yes`.slice(0, Math.max(0, width)), C.dim]]),
|
|
108
|
-
buildPanelLine(width, [[` next: /auth local delete-user ${selected.username} --yes`.slice(0, Math.max(0, width)), C.dim]]),
|
|
109
|
-
], C),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (snapshot.sessions.length > 0) {
|
|
114
|
-
footerLines.push(
|
|
115
|
-
...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
116
|
-
[' ', C.label],
|
|
117
|
-
[session.username.padEnd(18), C.value],
|
|
118
|
-
[` expires ${new Date(session.expiresAt).toLocaleString()}`.slice(0, Math.max(0, width - 20)), C.dim],
|
|
119
|
-
])),
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
footerLines.push(buildPanelLine(width, [[' /auth local review mutations require --yes: add-user rotate-password revoke-session ', C.dim]]));
|
|
123
|
-
|
|
124
|
-
return this.renderList(width, height, {
|
|
125
|
-
title: 'Local Auth Control Room',
|
|
126
|
-
header: headerLines,
|
|
127
|
-
footer: footerLines,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|