@pellux/goodvibes-tui 1.24.2 → 1.26.0
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 +32 -0
- package/README.md +1 -1
- package/bin/goodvibes-daemon +24 -0
- package/docs/foundation-artifacts/operator-contract.json +18234 -12253
- package/package.json +8 -7
- package/src/cli/help.ts +10 -0
- package/src/cli/redaction.ts +68 -3
- package/src/config/goodvibes-home.ts +149 -0
- package/src/config/secret-config.ts +64 -8
- package/src/core/turn-event-wiring.ts +7 -16
- package/src/core/workstream-notification.ts +69 -0
- package/src/daemon/cli.ts +62 -7
- package/src/daemon/handlers/credentials.ts +15 -5
- package/src/daemon/handlers/index.ts +7 -6
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +332 -0
- package/src/daemon/send/composition.ts +99 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/input/commands/calendar-runtime.ts +245 -0
- package/src/input/commands/config.ts +68 -2
- package/src/input/commands/connection-status.ts +258 -0
- package/src/input/commands/local-provider-runtime.ts +9 -1
- package/src/input/commands/mail-runtime.ts +267 -0
- package/src/input/commands/operator-runtime.ts +6 -1
- package/src/input/commands/payment-card-intake.ts +200 -0
- package/src/input/commands/profile-render.ts +250 -0
- package/src/input/commands/profile-runtime.ts +362 -0
- package/src/input/commands/profile-types.ts +381 -0
- package/src/input/commands/review-runtime.ts +13 -1
- package/src/input/commands.ts +11 -0
- package/src/input/composer-editor.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +13 -1
- package/src/input/payments-config.ts +167 -0
- package/src/input/provider-key-intake.ts +10 -2
- package/src/input/settings-modal-commit.ts +146 -0
- package/src/input/settings-modal-connections.ts +102 -0
- package/src/input/settings-modal-data.ts +30 -0
- package/src/input/settings-modal-reset.ts +6 -2
- package/src/input/settings-modal-secrets.ts +9 -2
- package/src/input/settings-modal-types.ts +19 -3
- package/src/input/settings-modal.ts +54 -107
- package/src/main.ts +7 -2
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/settings-modal-connections.ts +63 -0
- package/src/renderer/settings-modal-helpers.ts +6 -0
- package/src/renderer/settings-modal.ts +21 -1
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap.ts +2 -2
- package/src/runtime/context.ts +9 -0
- package/src/runtime/credential-composition.ts +54 -0
- package/src/runtime/daemon-handler-composition.ts +0 -4
- package/src/runtime/install-self-check-startup.ts +23 -0
- package/src/runtime/mail-composition.ts +65 -0
- package/src/runtime/onboarding/types.ts +6 -1
- package/src/runtime/path-shadow-startup.ts +243 -0
- package/src/runtime/path-shadow.ts +418 -0
- package/src/runtime/reachability-notice.ts +100 -0
- package/src/runtime/runtime-services-types.ts +243 -0
- package/src/runtime/services.ts +30 -224
- package/src/shell/ui-openers.ts +7 -0
- package/src/verification/verification-ledger.ts +158 -0
- package/src/version.ts +1 -1
- package/src/daemon/handlers/calendar/caldav-client.ts +0 -662
- package/src/daemon/handlers/calendar/ics.ts +0 -556
- package/src/daemon/handlers/calendar/index.ts +0 -316
- package/src/daemon/handlers/email/config.ts +0 -164
- package/src/daemon/handlers/email/imap-connector.ts +0 -441
- package/src/daemon/handlers/email/imap-parsing.ts +0 -499
- package/src/daemon/handlers/email/index.ts +0 -43
- package/src/daemon/handlers/email/read-handlers.ts +0 -80
- package/src/daemon/handlers/email/runtime.ts +0 -140
- package/src/daemon/handlers/email/smtp-connector.ts +0 -557
- package/src/daemon/handlers/email/validation.ts +0 -147
- package/src/daemon/handlers/email/write-handlers.ts +0 -133
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { LLMProvider } from '@pellux/goodvibes-sdk/platform/providers';
|
|
15
|
+
import type { SecretScope } from '../config/secrets.ts';
|
|
15
16
|
import type { ConcealedInputRequest } from './concealed-input.ts';
|
|
16
17
|
import type { CommandContext } from './command-registry.ts';
|
|
17
18
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -58,7 +59,7 @@ export interface ProviderKeyIntakeDeps {
|
|
|
58
59
|
readonly provider: LLMProvider | undefined;
|
|
59
60
|
/** Canonical secret store — the same path /secrets set writes through. */
|
|
60
61
|
readonly secretsManager?: {
|
|
61
|
-
set(key: string, value: string, options?: { scope?:
|
|
62
|
+
set(key: string, value: string, options?: { scope?: SecretScope; medium?: 'secure' | 'plaintext' }): Promise<void>;
|
|
62
63
|
} | undefined;
|
|
63
64
|
/** Re-resolves credentials and re-registers providers live (no restart). */
|
|
64
65
|
readonly refreshProviderCredentials?: (() => Promise<void>) | undefined;
|
|
@@ -112,7 +113,14 @@ export function runProviderKeyIntake(
|
|
|
112
113
|
}
|
|
113
114
|
void (async () => {
|
|
114
115
|
try {
|
|
115
|
-
|
|
116
|
+
// Daemon scope, not user scope. The DAEMON is the process that runs
|
|
117
|
+
// the model — it answers Telegram, ntfy, mail and every scheduled
|
|
118
|
+
// run with this TUI closed. A model key filed in this surface's own
|
|
119
|
+
// tier is readable only while the TUI is open, so the daemon has no
|
|
120
|
+
// provider to call and cannot answer at all. Daemon tier is the one
|
|
121
|
+
// home every surface and the daemon read, so a key entered here works
|
|
122
|
+
// everywhere, which is the point of configuring it once.
|
|
123
|
+
await secretsManager.set(secretKey, value, { scope: 'daemon', medium: 'secure' });
|
|
116
124
|
await deps.refreshProviderCredentials?.();
|
|
117
125
|
deps.print(`Stored the API key for ${label} and registered it live (value hidden). No restart needed.`);
|
|
118
126
|
} catch (error) {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-modal-commit.ts — commitEdit(), extracted out of the SettingsModal
|
|
3
|
+
* class body so the class file can stay under the repo's architecture
|
|
4
|
+
* line-count gate without trimming arbitrary lines to clear the number.
|
|
5
|
+
*
|
|
6
|
+
* Same dependency-injection shape as settings-modal-activation.ts /
|
|
7
|
+
* settings-modal-adjustment.ts / settings-modal-reset.ts: explicit getters
|
|
8
|
+
* and setter callbacks instead of a bound `this`, so the logic is
|
|
9
|
+
* unit-testable without a live SettingsModal instance and the class method
|
|
10
|
+
* stays a thin delegator.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
14
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
15
|
+
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
16
|
+
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
17
|
+
import { setSecretBackedSettingValue, type SettingsSecretsManager } from './settings-modal-secrets.ts';
|
|
18
|
+
import { buildMcpEntries } from './settings-modal-data.ts';
|
|
19
|
+
import { isWorktreeSetupListConfigKey, parseWorktreeSetupListInput } from './worktree-setup-config.ts';
|
|
20
|
+
import { isSandboxExecListConfigKey, parseSandboxExecListInput } from './sandbox-exec-config.ts';
|
|
21
|
+
import { isExecEnvScrubAllowlistConfigKey, parseExecEnvScrubAllowlistInput } from './exec-env-scrub-config.ts';
|
|
22
|
+
import type { McpEntry, SettingEntry } from './settings-modal-types.ts';
|
|
23
|
+
|
|
24
|
+
export interface CommitEditContext {
|
|
25
|
+
readonly editingMode: boolean;
|
|
26
|
+
readonly currentCategory: string;
|
|
27
|
+
readonly editBuffer: string;
|
|
28
|
+
readonly configManager: ConfigManager | null;
|
|
29
|
+
readonly secretsManager: SettingsSecretsManager | null;
|
|
30
|
+
readonly mcpRegistry: McpRegistry | null;
|
|
31
|
+
readonly mcpAllowAllConfirmationTarget: string | null;
|
|
32
|
+
getSelectedMcp(): McpEntry | null;
|
|
33
|
+
getSelected(): SettingEntry | null;
|
|
34
|
+
setValue(key: ConfigKey, value: unknown): void;
|
|
35
|
+
setEditingMode(value: boolean): void;
|
|
36
|
+
setEditBuffer(value: string): void;
|
|
37
|
+
setMcpEntries(entries: McpEntry[]): void;
|
|
38
|
+
setMcpAllowAllConfirmationTarget(value: string | null): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Commit the current editBuffer to the config. Returns true on success,
|
|
43
|
+
* false if validation failed (the caller always clears editingMode/editBuffer
|
|
44
|
+
* regardless — a failed commit never leaves stale edit state behind).
|
|
45
|
+
*/
|
|
46
|
+
export function commitEditValue(ctx: CommitEditContext): boolean {
|
|
47
|
+
if (!ctx.editingMode) return false;
|
|
48
|
+
|
|
49
|
+
if (ctx.currentCategory === 'mcp') {
|
|
50
|
+
const entry = ctx.getSelectedMcp();
|
|
51
|
+
if (!entry || !ctx.mcpRegistry) return false;
|
|
52
|
+
if (ctx.mcpAllowAllConfirmationTarget) {
|
|
53
|
+
const expected = `ALLOW ALL ${ctx.mcpAllowAllConfirmationTarget}`;
|
|
54
|
+
if (ctx.editBuffer.trim() !== expected) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
ctx.mcpRegistry.setServerTrustMode(entry.name, 'allow-all');
|
|
58
|
+
ctx.setMcpEntries(buildMcpEntries(ctx.mcpRegistry));
|
|
59
|
+
ctx.setEditingMode(false);
|
|
60
|
+
ctx.setEditBuffer('');
|
|
61
|
+
ctx.setMcpAllowAllConfirmationTarget(null);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nextMode = ctx.editBuffer.trim() as McpEntry['trustMode'];
|
|
66
|
+
const validModes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
67
|
+
if (!validModes.includes(nextMode)) {
|
|
68
|
+
ctx.setEditingMode(false);
|
|
69
|
+
ctx.setEditBuffer('');
|
|
70
|
+
ctx.setMcpAllowAllConfirmationTarget(null);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (nextMode === 'allow-all' && entry.trustMode !== 'allow-all') {
|
|
74
|
+
ctx.setMcpAllowAllConfirmationTarget(entry.name);
|
|
75
|
+
ctx.setEditBuffer('');
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
ctx.mcpRegistry.setServerTrustMode(entry.name, nextMode);
|
|
79
|
+
ctx.setMcpEntries(buildMcpEntries(ctx.mcpRegistry));
|
|
80
|
+
ctx.setEditingMode(false);
|
|
81
|
+
ctx.setEditBuffer('');
|
|
82
|
+
ctx.setMcpAllowAllConfirmationTarget(null);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const entry = ctx.getSelected();
|
|
87
|
+
if (!entry || !ctx.configManager) return false;
|
|
88
|
+
|
|
89
|
+
const { setting } = entry;
|
|
90
|
+
let parsed: unknown = ctx.editBuffer;
|
|
91
|
+
|
|
92
|
+
if (setting.type === 'number') {
|
|
93
|
+
parsed = Number(ctx.editBuffer);
|
|
94
|
+
if (isNaN(parsed as number)) {
|
|
95
|
+
ctx.setEditingMode(false);
|
|
96
|
+
ctx.setEditBuffer('');
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (setting.type === 'object') {
|
|
102
|
+
// Object-typed keys (e.g. pricing.modelPrices) edit as JSON; the
|
|
103
|
+
// schema's validate() below still rules on the parsed shape.
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(ctx.editBuffer);
|
|
106
|
+
} catch {
|
|
107
|
+
ctx.setEditingMode(false);
|
|
108
|
+
ctx.setEditBuffer('');
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (setting.validate && !setting.validate(parsed)) {
|
|
114
|
+
ctx.setEditingMode(false);
|
|
115
|
+
ctx.setEditBuffer('');
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isWorktreeSetupListConfigKey(setting.key)) {
|
|
120
|
+
// Comma-separated display/edit convention for the array-backed
|
|
121
|
+
// worktree.setup.* keys — see worktree-setup-config.ts.
|
|
122
|
+
ctx.setValue(setting.key, parseWorktreeSetupListInput(ctx.editBuffer));
|
|
123
|
+
} else if (isSandboxExecListConfigKey(setting.key)) {
|
|
124
|
+
// Same comma-separated convention for the array-backed
|
|
125
|
+
// sandbox.egressAllowlist / sandbox.workspaceWritable keys — see
|
|
126
|
+
// sandbox-exec-config.ts.
|
|
127
|
+
ctx.setValue(setting.key, parseSandboxExecListInput(ctx.editBuffer));
|
|
128
|
+
} else if (isExecEnvScrubAllowlistConfigKey(setting.key)) {
|
|
129
|
+
// Same comma-separated convention for permissions.execEnvScrubAllowlist
|
|
130
|
+
// — see exec-env-scrub-config.ts.
|
|
131
|
+
ctx.setValue(setting.key, parseExecEnvScrubAllowlistInput(ctx.editBuffer));
|
|
132
|
+
} else if (setting.type === 'string' && isSecretConfigKey(setting.key)) {
|
|
133
|
+
setSecretBackedSettingValue({
|
|
134
|
+
key: setting.key,
|
|
135
|
+
value: String(parsed ?? ''),
|
|
136
|
+
configManager: ctx.configManager,
|
|
137
|
+
secretsManager: ctx.secretsManager,
|
|
138
|
+
setConfigValue: (key, value) => ctx.setValue(key, value),
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
ctx.setValue(setting.key as ConfigKey, parsed);
|
|
142
|
+
}
|
|
143
|
+
ctx.setEditingMode(false);
|
|
144
|
+
ctx.setEditBuffer('');
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-modal-connections.ts — the Connections category of the settings
|
|
3
|
+
* workspace: whether mail and calendar are actually usable, and if not, the
|
|
4
|
+
* exact next step.
|
|
5
|
+
*
|
|
6
|
+
* This category exists because a capability reachable only by typing a slash
|
|
7
|
+
* command is not, as far as someone clicking through the workspace is
|
|
8
|
+
* concerned, wired up at all. `/mail status` and `/calendar status` answer the
|
|
9
|
+
* same question; a person opening `/settings` to see what is connected should
|
|
10
|
+
* not have to already know those commands exist.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the rows arrive asynchronously
|
|
13
|
+
*
|
|
14
|
+
* Every other settings category reads a value out of config and renders it.
|
|
15
|
+
* This one cannot: "is mail connected?" is only answerable by asking the
|
|
16
|
+
* daemon's handler, which is real I/O. The workspace renders synchronously, so
|
|
17
|
+
* the rows start in `checking` — which is true — and the refresh replaces them
|
|
18
|
+
* and asks for a re-render, the same contract the Services surface uses for its
|
|
19
|
+
* inspections. Nothing is ever shown as "not configured" merely because an
|
|
20
|
+
* answer has not arrived yet; that would be a guess rendered as a fact.
|
|
21
|
+
*
|
|
22
|
+
* No credential is read, held, or rendered here. The probe asks each surface
|
|
23
|
+
* for one item and keeps only whether it failed and why.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
27
|
+
import {
|
|
28
|
+
connectionSurfaceLabel,
|
|
29
|
+
probeConnection,
|
|
30
|
+
type ConnectionStatus,
|
|
31
|
+
type ConnectionSurface,
|
|
32
|
+
} from './commands/connection-status.ts';
|
|
33
|
+
|
|
34
|
+
/** The surfaces this category covers, in display order. */
|
|
35
|
+
export const CONNECTION_SURFACES: readonly ConnectionSurface[] = ['mail', 'calendar'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The pre-probe rows.
|
|
39
|
+
*
|
|
40
|
+
* Stated as `checking` rather than blank so the category is never empty and
|
|
41
|
+
* never asserts a state it has not established.
|
|
42
|
+
*/
|
|
43
|
+
export function initialConnectionEntries(): ConnectionStatus[] {
|
|
44
|
+
return CONNECTION_SURFACES.map((surface) => ({
|
|
45
|
+
surface,
|
|
46
|
+
state: 'checking' as const,
|
|
47
|
+
detail: `Asking the daemon whether ${connectionSurfaceLabel(surface).toLowerCase()} is reachable…`,
|
|
48
|
+
nextActions: [],
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Probe every surface. Returns rows in `CONNECTION_SURFACES` order so the
|
|
54
|
+
* category never reorders itself under the cursor between refreshes.
|
|
55
|
+
*/
|
|
56
|
+
export async function buildConnectionEntries(
|
|
57
|
+
gateway: GatewayMethodCatalog | undefined,
|
|
58
|
+
): Promise<ConnectionStatus[]> {
|
|
59
|
+
return Promise.all(CONNECTION_SURFACES.map((surface) => probeConnection(gateway, surface)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The mutable slice of the settings workspace this module drives. Declared
|
|
64
|
+
* structurally rather than importing the class, so the split stays a file-size
|
|
65
|
+
* split and does not become an import cycle.
|
|
66
|
+
*/
|
|
67
|
+
export interface ConnectionsHost {
|
|
68
|
+
active: boolean;
|
|
69
|
+
selectedIndex: number;
|
|
70
|
+
connectionEntries: ConnectionStatus[];
|
|
71
|
+
connectionsRefreshing: boolean;
|
|
72
|
+
gatewayMethods: GatewayMethodCatalog | null;
|
|
73
|
+
requestRender: (() => void) | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Refresh the rows from the daemon and repaint.
|
|
78
|
+
*
|
|
79
|
+
* A second call while one is in flight is dropped: entering the tab repeatedly
|
|
80
|
+
* should not queue a probe per keystroke. A probe that lands after the
|
|
81
|
+
* workspace closed is discarded rather than written, so reopening starts from
|
|
82
|
+
* `checking` instead of resurrecting an answer about a finished session.
|
|
83
|
+
*/
|
|
84
|
+
export async function refreshConnectionEntries(host: ConnectionsHost): Promise<void> {
|
|
85
|
+
if (host.connectionsRefreshing) return;
|
|
86
|
+
host.connectionsRefreshing = true;
|
|
87
|
+
try {
|
|
88
|
+
const entries = await buildConnectionEntries(host.gatewayMethods ?? undefined);
|
|
89
|
+
if (!host.active) return;
|
|
90
|
+
host.connectionEntries = entries;
|
|
91
|
+
host.requestRender?.();
|
|
92
|
+
} finally {
|
|
93
|
+
host.connectionsRefreshing = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The selected row, clamped, or null when the category is empty. */
|
|
98
|
+
export function selectedConnectionEntry(host: ConnectionsHost): ConnectionStatus | null {
|
|
99
|
+
const entries = host.connectionEntries;
|
|
100
|
+
if (entries.length === 0) return null;
|
|
101
|
+
return entries[Math.max(0, Math.min(entries.length - 1, host.selectedIndex))] ?? null;
|
|
102
|
+
}
|
|
@@ -55,6 +55,11 @@ import { enrichRelaySettingDescriptions } from './relay-settings-descriptions.ts
|
|
|
55
55
|
import { enrichDaemonOwnedSettingDescriptions } from './daemon-owned-settings-descriptions.ts';
|
|
56
56
|
import { applyFeatureUnitLayout } from './feature-unit-layout.ts';
|
|
57
57
|
import { FEATURE_SETTINGS_BY_ID } from '../runtime/feature-settings.ts';
|
|
58
|
+
import {
|
|
59
|
+
buildPaymentsSyntheticEntries,
|
|
60
|
+
isPaymentsSyntheticConfigKey,
|
|
61
|
+
refreshPaymentsSyntheticEntry,
|
|
62
|
+
} from './payments-config.ts';
|
|
58
63
|
|
|
59
64
|
// ---------------------------------------------------------------------------
|
|
60
65
|
// deepEqual — structural equality for isDefault comparisons
|
|
@@ -249,6 +254,19 @@ export function buildSettingGroups(
|
|
|
249
254
|
injectSandboxExecSyntheticEntries(groups, configManager);
|
|
250
255
|
injectExecEnvScrubSyntheticEntry(groups, configManager);
|
|
251
256
|
|
|
257
|
+
// Payment card capability (see payments-config.ts): the CONFIG_SCHEMA loop
|
|
258
|
+
// above already populated 'payments' with the fourteen real SDK settings
|
|
259
|
+
// (enabled, defaultCardId, currency, cvvHandling, budgets, shipping,
|
|
260
|
+
// windows, notifyChannels). What's injected here are the four secret-tier
|
|
261
|
+
// card-material fields and the two ordinary address fields, which are
|
|
262
|
+
// TUI-local synthetic sub-keys under that real section (not yet SDK
|
|
263
|
+
// CONFIG_SCHEMA entries — see payments-config.ts's header comment), backed
|
|
264
|
+
// by the same real configManager as everything else in this group.
|
|
265
|
+
const paymentsEntries = groups.get('payments');
|
|
266
|
+
if (paymentsEntries && !paymentsEntries.some((e) => isPaymentsSyntheticConfigKey(e.setting.key))) {
|
|
267
|
+
paymentsEntries.push(...buildPaymentsSyntheticEntries(configManager));
|
|
268
|
+
}
|
|
269
|
+
|
|
252
270
|
// relay.* is a real SDK CONFIG_SCHEMA domain; append the threat-model note
|
|
253
271
|
// the SDK's own descriptions don't carry (see relay-settings-descriptions.ts).
|
|
254
272
|
enrichRelaySettingDescriptions(groups);
|
|
@@ -656,6 +674,14 @@ export function refreshEntryValues(
|
|
|
656
674
|
entry.currentValue = refreshed.currentValue; entry.isDefault = refreshed.isDefault;
|
|
657
675
|
continue;
|
|
658
676
|
}
|
|
677
|
+
// payments.* synthetic sub-keys (card material + addresses — see
|
|
678
|
+
// payments-config.ts): real configManager-backed now, but still
|
|
679
|
+
// defensively normalized (undefined -> '') the same way the initial
|
|
680
|
+
// build does, so isDefault stays consistent between build and refresh.
|
|
681
|
+
if (isPaymentsSyntheticConfigKey(entry.setting.key)) {
|
|
682
|
+
refreshPaymentsSyntheticEntry(entry, configManager);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
659
685
|
const raw = configManager.get(entry.setting.key as ConfigKey);
|
|
660
686
|
// Synthetic entries (e.g. tts.speed) that have no SDK schema key return
|
|
661
687
|
// undefined from configManager. Normalize using the same logic used at
|
|
@@ -694,6 +720,10 @@ export function updateEntryForKey(
|
|
|
694
720
|
entry.currentValue = refreshed.currentValue; entry.isDefault = refreshed.isDefault;
|
|
695
721
|
continue;
|
|
696
722
|
}
|
|
723
|
+
if (isPaymentsSyntheticConfigKey(key)) {
|
|
724
|
+
refreshPaymentsSyntheticEntry(entry, configManager);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
697
727
|
if (isSandboxExecListConfigKey(key)) {
|
|
698
728
|
entry.currentValue = readSandboxExecList(configManager, key);
|
|
699
729
|
entry.isDefault = (entry.currentValue as string[]).length === 0;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
17
|
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
18
|
-
import { buildGoodVibesSecretKey, isSecretConfigKey } from '../config/secret-config.ts';
|
|
18
|
+
import { buildGoodVibesSecretKey, defaultSecretBackedScope, isSecretConfigKey } from '../config/secret-config.ts';
|
|
19
19
|
import type { SettingEntry, SettingsCategory } from './settings-modal-types.ts';
|
|
20
20
|
import type { SettingsSecretsManager } from './settings-modal-secrets.ts';
|
|
21
21
|
|
|
@@ -41,7 +41,11 @@ export function resetSelected({
|
|
|
41
41
|
const key = selected.setting.key as ConfigKey;
|
|
42
42
|
setValue(key, selected.setting.default);
|
|
43
43
|
if (isSecretConfigKey(key) && secretsManager) {
|
|
44
|
-
|
|
44
|
+
// Delete from the tier the write went to. A daemon-owned key was stored in
|
|
45
|
+
// the daemon tier (defaultSecretBackedScope), so a delete narrowed to 'user'
|
|
46
|
+
// would report a cleared setting while the daemon kept using the live
|
|
47
|
+
// credential — the reset that isn't one.
|
|
48
|
+
void secretsManager.delete(buildGoodVibesSecretKey(key), { scope: defaultSecretBackedScope(key) }).catch((error) => {
|
|
45
49
|
logger.error('SettingsModal: failed to clear secret while resetting setting', { key, error: summarizeError(error) });
|
|
46
50
|
});
|
|
47
51
|
}
|
|
@@ -5,6 +5,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
5
5
|
import type { SecretsManager } from '../config/secrets.ts';
|
|
6
6
|
import {
|
|
7
7
|
buildSecretBackedConfigUpdate,
|
|
8
|
+
defaultSecretBackedScope,
|
|
8
9
|
getSecretWriteMedium,
|
|
9
10
|
} from '../config/secret-config.ts';
|
|
10
11
|
|
|
@@ -24,16 +25,22 @@ export function setSecretBackedSettingValue(args: {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const update = buildSecretBackedConfigUpdate(key, value);
|
|
28
|
+
// A daemon-owned key (surfaces.*, payments.*, controlPlane.*, ...) names a
|
|
29
|
+
// credential the daemon itself executes with, so its secret material lands in
|
|
30
|
+
// the daemon tier — the one the daemon reads with every surface closed —
|
|
31
|
+
// regardless of which client edited it. Everything else stays at user scope.
|
|
32
|
+
// See secret-config.ts's defaultSecretBackedScope.
|
|
33
|
+
const scope = defaultSecretBackedScope(key);
|
|
27
34
|
if (update.secretKey && update.secretValue !== undefined) {
|
|
28
35
|
void secretsManager.set(update.secretKey, update.secretValue, {
|
|
29
|
-
scope
|
|
36
|
+
scope,
|
|
30
37
|
medium: getSecretWriteMedium(configManager.get('storage.secretPolicy')),
|
|
31
38
|
}).catch((error) => {
|
|
32
39
|
logger.error('SettingsModal: failed to store secret config value', { key, error: summarizeError(error) });
|
|
33
40
|
});
|
|
34
41
|
}
|
|
35
42
|
if (update.clearSecretKey) {
|
|
36
|
-
void secretsManager.delete(update.clearSecretKey, { scope
|
|
43
|
+
void secretsManager.delete(update.clearSecretKey, { scope }).catch((error) => {
|
|
37
44
|
logger.error('SettingsModal: failed to clear secret config value', { key, error: summarizeError(error) });
|
|
38
45
|
});
|
|
39
46
|
}
|
|
@@ -45,12 +45,24 @@ export type SettingsCategory =
|
|
|
45
45
|
| 'agents'
|
|
46
46
|
| 'security'
|
|
47
47
|
| 'integrations'
|
|
48
|
+
| 'connections'
|
|
48
49
|
| 'policy'
|
|
49
50
|
| 'notifications'
|
|
50
51
|
| 'release'
|
|
51
52
|
| 'update'
|
|
52
53
|
| 'power'
|
|
53
54
|
| 'memory'
|
|
55
|
+
// payments.* — the card on file for daemon-initiated purchases. Like
|
|
56
|
+
// 'profile' below, this entry and its SETTINGS_CATEGORY_GROUPS membership are
|
|
57
|
+
// both mandatory or the whole prefix is dropped from the workspace.
|
|
58
|
+
| 'payments'
|
|
59
|
+
// profile.* — the owner profile (docs/owner-profile.md §12.1). This entry and
|
|
60
|
+
// its SETTINGS_CATEGORY_GROUPS membership below are both mandatory:
|
|
61
|
+
// buildSettingGroups guards every push with `if (groups.has(cat))`, so a
|
|
62
|
+
// config prefix with no category here is dropped from the workspace entirely
|
|
63
|
+
// and reachable only by hand-editing a settings file — see the push.* and
|
|
64
|
+
// cluster.* comments in settings-modal-data.ts for the two times that happened.
|
|
65
|
+
| 'profile'
|
|
54
66
|
| 'danger';
|
|
55
67
|
|
|
56
68
|
export type SettingsFocusPane = 'categories' | 'settings';
|
|
@@ -62,9 +74,13 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
|
62
74
|
{ label: 'Interface', categories: ['display', 'ui', 'behavior', 'notifications', 'permissions', 'policy', 'security'] },
|
|
63
75
|
{ label: 'AI Routing', categories: ['provider', 'pricing', 'subscriptions', 'helper', 'tools', 'tts', 'voice'] },
|
|
64
76
|
{ label: 'Service & Network', categories: ['service', 'daemon', 'network', 'controlPlane', 'httpListener', 'web', 'relay'] },
|
|
65
|
-
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'device', 'conversationGate', 'integrations', 'mcp', 'cloudflare'] },
|
|
66
|
-
{ label: 'Automation', categories: ['batch', 'automation', 'checkin', 'watchers', 'orchestration', 'planner', 'wrfc'] },
|
|
67
|
-
|
|
77
|
+
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'device', 'conversationGate', 'integrations', 'connections', 'mcp', 'cloudflare'] },
|
|
78
|
+
{ label: 'Automation', categories: ['batch', 'automation', 'checkin', 'watchers', 'orchestration', 'planner', 'wrfc', 'payments'] },
|
|
79
|
+
// 'profile' sits beside 'memory' and 'learning': all three are what the
|
|
80
|
+
// platform retains about the person using it, and this is the group a reader
|
|
81
|
+
// looking for "what does it know about me" already scans — Interface is
|
|
82
|
+
// presentation and permission posture, and nothing there is a store of facts.
|
|
83
|
+
{ label: 'Runtime & Data', categories: ['storage', 'atRest', 'sandbox', 'fetch', 'agents', 'runtime', 'power', 'profile', 'memory', 'cache', 'telemetry', 'diagnostics', 'learning'] },
|
|
68
84
|
{ label: 'Advanced', categories: ['release', 'update', 'danger'] },
|
|
69
85
|
];
|
|
70
86
|
|