@pellux/goodvibes-tui 1.25.0 → 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 +15 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +6616 -2776
- 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 +10 -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 +48 -4
- package/src/daemon/handlers/credentials.ts +15 -5
- 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/config.ts +68 -2
- 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/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 +17 -2
- package/src/input/settings-modal.ts +27 -107
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/settings-modal-helpers.ts +4 -0
- package/src/renderer/settings-modal.ts +16 -1
- package/src/runtime/bootstrap.ts +2 -2
- 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 +243 -0
- package/src/runtime/services.ts +14 -205
- package/src/verification/verification-ledger.ts +104 -4
- package/src/version.ts +1 -1
|
@@ -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,17 @@ 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'
|
|
55
66
|
| 'danger';
|
|
56
67
|
|
|
57
68
|
export type SettingsFocusPane = 'categories' | 'settings';
|
|
@@ -64,8 +75,12 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
|
64
75
|
{ label: 'AI Routing', categories: ['provider', 'pricing', 'subscriptions', 'helper', 'tools', 'tts', 'voice'] },
|
|
65
76
|
{ label: 'Service & Network', categories: ['service', 'daemon', 'network', 'controlPlane', 'httpListener', 'web', 'relay'] },
|
|
66
77
|
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'device', 'conversationGate', 'integrations', 'connections', 'mcp', 'cloudflare'] },
|
|
67
|
-
{ label: 'Automation', categories: ['batch', 'automation', 'checkin', 'watchers', 'orchestration', 'planner', 'wrfc'] },
|
|
68
|
-
|
|
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'] },
|
|
69
84
|
{ label: 'Advanced', categories: ['release', 'update', 'danger'] },
|
|
70
85
|
];
|
|
71
86
|
|
|
@@ -21,19 +21,14 @@ import type { ModelPickerTarget } from './model-picker.ts';
|
|
|
21
21
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
22
22
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
23
23
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
type SettingsSecretsManager,
|
|
28
|
-
} from './settings-modal-secrets.ts';
|
|
24
|
+
import type { SettingsSecretsManager } from './settings-modal-secrets.ts';
|
|
25
|
+
import { CVV_PROMPT_TRADEOFF_WARNING } from '@pellux/goodvibes-sdk/platform/payments';
|
|
26
|
+
import { PAYMENTS_CVV_HANDLING_CONFIG_KEY } from './payments-config.ts';
|
|
29
27
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
30
28
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
31
29
|
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
32
30
|
import type { ConnectionStatus } from './commands/connection-status.ts';
|
|
33
31
|
import { initialConnectionEntries, refreshConnectionEntries } from './settings-modal-connections.ts';
|
|
34
|
-
import { isWorktreeSetupListConfigKey, parseWorktreeSetupListInput } from './worktree-setup-config.ts';
|
|
35
|
-
import { isSandboxExecListConfigKey, parseSandboxExecListInput } from './sandbox-exec-config.ts';
|
|
36
|
-
import { isExecEnvScrubAllowlistConfigKey, parseExecEnvScrubAllowlistInput } from './exec-env-scrub-config.ts';
|
|
37
32
|
|
|
38
33
|
import {
|
|
39
34
|
SETTINGS_CATEGORIES,
|
|
@@ -74,6 +69,7 @@ import {
|
|
|
74
69
|
handleResetConfirmKey as _handleResetConfirmKey,
|
|
75
70
|
type ResetConfirmKeyResult,
|
|
76
71
|
} from './settings-modal-reset.ts';
|
|
72
|
+
import { commitEditValue as _commitEditValue } from './settings-modal-commit.ts';
|
|
77
73
|
|
|
78
74
|
export interface SettingsModalChange {
|
|
79
75
|
readonly key: ConfigKey;
|
|
@@ -555,105 +551,22 @@ export class SettingsModal {
|
|
|
555
551
|
* Returns true on success, false if validation failed.
|
|
556
552
|
*/
|
|
557
553
|
commitEdit(): boolean {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const nextMode = this.editBuffer.trim() as McpEntry['trustMode'];
|
|
577
|
-
const validModes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
578
|
-
if (!validModes.includes(nextMode)) {
|
|
579
|
-
this.editingMode = false;
|
|
580
|
-
this.editBuffer = '';
|
|
581
|
-
this.mcpAllowAllConfirmationTarget = null;
|
|
582
|
-
return false;
|
|
583
|
-
}
|
|
584
|
-
if (nextMode === 'allow-all' && entry.trustMode !== 'allow-all') {
|
|
585
|
-
this.mcpAllowAllConfirmationTarget = entry.name;
|
|
586
|
-
this.editBuffer = '';
|
|
587
|
-
return false;
|
|
588
|
-
}
|
|
589
|
-
this.mcpRegistry.setServerTrustMode(entry.name, nextMode);
|
|
590
|
-
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
591
|
-
this.editingMode = false;
|
|
592
|
-
this.editBuffer = '';
|
|
593
|
-
this.mcpAllowAllConfirmationTarget = null;
|
|
594
|
-
return true;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const entry = this.getSelected();
|
|
598
|
-
if (!entry || !this.configManager) return false;
|
|
599
|
-
|
|
600
|
-
const { setting } = entry;
|
|
601
|
-
let parsed: unknown = this.editBuffer;
|
|
602
|
-
|
|
603
|
-
if (setting.type === 'number') {
|
|
604
|
-
parsed = Number(this.editBuffer);
|
|
605
|
-
if (isNaN(parsed as number)) {
|
|
606
|
-
this.editingMode = false;
|
|
607
|
-
this.editBuffer = '';
|
|
608
|
-
return false;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (setting.type === 'object') {
|
|
613
|
-
// Object-typed keys (e.g. pricing.modelPrices) edit as JSON; the
|
|
614
|
-
// schema's validate() below still rules on the parsed shape.
|
|
615
|
-
try {
|
|
616
|
-
parsed = JSON.parse(this.editBuffer);
|
|
617
|
-
} catch {
|
|
618
|
-
this.editingMode = false;
|
|
619
|
-
this.editBuffer = '';
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (setting.validate && !setting.validate(parsed)) {
|
|
625
|
-
this.editingMode = false;
|
|
626
|
-
this.editBuffer = '';
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (isWorktreeSetupListConfigKey(setting.key)) {
|
|
631
|
-
// Comma-separated display/edit convention for the array-backed
|
|
632
|
-
// worktree.setup.* keys — see worktree-setup-config.ts.
|
|
633
|
-
this._setValue(setting.key, parseWorktreeSetupListInput(this.editBuffer));
|
|
634
|
-
} else if (isSandboxExecListConfigKey(setting.key)) {
|
|
635
|
-
// Same comma-separated convention for the array-backed
|
|
636
|
-
// sandbox.egressAllowlist / sandbox.workspaceWritable keys — see
|
|
637
|
-
// sandbox-exec-config.ts.
|
|
638
|
-
this._setValue(setting.key, parseSandboxExecListInput(this.editBuffer));
|
|
639
|
-
} else if (isExecEnvScrubAllowlistConfigKey(setting.key)) {
|
|
640
|
-
// Same comma-separated convention for permissions.execEnvScrubAllowlist
|
|
641
|
-
// — see exec-env-scrub-config.ts.
|
|
642
|
-
this._setValue(setting.key, parseExecEnvScrubAllowlistInput(this.editBuffer));
|
|
643
|
-
} else if (setting.type === 'string' && isSecretConfigKey(setting.key)) {
|
|
644
|
-
setSecretBackedSettingValue({
|
|
645
|
-
key: setting.key,
|
|
646
|
-
value: String(parsed ?? ''),
|
|
647
|
-
configManager: this.configManager,
|
|
648
|
-
secretsManager: this.secretsManager,
|
|
649
|
-
setConfigValue: (key, value) => this._setValue(key, value),
|
|
650
|
-
});
|
|
651
|
-
} else {
|
|
652
|
-
this._setValue(setting.key as ConfigKey, parsed);
|
|
653
|
-
}
|
|
654
|
-
this.editingMode = false;
|
|
655
|
-
this.editBuffer = '';
|
|
656
|
-
return true;
|
|
554
|
+
return _commitEditValue({
|
|
555
|
+
editingMode: this.editingMode,
|
|
556
|
+
currentCategory: this.currentCategory,
|
|
557
|
+
editBuffer: this.editBuffer,
|
|
558
|
+
configManager: this.configManager,
|
|
559
|
+
secretsManager: this.secretsManager,
|
|
560
|
+
mcpRegistry: this.mcpRegistry,
|
|
561
|
+
mcpAllowAllConfirmationTarget: this.mcpAllowAllConfirmationTarget,
|
|
562
|
+
getSelectedMcp: () => this.getSelectedMcp(),
|
|
563
|
+
getSelected: () => this.getSelected(),
|
|
564
|
+
setValue: (key, value) => this._setValue(key, value),
|
|
565
|
+
setEditingMode: (v) => { this.editingMode = v; },
|
|
566
|
+
setEditBuffer: (v) => { this.editBuffer = v; },
|
|
567
|
+
setMcpEntries: (entries) => { this.mcpEntries = entries; },
|
|
568
|
+
setMcpAllowAllConfirmationTarget: (v) => { this.mcpAllowAllConfirmationTarget = v; },
|
|
569
|
+
});
|
|
657
570
|
}
|
|
658
571
|
|
|
659
572
|
/** Cancel inline edit without saving. */
|
|
@@ -781,6 +694,13 @@ export class SettingsModal {
|
|
|
781
694
|
this.lastSettingEffectMessage = result.effectMessage;
|
|
782
695
|
}
|
|
783
696
|
// No-op (result.changed === false, effectMessage === null): leave lastSettingEffectMessage untouched.
|
|
697
|
+
|
|
698
|
+
// Selecting 'prompt' for payments.cvvHandling states the tradeoff at the
|
|
699
|
+
// moment of selection, not just in the documentation pane — see the
|
|
700
|
+
// SDK's own CVV_PROMPT_TRADEOFF_WARNING (platform/payments).
|
|
701
|
+
if (key === PAYMENTS_CVV_HANDLING_CONFIG_KEY && value === 'prompt') {
|
|
702
|
+
this.lastSettingEffectMessage = CVV_PROMPT_TRADEOFF_WARNING;
|
|
703
|
+
}
|
|
784
704
|
}
|
|
785
705
|
|
|
786
706
|
/** Refresh live/persisted/pending state for every feature header bound to `key`. */
|
|
@@ -207,6 +207,11 @@ export function createMarketplaceModalSurface(deps: MarketplaceModalDeps): Confi
|
|
|
207
207
|
* missing paths with existsSync → [], so refresh() finds nothing and renders the
|
|
208
208
|
* honest empty-state copy). The random tmp path never appears in the
|
|
209
209
|
* rendered lines (the empty-state copy is static), so the golden is byte-stable.
|
|
210
|
+
*
|
|
211
|
+
* This is production code (ships in the real binary), not test scratch, so
|
|
212
|
+
* it stays rooted at the real OS temp dir rather than the test-only
|
|
213
|
+
* makeProjectTempDir helper. Created and removed synchronously in the same
|
|
214
|
+
* call, so the on-disk window is negligible.
|
|
210
215
|
*/
|
|
211
216
|
export function marketplaceModalGoldenSurface(): ConfigModalSurface {
|
|
212
217
|
const root = mkdtempSync(join(tmpdir(), 'gv-marketplace-golden-'));
|
|
@@ -180,6 +180,11 @@ export function createSkillsModalSurface(deps: SkillsModalDeps): ConfigModalSurf
|
|
|
180
180
|
* homeDir and removed immediately (scanSkillDirectorySync catches missing dirs →
|
|
181
181
|
* []), so refresh() finds no `.goodvibes/skills` directories and renders the
|
|
182
182
|
* static empty-state copy. The random tmp path never appears in the output.
|
|
183
|
+
*
|
|
184
|
+
* This is production code (ships in the real binary), not test scratch, so
|
|
185
|
+
* it stays rooted at the real OS temp dir rather than the test-only
|
|
186
|
+
* makeProjectTempDir helper. Created and removed synchronously in the same
|
|
187
|
+
* call, so the on-disk window is negligible.
|
|
183
188
|
*/
|
|
184
189
|
export function skillsModalGoldenSurface(): ConfigModalSurface {
|
|
185
190
|
const root = mkdtempSync(join(tmpdir(), 'gv-skills-golden-'));
|
|
@@ -112,6 +112,8 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
112
112
|
notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
|
|
113
113
|
relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
|
|
114
114
|
learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
|
|
115
|
+
payments: 'Payment card on file for daemon-initiated purchases: card number, expiry, CVV, cardholder name, billing and shipping address, and whether the CVV is stored or requested at purchase time. Card fields are entered through /payments card (concealed input) and are secret-tier — never rendered in plaintext.',
|
|
116
|
+
profile: 'What the platform knows about you — your name, how to reach you, where you are, how you like answers written, the people and places you mention — kept as one Markdown file at daemon scope that you can open and edit by hand at any time; your edits win and are never rewritten. These rows decide whether it is loaded at all, whether the assistant records facts it learns from things you say directly to it, whether it tells you in one line each time it does, whether the harmless part (how you are addressed, your city, your timezone, your formatting preferences, your style) is put in the model\'s context so it stops guessing, whether using a private detail such as your address is announced in the reply, whether settings left unset read their value from the profile, and where the file lives. Everything private — your name, contact details, home address, billing and shipping details, quiet hours, and the People, Places, Work and Notes sections — is never put in context in bulk and is reachable only by a named lookup that is disclosed when it happens. Run /profile to read the whole thing, /profile where <field> to see the surface, date and your exact words behind any recorded line, and /profile forget <field> to delete one.',
|
|
115
117
|
};
|
|
116
118
|
|
|
117
119
|
export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], string> = {
|
|
@@ -165,6 +167,8 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
165
167
|
connections: 'Connections',
|
|
166
168
|
policy: 'Policy-as-Code',
|
|
167
169
|
notifications: 'Notifications',
|
|
170
|
+
payments: 'Payments',
|
|
171
|
+
profile: 'Owner Profile',
|
|
168
172
|
};
|
|
169
173
|
|
|
170
174
|
const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
@@ -13,6 +13,8 @@ import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
|
13
13
|
import { CATEGORY_INFO, CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
14
14
|
import { buildConnectionContext, renderConnectionRows } from './settings-modal-connections.ts';
|
|
15
15
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
16
|
+
import { maskConcealedText } from '../input/concealed-input.ts';
|
|
17
|
+
import { CVV_PROMPT_TRADEOFF_WARNING } from '@pellux/goodvibes-sdk/platform/payments';
|
|
16
18
|
import { GLYPHS } from './ui-primitives.ts';
|
|
17
19
|
import { formatHints, joinHints } from './hint-grammar.ts';
|
|
18
20
|
import {
|
|
@@ -105,6 +107,10 @@ const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
|
|
|
105
107
|
'meta-cloud': 'Use Meta Cloud API credentials and identifiers.',
|
|
106
108
|
bridge: 'Use a bridge service URL/token flow instead of direct Meta Cloud API delivery.',
|
|
107
109
|
},
|
|
110
|
+
'payments.cvvHandling': {
|
|
111
|
+
stored: 'Store the CVV through the secret manager so the daemon can complete a purchase unattended.',
|
|
112
|
+
prompt: CVV_PROMPT_TRADEOFF_WARNING,
|
|
113
|
+
},
|
|
108
114
|
};
|
|
109
115
|
|
|
110
116
|
function paddedWrapped(text: string, width: number, prefix = ''): string[] {
|
|
@@ -121,7 +127,16 @@ function formatDefaultValue(value: unknown): string {
|
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
function currentSettingValue(modal: SettingsModal, entry: SettingEntry, selected: boolean): string {
|
|
124
|
-
if (selected && modal.editingMode)
|
|
130
|
+
if (selected && modal.editingMode) {
|
|
131
|
+
// Secret-backed keys (surfaces.*.botToken, .signingSecret, etc. — see
|
|
132
|
+
// secret-config.ts) must never echo the in-progress plaintext buffer:
|
|
133
|
+
// not in the row, not in the "Current: ..." context line, not in search
|
|
134
|
+
// results. Reuse the composer's concealed-input mask (concealed-input.ts)
|
|
135
|
+
// rather than a second masking implementation — same bullet-per-character
|
|
136
|
+
// shape, so keystrokes still visibly register without revealing content.
|
|
137
|
+
const buffer = isSecretConfigKey(entry.setting.key) ? maskConcealedText(modal.editBuffer) : modal.editBuffer;
|
|
138
|
+
return `${buffer}${GLYPHS.surface.cursor}`;
|
|
139
|
+
}
|
|
125
140
|
return formatValue(entry);
|
|
126
141
|
}
|
|
127
142
|
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -41,7 +41,7 @@ import { initializeBootstrapCore } from './bootstrap-core.ts';
|
|
|
41
41
|
import { ensureBootModelResolvable } from './provider-fallback.ts';
|
|
42
42
|
import { createBootstrapShell } from './bootstrap-shell.ts';
|
|
43
43
|
import { announceResumeState } from './resume-notice.ts';
|
|
44
|
-
import {
|
|
44
|
+
import { announceInstallHealth } from './install-self-check-startup.ts';
|
|
45
45
|
import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './orchestrator-core-services.ts';
|
|
46
46
|
import { consumeDaemonAttachNotices, consumeExternalDaemonAttachNotices } from './daemon-attach-notices.ts';
|
|
47
47
|
import { wireContextAccountingSource } from './context-accounting-source.ts';
|
|
@@ -322,7 +322,7 @@ export async function bootstrapRuntime(
|
|
|
322
322
|
}).catch(() => {
|
|
323
323
|
// Best-effort — never let the resume notice block or crash boot.
|
|
324
324
|
});
|
|
325
|
-
|
|
325
|
+
announceInstallHealth(systemMessageRouter);
|
|
326
326
|
const { gitStatusProvider, inputHistory, lastGitInfoRef } = shell;
|
|
327
327
|
// FIX 2: dispose the header's live-repo-state poll (git-status.ts
|
|
328
328
|
// startPolling) on shutdown, same pattern as acpTaskSyncInterval above.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
18
|
import { getGoodVibesPackageRoot, resolveGoodVibesDaemonExecutable } from '../cli/service-posture.ts';
|
|
19
19
|
import { runInstallSelfCheck } from './install-self-check.ts';
|
|
20
|
+
import { announceReachability } from './path-shadow-startup.ts';
|
|
20
21
|
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
21
22
|
|
|
22
23
|
export function announceInstallSelfCheck(router: SystemMessageRouter): void {
|
|
@@ -34,3 +35,25 @@ export function announceInstallSelfCheck(router: SystemMessageRouter): void {
|
|
|
34
35
|
// Best-effort — an install self-check must never block or crash boot.
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The one call boot makes about the state of this install: is it complete
|
|
41
|
+
* (the check above), is it the copy the shell actually reaches, and is it the
|
|
42
|
+
* current release (path-shadow-startup).
|
|
43
|
+
*
|
|
44
|
+
* They are announced together because they are the same question from the
|
|
45
|
+
* user's side — "am I looking at the build I think I am" — and because the
|
|
46
|
+
* three answers are useless apart: a complete install nobody can reach, or a
|
|
47
|
+
* reachable install three versions old, both end with the product appearing
|
|
48
|
+
* to have lost a capability it never had.
|
|
49
|
+
*
|
|
50
|
+
* The reachability half is async (it may look up the latest release) and is
|
|
51
|
+
* deliberately not awaited: boot continues, the notice arrives through the
|
|
52
|
+
* same router a moment later.
|
|
53
|
+
*/
|
|
54
|
+
export function announceInstallHealth(router: SystemMessageRouter): void {
|
|
55
|
+
announceInstallSelfCheck(router);
|
|
56
|
+
void announceReachability(router).catch(() => {
|
|
57
|
+
// Best-effort — a reachability check must never block or crash boot.
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -281,7 +281,12 @@ export type OnboardingApplyOperation =
|
|
|
281
281
|
readonly kind: 'set-secret';
|
|
282
282
|
readonly key: string;
|
|
283
283
|
readonly value: string;
|
|
284
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Which secret tier the value is filed in. 'daemon' is the tier the
|
|
286
|
+
* daemon reads with every surface closed — see
|
|
287
|
+
* config/secret-config.ts#defaultSecretBackedScope.
|
|
288
|
+
*/
|
|
289
|
+
readonly scope?: 'project' | 'user' | 'daemon';
|
|
285
290
|
readonly medium: 'secure' | 'plaintext';
|
|
286
291
|
}
|
|
287
292
|
| {
|