@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +9605 -3119
  4. package/package.json +9 -7
  5. package/scripts/postinstall.js +57 -1
  6. package/src/audio/activation-sound.ts +117 -0
  7. package/src/audio/capture.ts +112 -0
  8. package/src/audio/onnxruntime-assets.d.ts +24 -0
  9. package/src/audio/voice-capture-wiring.ts +119 -0
  10. package/src/audio/voice-input-session.ts +181 -0
  11. package/src/audio/wake-inference.ts +182 -0
  12. package/src/audio/wake-runtime.ts +341 -0
  13. package/src/cli/bundle-command.ts +4 -1
  14. package/src/cli/help.ts +10 -0
  15. package/src/cli/management-utils.ts +4 -1
  16. package/src/cli/redaction.ts +68 -3
  17. package/src/config/goodvibes-home.ts +10 -0
  18. package/src/config/secret-config.ts +64 -8
  19. package/src/core/turn-event-wiring.ts +7 -16
  20. package/src/core/voice-capture-status.ts +53 -0
  21. package/src/core/voice-stt-gateway.ts +75 -0
  22. package/src/core/wake-provision-runner.ts +86 -0
  23. package/src/core/wake-provision-status.ts +170 -0
  24. package/src/core/workstream-notification.ts +69 -0
  25. package/src/daemon/cli.ts +86 -5
  26. package/src/daemon/handlers/credentials.ts +15 -5
  27. package/src/daemon/provision-wake-model.ts +81 -0
  28. package/src/daemon/send/channels.ts +200 -0
  29. package/src/daemon/send/command.ts +332 -0
  30. package/src/daemon/send/composition.ts +99 -0
  31. package/src/daemon/send/failure-text.ts +93 -0
  32. package/src/daemon/send/inert-text.ts +225 -0
  33. package/src/daemon/send/stdin.ts +24 -0
  34. package/src/input/command-registry.ts +6 -0
  35. package/src/input/commands/config.ts +68 -2
  36. package/src/input/commands/experience-runtime.ts +37 -3
  37. package/src/input/commands/local-provider-runtime.ts +9 -1
  38. package/src/input/commands/operator-runtime.ts +6 -1
  39. package/src/input/commands/payment-card-intake.ts +200 -0
  40. package/src/input/commands/profile-render.ts +250 -0
  41. package/src/input/commands/profile-runtime.ts +362 -0
  42. package/src/input/commands/profile-types.ts +381 -0
  43. package/src/input/commands.ts +7 -0
  44. package/src/input/composer-editor.ts +4 -0
  45. package/src/input/handler-shortcuts.ts +8 -0
  46. package/src/input/keybindings.ts +10 -1
  47. package/src/input/onboarding/onboarding-feature-units.ts +5 -5
  48. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -1
  49. package/src/input/payments-config.ts +167 -0
  50. package/src/input/provider-key-intake.ts +10 -2
  51. package/src/input/settings-modal-commit.ts +146 -0
  52. package/src/input/settings-modal-data.ts +30 -0
  53. package/src/input/settings-modal-reset.ts +6 -2
  54. package/src/input/settings-modal-secrets.ts +9 -2
  55. package/src/input/settings-modal-types.ts +30 -2
  56. package/src/input/settings-modal.ts +27 -107
  57. package/src/main.ts +12 -14
  58. package/src/panels/modals/marketplace-modal.ts +5 -0
  59. package/src/panels/modals/skills-modal.ts +5 -0
  60. package/src/renderer/process-indicator.ts +66 -0
  61. package/src/renderer/settings-modal-helpers.ts +8 -2
  62. package/src/renderer/settings-modal.ts +16 -1
  63. package/src/renderer/shell-surface.ts +22 -3
  64. package/src/runtime/bootstrap-core.ts +7 -7
  65. package/src/runtime/bootstrap.ts +3 -3
  66. package/src/runtime/device-posture-composition.ts +110 -0
  67. package/src/runtime/disposal-wiring.ts +16 -0
  68. package/src/runtime/install-self-check-startup.ts +23 -0
  69. package/src/runtime/onboarding/types.ts +6 -1
  70. package/src/runtime/path-shadow-startup.ts +243 -0
  71. package/src/runtime/path-shadow.ts +418 -0
  72. package/src/runtime/reachability-notice.ts +100 -0
  73. package/src/runtime/runtime-services-types.ts +260 -0
  74. package/src/runtime/services.ts +95 -209
  75. package/src/runtime/voice-setup-services.ts +50 -4
  76. package/src/shell/voice-capture-shell.ts +69 -0
  77. package/src/verification/verification-ledger.ts +163 -24
  78. 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?: 'project' | 'user'; medium?: 'secure' | 'plaintext' }): Promise<void>;
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
- await secretsManager.set(secretKey, value, { scope: 'user', medium: 'secure' });
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
- void secretsManager.delete(buildGoodVibesSecretKey(key), { scope: 'user' }).catch((error) => {
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: 'user',
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: 'user' }).catch((error) => {
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
- { label: 'Automation', categories: ['batch', 'automation', 'checkin', 'watchers', 'orchestration', 'planner', 'wrfc'] },
68
- { label: 'Runtime & Data', categories: ['storage', 'atRest', 'sandbox', 'fetch', 'agents', 'runtime', 'power', 'memory', 'cache', 'telemetry', 'diagnostics', 'learning'] },
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