@pellux/goodvibes-tui 1.25.0 → 1.27.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 +15 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +9605 -3119
- package/package.json +9 -7
- package/scripts/postinstall.js +57 -1
- package/src/audio/activation-sound.ts +117 -0
- package/src/audio/capture.ts +112 -0
- package/src/audio/onnxruntime-assets.d.ts +24 -0
- package/src/audio/voice-capture-wiring.ts +119 -0
- package/src/audio/voice-input-session.ts +181 -0
- package/src/audio/wake-inference.ts +182 -0
- package/src/audio/wake-runtime.ts +341 -0
- package/src/cli/bundle-command.ts +4 -1
- package/src/cli/help.ts +10 -0
- package/src/cli/management-utils.ts +4 -1
- package/src/cli/redaction.ts +68 -3
- package/src/config/goodvibes-home.ts +10 -0
- package/src/config/secret-config.ts +64 -8
- package/src/core/turn-event-wiring.ts +7 -16
- package/src/core/voice-capture-status.ts +53 -0
- package/src/core/voice-stt-gateway.ts +75 -0
- package/src/core/wake-provision-runner.ts +86 -0
- package/src/core/wake-provision-status.ts +170 -0
- package/src/core/workstream-notification.ts +69 -0
- package/src/daemon/cli.ts +86 -5
- package/src/daemon/handlers/credentials.ts +15 -5
- package/src/daemon/provision-wake-model.ts +81 -0
- 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/command-registry.ts +6 -0
- package/src/input/commands/config.ts +68 -2
- package/src/input/commands/experience-runtime.ts +37 -3
- package/src/input/commands/local-provider-runtime.ts +9 -1
- 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.ts +7 -0
- package/src/input/composer-editor.ts +4 -0
- package/src/input/handler-shortcuts.ts +8 -0
- package/src/input/keybindings.ts +10 -1
- package/src/input/onboarding/onboarding-feature-units.ts +5 -5
- 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-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 +30 -2
- package/src/input/settings-modal.ts +27 -107
- package/src/main.ts +12 -14
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/process-indicator.ts +66 -0
- package/src/renderer/settings-modal-helpers.ts +8 -2
- package/src/renderer/settings-modal.ts +16 -1
- package/src/renderer/shell-surface.ts +22 -3
- package/src/runtime/bootstrap-core.ts +7 -7
- package/src/runtime/bootstrap.ts +3 -3
- package/src/runtime/device-posture-composition.ts +110 -0
- package/src/runtime/disposal-wiring.ts +16 -0
- package/src/runtime/install-self-check-startup.ts +23 -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 +260 -0
- package/src/runtime/services.ts +95 -209
- package/src/runtime/voice-setup-services.ts +50 -4
- package/src/shell/voice-capture-shell.ts +69 -0
- package/src/verification/verification-ledger.ts +163 -24
- package/src/version.ts +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* payments-config.ts — TUI-local synthetic settings for the payment card
|
|
3
|
+
* capability's card MATERIAL fields.
|
|
4
|
+
*
|
|
5
|
+
* The SDK's CONFIG_SCHEMA now has a full real `payments` section (28 keys:
|
|
6
|
+
* enabled, defaultCardId, currency, cvvHandling, the six budget keys,
|
|
7
|
+
* shipping.preferredTier, the fourteen billing/shipping address sub-fields
|
|
8
|
+
* (name, line1, line2, city, region, postalCode, country — each repeated for
|
|
9
|
+
* billingAddress and shippingAddress), the two window keys, and
|
|
10
|
+
* notifyChannels — see `@pellux/goodvibes-sdk/platform/config`). Every one of
|
|
11
|
+
* those keys reads and writes through the ordinary CONFIG_SCHEMA-driven path
|
|
12
|
+
* in settings-modal-data.ts's buildSettingGroups, exactly like `relay.*` or
|
|
13
|
+
* any other real SDK domain — no synthetic entry or dedicated store is needed
|
|
14
|
+
* for them, and this module builds none. (An earlier round of this module
|
|
15
|
+
* modeled billing/shipping address as two flat TUI-local strings; the SDK's
|
|
16
|
+
* own structured per-field keys superseded that the moment they shipped, so
|
|
17
|
+
* those two synthetic entries are gone from this file entirely.)
|
|
18
|
+
*
|
|
19
|
+
* What this module still builds synthetic entries for is the four card
|
|
20
|
+
* MATERIAL fields: number, expiry, CVV, cardholder name. Per the SDK's own
|
|
21
|
+
* design (see its schema-domain-payments.ts header and `platform/payments`'s
|
|
22
|
+
* method catalog), card material is deliberately never a config path at all:
|
|
23
|
+
* it lives write-only in the daemon secret store, named by keys the daemon's
|
|
24
|
+
* own `payments.cards.create` control-plane method derives internally. The
|
|
25
|
+
* SDK does not yet expose an equivalent config-level path for a single
|
|
26
|
+
* implicit card the way this app's `/payments card` flow models it, so these
|
|
27
|
+
* four fields are synthetic sub-keys under the SDK's real `payments`
|
|
28
|
+
* section — the same established pattern this codebase already uses for
|
|
29
|
+
* `tts.speed`, `behavior.notifyAfterSeconds`, etc.: a key one level under an
|
|
30
|
+
* EXISTING section that CONFIG_SCHEMA hasn't grown a scalar entry for yet.
|
|
31
|
+
*
|
|
32
|
+
* That one-level-under-an-existing-section shape matters mechanically: the
|
|
33
|
+
* real ConfigManager's dotted-path resolver only throws "Invalid config path"
|
|
34
|
+
* when an INTERMEDIATE segment is missing (e.g. `payments.card.number`, where
|
|
35
|
+
* `card` is not itself a section) — it does not throw for a final leaf that
|
|
36
|
+
* the schema hasn't declared (`payments.cardNumber`), the same tolerance
|
|
37
|
+
* `tts.speed` already relies on under `tts`. So these four keys are named
|
|
38
|
+
* FLAT (`payments.cardNumber`, not `payments.card.number`) specifically so
|
|
39
|
+
* `ConfigManager.get/setDynamic` accepts them — verified against the real
|
|
40
|
+
* ConfigManager (not the fake store this module used before this round): a
|
|
41
|
+
* flat sub-key persists to the real daemon-owned settings file precisely
|
|
42
|
+
* because `payments.` is a `DAEMON_OWNED_CONFIG_PREFIXES` entry, which is
|
|
43
|
+
* exactly what makes the daemon (and every other surface) able to see it
|
|
44
|
+
* after this TUI closes — the thing the local JSON store this module used to
|
|
45
|
+
* use never gave them.
|
|
46
|
+
*
|
|
47
|
+
* All four keys are secret-tier (see config/secret-config.ts's
|
|
48
|
+
* SECRET_CONFIG_KEYS). Primary entry for those is the concealed-input flow in
|
|
49
|
+
* commands/payment-card-intake.ts, itself gated on the SDK's
|
|
50
|
+
* `mayOfferCardEntryFlow` (card details may only be typed at a local
|
|
51
|
+
* terminal or the webui, never over a remote messaging surface — see that
|
|
52
|
+
* file's header); they are also reachable as ordinary secret-backed settings
|
|
53
|
+
* rows here, which is what makes them masked mid-edit — see
|
|
54
|
+
* renderer/settings-modal.ts's currentSettingValue().
|
|
55
|
+
*
|
|
56
|
+
* cvvHandling — real now, not built here — selects whether the CVV is stored
|
|
57
|
+
* (the default, per the 2026-07-27 payment-capability ruling) or requested at
|
|
58
|
+
* purchase time; see `CVV_PROMPT_TRADEOFF_WARNING`, imported directly from
|
|
59
|
+
* `@pellux/goodvibes-sdk/platform/payments` by every caller that needs it
|
|
60
|
+
* (renderer/settings-modal.ts, settings-modal.ts) — no local copy lives here
|
|
61
|
+
* or anywhere else in this app.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
65
|
+
import type { ConfigKey } from '../config/index.ts';
|
|
66
|
+
import type { SettingEntry } from './settings-modal-types.ts';
|
|
67
|
+
|
|
68
|
+
/** Real ConfigManager's read surface — these four keys are defensive reads (see header comment). */
|
|
69
|
+
export type PaymentsConfigReader = Pick<ConfigManager, 'get'>;
|
|
70
|
+
|
|
71
|
+
export const PAYMENTS_CARD_NUMBER_CONFIG_KEY = 'payments.cardNumber' as ConfigKey;
|
|
72
|
+
export const PAYMENTS_CARD_EXPIRY_CONFIG_KEY = 'payments.cardExpiry' as ConfigKey;
|
|
73
|
+
export const PAYMENTS_CARD_CVV_CONFIG_KEY = 'payments.cardCvv' as ConfigKey;
|
|
74
|
+
export const PAYMENTS_CARD_CARDHOLDER_NAME_CONFIG_KEY = 'payments.cardholderName' as ConfigKey;
|
|
75
|
+
|
|
76
|
+
/** The real SDK schema key for the CVV-handling selector — no longer synthetic. */
|
|
77
|
+
export const PAYMENTS_CVV_HANDLING_CONFIG_KEY = 'payments.cvvHandling' as ConfigKey;
|
|
78
|
+
|
|
79
|
+
function readStringField(configManager: PaymentsConfigReader, key: ConfigKey): string {
|
|
80
|
+
// Defensive try/catch, same rationale as worktree-setup-config.ts's
|
|
81
|
+
// readWorktreeSetupList and this codebase's other one-level-under-a-real-
|
|
82
|
+
// section synthetic reads (tts.speed, behavior.notifyAfterSeconds): the real
|
|
83
|
+
// ConfigManager never throws for these particular keys (verified — see
|
|
84
|
+
// header comment), but degrading to empty rather than crashing the whole
|
|
85
|
+
// settings modal is the same posture every synthetic setting here takes
|
|
86
|
+
// toward an unexpected value.
|
|
87
|
+
try {
|
|
88
|
+
const raw = configManager.get(key);
|
|
89
|
+
return typeof raw === 'string' ? raw : '';
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildStringFieldEntry(
|
|
96
|
+
configManager: PaymentsConfigReader,
|
|
97
|
+
key: ConfigKey,
|
|
98
|
+
description: string,
|
|
99
|
+
): SettingEntry {
|
|
100
|
+
const currentValue = readStringField(configManager, key);
|
|
101
|
+
return {
|
|
102
|
+
setting: { key, type: 'string', default: '', description },
|
|
103
|
+
currentValue,
|
|
104
|
+
isDefault: currentValue === '',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildPaymentsCardNumberEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
109
|
+
return buildStringFieldEntry(
|
|
110
|
+
configManager,
|
|
111
|
+
PAYMENTS_CARD_NUMBER_CONFIG_KEY,
|
|
112
|
+
'Payment card number. Stored through the secret manager at daemon scope; entering it here or via /payments card never shows the typed characters in plaintext.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildPaymentsCardExpiryEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
117
|
+
return buildStringFieldEntry(
|
|
118
|
+
configManager,
|
|
119
|
+
PAYMENTS_CARD_EXPIRY_CONFIG_KEY,
|
|
120
|
+
'Payment card expiry (MM/YY). Stored through the secret manager at daemon scope, same handling as the card number.',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildPaymentsCardCvvEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
125
|
+
return buildStringFieldEntry(
|
|
126
|
+
configManager,
|
|
127
|
+
PAYMENTS_CARD_CVV_CONFIG_KEY,
|
|
128
|
+
'Payment card CVV. Stored through the secret manager at daemon scope: never logged, never rendered, never shown mid-edit, excluded from every export and diagnostic dump.',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildPaymentsCardCardholderNameEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
133
|
+
return buildStringFieldEntry(
|
|
134
|
+
configManager,
|
|
135
|
+
PAYMENTS_CARD_CARDHOLDER_NAME_CONFIG_KEY,
|
|
136
|
+
'Name on the payment card. Stored through the secret manager at daemon scope, same handling as the card number.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** All payments synthetic entries (the four card-material fields), in the order they should render. */
|
|
141
|
+
export function buildPaymentsSyntheticEntries(configManager: PaymentsConfigReader): SettingEntry[] {
|
|
142
|
+
return [
|
|
143
|
+
buildPaymentsCardNumberEntry(configManager),
|
|
144
|
+
buildPaymentsCardExpiryEntry(configManager),
|
|
145
|
+
buildPaymentsCardCvvEntry(configManager),
|
|
146
|
+
buildPaymentsCardCardholderNameEntry(configManager),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Every payments synthetic config key this module owns, for refresh-loop membership checks. */
|
|
151
|
+
export const PAYMENTS_SYNTHETIC_CONFIG_KEYS: readonly ConfigKey[] = [
|
|
152
|
+
PAYMENTS_CARD_NUMBER_CONFIG_KEY,
|
|
153
|
+
PAYMENTS_CARD_EXPIRY_CONFIG_KEY,
|
|
154
|
+
PAYMENTS_CARD_CVV_CONFIG_KEY,
|
|
155
|
+
PAYMENTS_CARD_CARDHOLDER_NAME_CONFIG_KEY,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
export function isPaymentsSyntheticConfigKey(key: string): key is ConfigKey {
|
|
159
|
+
return (PAYMENTS_SYNTHETIC_CONFIG_KEYS as readonly string[]).includes(key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Refresh one payments synthetic entry's currentValue/isDefault in place. */
|
|
163
|
+
export function refreshPaymentsSyntheticEntry(entry: SettingEntry, configManager: PaymentsConfigReader): void {
|
|
164
|
+
const currentValue = readStringField(configManager, entry.setting.key as ConfigKey);
|
|
165
|
+
entry.currentValue = currentValue;
|
|
166
|
+
entry.isDefault = currentValue === '';
|
|
167
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -52,6 +52,25 @@ export type SettingsCategory =
|
|
|
52
52
|
| 'update'
|
|
53
53
|
| 'power'
|
|
54
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'
|
|
66
|
+
// occasions.* — the proactive occasions/plans loop (docs/occasions.md §8):
|
|
67
|
+
// lead time, active hours, nudge channel and cadence, away-date adjustment,
|
|
68
|
+
// calendar mirroring, interview length, gift-history retention, and the
|
|
69
|
+
// sweep interval. Same mandatory-dual-membership rule as profile above, or
|
|
70
|
+
// all twelve keys are dropped from the workspace and reachable only by
|
|
71
|
+
// hand-editing a settings file — a third occurrence of the push.*/cluster.*
|
|
72
|
+
// class the profile comment already names two of.
|
|
73
|
+
| 'occasions'
|
|
55
74
|
| 'danger';
|
|
56
75
|
|
|
57
76
|
export type SettingsFocusPane = 'categories' | 'settings';
|
|
@@ -64,8 +83,17 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
|
64
83
|
{ label: 'AI Routing', categories: ['provider', 'pricing', 'subscriptions', 'helper', 'tools', 'tts', 'voice'] },
|
|
65
84
|
{ label: 'Service & Network', categories: ['service', 'daemon', 'network', 'controlPlane', 'httpListener', 'web', 'relay'] },
|
|
66
85
|
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'device', 'conversationGate', 'integrations', 'connections', 'mcp', 'cloudflare'] },
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
// 'occasions' sits beside 'checkin': both are proactive background loops
|
|
87
|
+
// (a cadence, a judgment pass, a conditional channel delivery) rather than
|
|
88
|
+
// facts held about the owner — the data occasions/plans declare lives in
|
|
89
|
+
// the owner profile file itself (docs/occasions.md §3), but these SETTINGS
|
|
90
|
+
// rows tune the loop, exactly like checkin's.
|
|
91
|
+
{ label: 'Automation', categories: ['batch', 'automation', 'checkin', 'occasions', 'watchers', 'orchestration', 'planner', 'wrfc', 'payments'] },
|
|
92
|
+
// 'profile' sits beside 'memory' and 'learning': all three are what the
|
|
93
|
+
// platform retains about the person using it, and this is the group a reader
|
|
94
|
+
// looking for "what does it know about me" already scans — Interface is
|
|
95
|
+
// presentation and permission posture, and nothing there is a store of facts.
|
|
96
|
+
{ label: 'Runtime & Data', categories: ['storage', 'atRest', 'sandbox', 'fetch', 'agents', 'runtime', 'power', 'profile', 'memory', 'cache', 'telemetry', 'diagnostics', 'learning'] },
|
|
69
97
|
{ label: 'Advanced', categories: ['release', 'update', 'danger'] },
|
|
70
98
|
];
|
|
71
99
|
|