@pellux/goodvibes-tui 1.24.1 → 1.25.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 +24 -0
- package/README.md +1 -1
- package/bin/goodvibes-daemon +24 -0
- package/docs/foundation-artifacts/operator-contract.json +3237 -1096
- package/package.json +7 -7
- package/src/config/goodvibes-home.ts +139 -0
- package/src/daemon/cli.ts +17 -6
- package/src/daemon/handlers/index.ts +7 -6
- package/src/input/commands/calendar-runtime.ts +245 -0
- package/src/input/commands/connection-status.ts +258 -0
- package/src/input/commands/mail-runtime.ts +267 -0
- package/src/input/commands/review-runtime.ts +13 -1
- package/src/input/commands.ts +4 -0
- package/src/input/settings-modal-connections.ts +102 -0
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +27 -0
- package/src/main.ts +7 -2
- package/src/renderer/settings-modal-connections.ts +63 -0
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +5 -0
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/context.ts +9 -0
- package/src/runtime/credential-composition.ts +54 -0
- package/src/runtime/daemon-handler-composition.ts +0 -4
- package/src/runtime/mail-composition.ts +65 -0
- package/src/runtime/services.ts +21 -24
- package/src/shell/ui-openers.ts +7 -0
- package/src/verification/verification-ledger.ts +58 -0
- package/src/version.ts +1 -1
- package/src/daemon/handlers/calendar/caldav-client.ts +0 -662
- package/src/daemon/handlers/calendar/ics.ts +0 -556
- package/src/daemon/handlers/calendar/index.ts +0 -316
- package/src/daemon/handlers/email/config.ts +0 -164
- package/src/daemon/handlers/email/imap-connector.ts +0 -441
- package/src/daemon/handlers/email/imap-parsing.ts +0 -499
- package/src/daemon/handlers/email/index.ts +0 -43
- package/src/daemon/handlers/email/read-handlers.ts +0 -80
- package/src/daemon/handlers/email/runtime.ts +0 -140
- package/src/daemon/handlers/email/smtp-connector.ts +0 -557
- package/src/daemon/handlers/email/validation.ts +0 -147
- package/src/daemon/handlers/email/write-handlers.ts +0 -133
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
} from './settings-modal-secrets.ts';
|
|
29
29
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
30
30
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
31
|
+
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
32
|
+
import type { ConnectionStatus } from './commands/connection-status.ts';
|
|
33
|
+
import { initialConnectionEntries, refreshConnectionEntries } from './settings-modal-connections.ts';
|
|
31
34
|
import { isWorktreeSetupListConfigKey, parseWorktreeSetupListInput } from './worktree-setup-config.ts';
|
|
32
35
|
import { isSandboxExecListConfigKey, parseSandboxExecListInput } from './sandbox-exec-config.ts';
|
|
33
36
|
import { isExecEnvScrubAllowlistConfigKey, parseExecEnvScrubAllowlistInput } from './exec-env-scrub-config.ts';
|
|
@@ -86,6 +89,10 @@ export type SettingsModalChangeHandler = (change: SettingsModalChange) => Settin
|
|
|
86
89
|
|
|
87
90
|
export interface SettingsModalOpenOptions {
|
|
88
91
|
readonly onSettingApplied?: SettingsModalChangeHandler;
|
|
92
|
+
/** In-process catalog the Connections category probes; see that module. */
|
|
93
|
+
readonly gatewayMethods?: GatewayMethodCatalog;
|
|
94
|
+
/** Called when an async connection refresh has new rows to paint. */
|
|
95
|
+
readonly requestRender?: () => void;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
export {
|
|
@@ -154,6 +161,11 @@ export class SettingsModal {
|
|
|
154
161
|
public mcpEntries: McpEntry[] = [];
|
|
155
162
|
/** Provider subscription entries (populated when subscriptions tab is active). */
|
|
156
163
|
public subscriptionEntries: SubscriptionEntry[] = [];
|
|
164
|
+
/** Connections-tab state; all of it driven by settings-modal-connections.ts. */
|
|
165
|
+
public connectionEntries: ConnectionStatus[] = initialConnectionEntries();
|
|
166
|
+
public gatewayMethods: GatewayMethodCatalog | null = null;
|
|
167
|
+
public requestRender: (() => void) | null = null;
|
|
168
|
+
public connectionsRefreshing = false;
|
|
157
169
|
|
|
158
170
|
/**
|
|
159
171
|
* Whether the user has entered search mode (pressed / or a printable key
|
|
@@ -217,6 +229,9 @@ export class SettingsModal {
|
|
|
217
229
|
this.groups = buildSettingGroups(configManager, featureFlagManager);
|
|
218
230
|
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
219
231
|
this.subscriptionEntries = buildSubscriptionEntries(subscriptionManager, serviceRegistry);
|
|
232
|
+
this.gatewayMethods = options?.gatewayMethods ?? null;
|
|
233
|
+
this.requestRender = options?.requestRender ?? null;
|
|
234
|
+
this.connectionEntries = initialConnectionEntries();
|
|
220
235
|
this.categoryIndex = 0;
|
|
221
236
|
this.selectedIndex = 0;
|
|
222
237
|
this.focusPane = 'categories';
|
|
@@ -256,6 +271,11 @@ export class SettingsModal {
|
|
|
256
271
|
this.searchResults = [];
|
|
257
272
|
this.searchFocused = false;
|
|
258
273
|
this.contextScroll = 0;
|
|
274
|
+
// A probe still in flight checks `active` before writing, so a reopen
|
|
275
|
+
// starts from `checking` rather than from a finished session's answer.
|
|
276
|
+
this.gatewayMethods = null;
|
|
277
|
+
this.requestRender = null;
|
|
278
|
+
this.connectionEntries = initialConnectionEntries();
|
|
259
279
|
this.serviceRegistry = null;
|
|
260
280
|
this.secretsManager = null;
|
|
261
281
|
this.onSettingApplied = null;
|
|
@@ -357,6 +377,8 @@ export class SettingsModal {
|
|
|
357
377
|
} else if (this.currentCategory === 'subscriptions' && this.subscriptionEntries.length > 0) {
|
|
358
378
|
this.selectedIndex = (this.selectedIndex - 1 + this.subscriptionEntries.length) % this.subscriptionEntries.length;
|
|
359
379
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
380
|
+
} else if (this.currentCategory === 'connections' && this.connectionEntries.length > 0) {
|
|
381
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.connectionEntries.length) % this.connectionEntries.length;
|
|
360
382
|
}
|
|
361
383
|
return;
|
|
362
384
|
}
|
|
@@ -380,6 +402,8 @@ export class SettingsModal {
|
|
|
380
402
|
} else if (this.currentCategory === 'subscriptions' && this.subscriptionEntries.length > 0) {
|
|
381
403
|
this.selectedIndex = (this.selectedIndex + 1) % this.subscriptionEntries.length;
|
|
382
404
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
405
|
+
} else if (this.currentCategory === 'connections' && this.connectionEntries.length > 0) {
|
|
406
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.connectionEntries.length;
|
|
383
407
|
}
|
|
384
408
|
return;
|
|
385
409
|
}
|
|
@@ -702,6 +726,8 @@ export class SettingsModal {
|
|
|
702
726
|
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
703
727
|
} else if (this.currentCategory === 'subscriptions') {
|
|
704
728
|
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
729
|
+
} else if (this.currentCategory === 'connections') {
|
|
730
|
+
void refreshConnectionEntries(this);
|
|
705
731
|
}
|
|
706
732
|
}
|
|
707
733
|
|
|
@@ -714,6 +740,7 @@ export class SettingsModal {
|
|
|
714
740
|
if (
|
|
715
741
|
this.currentCategory === 'mcp'
|
|
716
742
|
|| this.currentCategory === 'subscriptions'
|
|
743
|
+
|| this.currentCategory === 'connections'
|
|
717
744
|
) return [];
|
|
718
745
|
const items = this.groups.get(this.currentCategory) ?? [];
|
|
719
746
|
if (this.currentCategory === 'network') {
|
package/src/main.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { resolveGoodVibesDaemonHome, resolveGoodVibesHome } from './config/goodvibes-home.ts';
|
|
3
3
|
import { Compositor } from './renderer/compositor.ts';
|
|
4
4
|
import { type Line } from './types/grid.ts';
|
|
5
5
|
import { UIFactory } from './renderer/ui-factory.ts';
|
|
@@ -87,9 +87,12 @@ import { VERSION } from './version.ts';
|
|
|
87
87
|
async function main() {
|
|
88
88
|
const stdout = process.stdout;
|
|
89
89
|
const stdin = process.stdin;
|
|
90
|
+
// Both roots come from the one resolver the daemon CLI also uses. This line
|
|
91
|
+
// called homedir() unconditionally, so a harness that redirected the tree got
|
|
92
|
+
// a client that wrote the real one — secret store included.
|
|
90
93
|
const { cli, configManager, bootstrapWorkingDir, bootstrapHomeDirectory } = await prepareShellCliRuntime(process.argv.slice(2), {
|
|
91
94
|
defaultWorkingDirectory: process.env['GOODVIBES_WORKING_DIR'] ?? process.cwd(),
|
|
92
|
-
homeDirectory:
|
|
95
|
+
homeDirectory: resolveGoodVibesHome(),
|
|
93
96
|
}, 'goodvibes');
|
|
94
97
|
|
|
95
98
|
// Between binary start and the first frame, boot used to be silent (config
|
|
@@ -106,6 +109,8 @@ async function main() {
|
|
|
106
109
|
configManager,
|
|
107
110
|
workingDir: bootstrapWorkingDir,
|
|
108
111
|
homeDirectory: bootstrapHomeDirectory,
|
|
112
|
+
// Named, so GOODVIBES_DAEMON_HOME on its own moves the client's daemon tier.
|
|
113
|
+
daemonHomeDirectory: resolveGoodVibesDaemonHome(bootstrapHomeDirectory),
|
|
109
114
|
});
|
|
110
115
|
const {
|
|
111
116
|
conversation,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-modal-connections.ts — rendering for the Connections category of the
|
|
3
|
+
* settings workspace.
|
|
4
|
+
*
|
|
5
|
+
* Split out of settings-modal.ts for the 800-line file cap, the same reason
|
|
6
|
+
* settings-modal-helpers.ts exists. Pure rendering: every value shown here was
|
|
7
|
+
* decided by the daemon probe in input/commands/connection-status.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SettingsModal } from '../input/settings-modal.ts';
|
|
11
|
+
import { selectedConnectionEntry } from '../input/settings-modal-connections.ts';
|
|
12
|
+
import { connectionSurfaceLabel, type ConnectionStatus } from '../input/commands/connection-status.ts';
|
|
13
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
14
|
+
import { clamp, padDisplay, stableWindow } from './fullscreen-workspace.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The Connections rows: one line per surface, saying what it actually is.
|
|
18
|
+
*
|
|
19
|
+
* `checking` is rendered as its own state rather than blanked, because a row
|
|
20
|
+
* that showed nothing while the probe was in flight would read as "nothing
|
|
21
|
+
* configured" — the one thing this category exists to stop claiming falsely.
|
|
22
|
+
*/
|
|
23
|
+
export function renderConnectionRows(modal: SettingsModal, width: number, height: number): string[] {
|
|
24
|
+
const rows: string[] = [];
|
|
25
|
+
const items = modal.connectionEntries;
|
|
26
|
+
if (items.length === 0) return ['No connection surfaces are known to this build.'];
|
|
27
|
+
const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
|
|
28
|
+
const surfaceWidth = clamp(Math.floor(width * 0.16), 10, 20);
|
|
29
|
+
const stateWidth = 12;
|
|
30
|
+
const detailWidth = Math.max(16, width - surfaceWidth - stateWidth - 8);
|
|
31
|
+
rows.push(` ${padDisplay('Surface', surfaceWidth)} ${padDisplay('State', stateWidth)} ${padDisplay('Detail', detailWidth)}`);
|
|
32
|
+
const window = stableWindow(items.length, selectedIndex, Math.max(1, height - 2));
|
|
33
|
+
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more connection(s) above`);
|
|
34
|
+
for (let index = window.start; index < window.end; index += 1) {
|
|
35
|
+
const entry = items[index]!;
|
|
36
|
+
const selected = index === selectedIndex;
|
|
37
|
+
const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '\u2022') : ' ';
|
|
38
|
+
rows.push(`${marker} ${padDisplay(connectionSurfaceLabel(entry.surface), surfaceWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(entry.detail, detailWidth)}`);
|
|
39
|
+
}
|
|
40
|
+
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more connection(s) below`);
|
|
41
|
+
return rows.slice(0, height);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The detail pane for one connection: the state, what it means, and every next
|
|
46
|
+
* step verbatim. The next steps are the daemon's requirements, so they are
|
|
47
|
+
* listed in full rather than summarized — a truncated instruction is a wrong
|
|
48
|
+
* instruction.
|
|
49
|
+
*/
|
|
50
|
+
export function buildConnectionContext(modal: SettingsModal): string[] {
|
|
51
|
+
const entry = selectedConnectionEntry(modal);
|
|
52
|
+
if (!entry) return ['Connections', 'No connection surface is selected.'];
|
|
53
|
+
return [
|
|
54
|
+
connectionSurfaceLabel(entry.surface),
|
|
55
|
+
`State: ${entry.state}`,
|
|
56
|
+
entry.detail,
|
|
57
|
+
...(entry.nextActions.length > 0 ? ['', 'Next steps:', ...entry.nextActions] : []),
|
|
58
|
+
'',
|
|
59
|
+
entry.surface === 'mail'
|
|
60
|
+
? 'Use it from the transcript with /mail (status, list, read, draft, send).'
|
|
61
|
+
: 'Use it from the transcript with /calendar (status, list, get, create).',
|
|
62
|
+
];
|
|
63
|
+
}
|
|
@@ -107,6 +107,7 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
107
107
|
agents: 'Sub-agent context-window awareness and per-turn passive knowledge/code injection: token budget, relevance floor, code-chunk limit, and the compaction threshold. Gated by the agent context/injection features.',
|
|
108
108
|
security: 'API token scope and rotation auditing: rotation cadence, warning lead time, and whether overdue/over-scoped tokens are blocked or only reported. Gated by the Token Scope and Rotation Audit feature.',
|
|
109
109
|
integrations: 'Integration delivery reliability (Slack/Discord/webhook): retry counts, backoff bounds, dead-letter queue size, and SLO enforcement. Gated by the Integration Delivery SLO feature.',
|
|
110
|
+
connections: 'Whether mail and calendar are actually usable right now. Each row is answered by asking the daemon that owns the connection, not by reading config here, so a row says ready only when a real request succeeded.',
|
|
110
111
|
policy: 'Policy-as-code bundle loading: where the startup policy bundle is loaded from and its file path. Gated by the Policy-as-Code feature.',
|
|
111
112
|
notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
|
|
112
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.',
|
|
@@ -161,6 +162,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
161
162
|
agents: 'Agents & Context',
|
|
162
163
|
security: 'Token Security',
|
|
163
164
|
integrations: 'Integration Delivery',
|
|
165
|
+
connections: 'Connections',
|
|
164
166
|
policy: 'Policy-as-Code',
|
|
165
167
|
notifications: 'Notifications',
|
|
166
168
|
};
|
|
@@ -11,6 +11,7 @@ import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings
|
|
|
11
11
|
import { FEATURE_SETTINGS_BY_ID, isFeatureValueEnabled } from '../runtime/feature-settings.ts';
|
|
12
12
|
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
|
+
import { buildConnectionContext, renderConnectionRows } from './settings-modal-connections.ts';
|
|
14
15
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
15
16
|
import { GLYPHS } from './ui-primitives.ts';
|
|
16
17
|
import { formatHints, joinHints } from './hint-grammar.ts';
|
|
@@ -360,6 +361,8 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
|
|
|
360
361
|
lines.push(...buildMcpContext(modal, modal.getSelectedMcp()));
|
|
361
362
|
} else if (category === 'subscriptions') {
|
|
362
363
|
lines.push(...buildSubscriptionContext(modal, modal.getSelectedSubscription()));
|
|
364
|
+
} else if (category === 'connections') {
|
|
365
|
+
lines.push(...buildConnectionContext(modal));
|
|
363
366
|
} else {
|
|
364
367
|
const selected = modal.getSelected();
|
|
365
368
|
// A feature-unit header shows the feature's documentation (full
|
|
@@ -386,6 +389,7 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
|
|
|
386
389
|
function categoryItemCount(modal: SettingsModal, category: SettingsCategory): number {
|
|
387
390
|
if (category === 'mcp') return modal.mcpEntries.length;
|
|
388
391
|
if (category === 'subscriptions') return modal.subscriptionEntries.length;
|
|
392
|
+
if (category === 'connections') return modal.connectionEntries.length;
|
|
389
393
|
return modal.groups.get(category)?.length ?? 0;
|
|
390
394
|
}
|
|
391
395
|
|
|
@@ -601,6 +605,7 @@ function renderControlRows(modal: SettingsModal, width: number, height: number):
|
|
|
601
605
|
if (modal.searchFocused) return renderSearchRows(modal, width, height);
|
|
602
606
|
if (modal.currentCategory === 'mcp') return renderMcpRows(modal, width, height);
|
|
603
607
|
if (modal.currentCategory === 'subscriptions') return renderSubscriptionRows(modal, width, height);
|
|
608
|
+
if (modal.currentCategory === 'connections') return renderConnectionRows(modal, width, height);
|
|
604
609
|
return renderSettingRows(modal, width, height);
|
|
605
610
|
}
|
|
606
611
|
|
|
@@ -248,7 +248,7 @@ export async function initializeBootstrapCore(
|
|
|
248
248
|
runtimeBus,
|
|
249
249
|
runtimeStore: store,
|
|
250
250
|
getConversationTitle: () => getConversationTitle(),
|
|
251
|
-
workingDir, homeDirectory,
|
|
251
|
+
workingDir, homeDirectory, ...(options.daemonHomeDirectory === undefined ? {} : { daemonHomeDirectory: options.daemonHomeDirectory }),
|
|
252
252
|
// Embedded topology: the in-process runtime IS the daemon and holds its own OS
|
|
253
253
|
// inhibitor, so opt into the real host seam (fork default is non-spawning). See services.ts.
|
|
254
254
|
powerSeam: createHostPowerSeam(),
|
package/src/runtime/context.ts
CHANGED
|
@@ -31,6 +31,15 @@ export interface BootstrapOptions {
|
|
|
31
31
|
workingDir: string;
|
|
32
32
|
/** App-owned home directory for this runtime instance. */
|
|
33
33
|
homeDirectory: string;
|
|
34
|
+
/**
|
|
35
|
+
* The daemon's identity directory for this runtime instance.
|
|
36
|
+
*
|
|
37
|
+
* Named rather than derived so the client's daemon-TIER secret reads and
|
|
38
|
+
* writes follow a GOODVIBES_DAEMON_HOME set on its own. Absent, the secret
|
|
39
|
+
* store falls back to `<homeDirectory>/.goodvibes/daemon`, which is the same
|
|
40
|
+
* answer whenever that variable is unset.
|
|
41
|
+
*/
|
|
42
|
+
daemonHomeDirectory?: string;
|
|
34
43
|
/** Explicit app-owned config manager for this runtime instance. */
|
|
35
44
|
configManager: import('@pellux/goodvibes-sdk/platform/config').ConfigManager;
|
|
36
45
|
/**
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credential-composition.ts
|
|
3
|
+
*
|
|
4
|
+
* The credential and identity seam: the secret store, the step-up ceremony
|
|
5
|
+
* service that verifies against it, and the per-device pairing tokens.
|
|
6
|
+
*
|
|
7
|
+
* These are grouped because they share one fact, and it is the fact this round
|
|
8
|
+
* had to fix: WHERE the daemon's own credentials live. `daemonHome` is threaded
|
|
9
|
+
* rather than defaulted, because without it a daemon told to run out of a temp
|
|
10
|
+
* tree moved its identity directory and nothing else — the credential store
|
|
11
|
+
* stayed in the real home, so an "isolated" test daemon held live credentials
|
|
12
|
+
* and long-polled a real account. A default here would silently restore that.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from services.ts because that file sits at the 800-line
|
|
15
|
+
* architecture cap. Moving a coherent seam out is what the cap is for; trimming
|
|
16
|
+
* unrelated comments to buy headroom is the thing it exists to prevent.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { SecretsManager } from '../config/secrets.ts';
|
|
20
|
+
import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
|
|
21
|
+
import { PairingTokenManager } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
22
|
+
|
|
23
|
+
interface CredentialCompositionInput {
|
|
24
|
+
readonly workingDirectory: string;
|
|
25
|
+
readonly homeDirectory: string;
|
|
26
|
+
/**
|
|
27
|
+
* The daemon's state root when the host was told one. Absent means "not
|
|
28
|
+
* overridden" and must stay absent rather than being defaulted — see above.
|
|
29
|
+
*/
|
|
30
|
+
readonly daemonHomeDirectory?: string | undefined;
|
|
31
|
+
readonly configManager: ConstructorParameters<typeof SecretsManager>[0]['configManager'];
|
|
32
|
+
readonly pairingTokenPath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function composeCredentialServices(input: CredentialCompositionInput): {
|
|
36
|
+
readonly secretsManager: SecretsManager;
|
|
37
|
+
readonly stepUpService: StepUpService;
|
|
38
|
+
readonly pairingTokens: PairingTokenManager;
|
|
39
|
+
} {
|
|
40
|
+
const secretsManager = new SecretsManager({
|
|
41
|
+
projectRoot: input.workingDirectory,
|
|
42
|
+
globalHome: input.homeDirectory,
|
|
43
|
+
// Threaded, not defaulted: else an isolated daemon reads the real store.
|
|
44
|
+
...(input.daemonHomeDirectory === undefined ? {} : { daemonHome: input.daemonHomeDirectory }),
|
|
45
|
+
configManager: input.configManager,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
secretsManager,
|
|
49
|
+
// Shared between the ceremony gateway verbs and the relay gate's verifier,
|
|
50
|
+
// so both check a step-up against the same store.
|
|
51
|
+
stepUpService: new StepUpService({ secrets: secretsManager }),
|
|
52
|
+
pairingTokens: new PairingTokenManager(input.pairingTokenPath),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -27,8 +27,6 @@ import { registerRouting } from '../daemon/handlers/routing/index.ts';
|
|
|
27
27
|
import { registerInboxMethods } from '../daemon/handlers/inbox/index.ts';
|
|
28
28
|
import { registerTriagedInbox } from '../daemon/handlers/triage/index.ts';
|
|
29
29
|
import { registerDraftMethods } from '../daemon/handlers/drafts/index.ts';
|
|
30
|
-
import { registerCalendar } from '../daemon/handlers/calendar/index.ts';
|
|
31
|
-
import { registerEmailMethods } from '../daemon/handlers/email/index.ts';
|
|
32
30
|
import { registerRemoteSurface } from '../daemon/handlers/remote/index.ts';
|
|
33
31
|
import { inboxPollerGate } from './cluster-composition.ts';
|
|
34
32
|
|
|
@@ -73,8 +71,6 @@ export function createDaemonHandlerComposition(
|
|
|
73
71
|
options.clusterCoordinator.register(inboxPollerGate(providerId, control)),
|
|
74
72
|
})).unregister,
|
|
75
73
|
registerDrafts: (ctx) => registerDraftMethods(ctx),
|
|
76
|
-
registerCalendar,
|
|
77
|
-
registerEmail: (ctx) => registerEmailMethods(ctx),
|
|
78
74
|
registerRemote: (ctx) => registerRemoteSurface(ctx, { manager: options.distributedRuntime }),
|
|
79
75
|
});
|
|
80
76
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mail-composition.ts
|
|
3
|
+
*
|
|
4
|
+
* The deps that let the platform register this daemon's `email.*` verbs.
|
|
5
|
+
*
|
|
6
|
+
* `calendar.*` and `email.*` are served by the platform now, not by handlers in
|
|
7
|
+
* this repository — those were deleted when the platform gained an
|
|
8
|
+
* implementation the daemon could call. What is left for a product to supply is
|
|
9
|
+
* the wiring, and it is not optional: without `homeDirectory` the calendar
|
|
10
|
+
* composition returns null, without these deps the mail one does, and either way
|
|
11
|
+
* the verbs stay cataloged-but-unhandled while every surface reports the daemon
|
|
12
|
+
* unreachable on a daemon that is working perfectly.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from services.ts rather than living there, because that file sits at
|
|
15
|
+
* the 800-line architecture cap and a composition step is exactly the kind of
|
|
16
|
+
* thing the cap exists to push into a module of its own.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
withSurfaceEmailConfig,
|
|
21
|
+
describeSurfaceEmailConfigProblem,
|
|
22
|
+
describeSenderClaimNeutrally,
|
|
23
|
+
type EmailServiceDeps,
|
|
24
|
+
type SurfaceEmailConfigProblem,
|
|
25
|
+
} from '@pellux/goodvibes-sdk/platform/email';
|
|
26
|
+
import { nodeEmailTransport } from '@pellux/goodvibes-sdk/platform/email/node';
|
|
27
|
+
|
|
28
|
+
/** The narrow slices this composition needs; the real managers satisfy them. */
|
|
29
|
+
interface MailCompositionInput {
|
|
30
|
+
readonly configManager: { get(key: never): unknown };
|
|
31
|
+
readonly secretsManager: { get(key: string): Promise<string | null> };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the mail deps and the not-configured describer that go to
|
|
36
|
+
* `registerGatewayVerbGroups`.
|
|
37
|
+
*
|
|
38
|
+
* The settings come from the daemon's own `surfaces.email.*` keys through
|
|
39
|
+
* `withSurfaceEmailConfig`, so the keys an operator has already set — and that
|
|
40
|
+
* the settings modal now shows — keep working unchanged, and a not-configured
|
|
41
|
+
* answer names the keys THIS operator actually has rather than the ones the
|
|
42
|
+
* service validates internally.
|
|
43
|
+
*/
|
|
44
|
+
export function composeMailDeps(input: MailCompositionInput): {
|
|
45
|
+
readonly emailServiceDeps: EmailServiceDeps;
|
|
46
|
+
readonly describeEmailConfigProblem: () => Promise<SurfaceEmailConfigProblem | null>;
|
|
47
|
+
} {
|
|
48
|
+
const getConfig = (key: string): unknown => input.configManager.get(key as never);
|
|
49
|
+
const emailServiceDeps = withSurfaceEmailConfig({
|
|
50
|
+
getConfig,
|
|
51
|
+
secretsManager: input.secretsManager,
|
|
52
|
+
transport: nodeEmailTransport,
|
|
53
|
+
// The daemon has no wording of its own for a sender line, so it takes the
|
|
54
|
+
// platform's rather than growing a second implementation of a rule that is
|
|
55
|
+
// security-relevant: a From: header is a claim, sender authentication
|
|
56
|
+
// raises display confidence only, and commandAuthority is the literal
|
|
57
|
+
// 'none'.
|
|
58
|
+
describeSenderClaim: describeSenderClaimNeutrally,
|
|
59
|
+
} as never);
|
|
60
|
+
return {
|
|
61
|
+
emailServiceDeps,
|
|
62
|
+
describeEmailConfigProblem: () =>
|
|
63
|
+
describeSurfaceEmailConfigProblem(getConfig, input.secretsManager),
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/runtime/services.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
|
|
|
11
11
|
import { PairingTokenManager } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
12
12
|
import { resolvePairingWebOrigin } from '../core/pairing-origin.ts';
|
|
13
13
|
import { attachWsOnlyGatewayVerbHandlers } from '@pellux/goodvibes-terminal-shell';
|
|
14
|
+
import { composeMailDeps } from './mail-composition.ts';
|
|
15
|
+
import { composeCredentialServices } from './credential-composition.ts';
|
|
14
16
|
import { createDisposalScope, registerSurfaceRuntimePollers } from './disposal-wiring.ts';
|
|
15
17
|
import { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
|
|
16
18
|
import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
|
|
@@ -85,6 +87,11 @@ export interface RuntimeServicesOptions {
|
|
|
85
87
|
readonly getConversationTitle?: () => string | undefined;
|
|
86
88
|
readonly workingDir: string;
|
|
87
89
|
readonly homeDirectory: string;
|
|
90
|
+
/** Daemon state root (`--daemon-home`); absent ⇒ `<homeDirectory>/.goodvibes/daemon`.
|
|
91
|
+
* Threaded into SecretsManager so the override MOVES the credential store —
|
|
92
|
+
* without it an "isolated" daemon read the real home's secrets. One name for
|
|
93
|
+
* one thing; resolveGoodVibesHomeOwnership is the single reader. */
|
|
94
|
+
readonly daemonHomeDirectory?: string | undefined;
|
|
88
95
|
/** Opt-in (daemon-side only): fold host-observed external coding-agent sessions
|
|
89
96
|
* into the fleet as 'observed-external' rows. Interactive leaves it off and reads
|
|
90
97
|
* the daemon snapshot. Mirrors the SDK's own createRuntimeServices option. */
|
|
@@ -292,14 +299,12 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
292
299
|
runtimeBus: options.runtimeBus,
|
|
293
300
|
featureFlags,
|
|
294
301
|
});
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
302
|
+
// The credential/identity seam (credential-composition.ts).
|
|
303
|
+
const { secretsManager, stepUpService, pairingTokens } = composeCredentialServices({
|
|
304
|
+
workingDirectory, homeDirectory, configManager,
|
|
305
|
+
daemonHomeDirectory: options.daemonHomeDirectory,
|
|
306
|
+
pairingTokenPath: shellPaths.resolveUserPath('control-plane', 'pairing-tokens.json'),
|
|
299
307
|
});
|
|
300
|
-
// Step-up (WebAuthn) ceremony service, shared between the ceremony gateway verbs and the relay gate's verifier.
|
|
301
|
-
const stepUpService = new StepUpService({ secrets: secretsManager });
|
|
302
|
-
const pairingTokens = new PairingTokenManager(shellPaths.resolveUserPath('control-plane', 'pairing-tokens.json'));
|
|
303
308
|
const subscriptionManager = new SubscriptionManager(shellPaths.resolveUserPath('tui', 'subscriptions.json'));
|
|
304
309
|
const serviceRegistry = new ServiceRegistry(shellPaths.resolveProjectPath('tui', 'services.json'), {
|
|
305
310
|
secretsManager,
|
|
@@ -603,25 +608,17 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
603
608
|
const { voiceSetup } = wireVoiceSetup({ configManager, shellPaths, voiceProviders, admitExpensiveWork });
|
|
604
609
|
|
|
605
610
|
// Terminal-shell wrapper over the SDK registerGatewayVerbGroups (gateway-verbs.ts); checkin.*/fleet-needs-input/pairing.* register only when their deps are present. memoryGovernor lights up ops.memory.get; voiceSetup lights up voice.local.status/install.
|
|
606
|
-
|
|
611
|
+
// calendar.*/email.* are platform-served; these two let it register (mail-composition.ts).
|
|
612
|
+
const { emailServiceDeps, describeEmailConfigProblem } = composeMailDeps({ configManager, secretsManager });
|
|
613
|
+
attachWsOnlyGatewayVerbHandlers(gatewayMethods, { homeDirectory, emailServiceDeps, describeEmailConfigProblem, processRegistry, workspaceCheckpointManager, conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService, approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input), watcherRegistry, userPermissionRuleStore, shellPaths, configManager, runtimeStore: options.runtimeStore, channelDeliveryRouter, providerRegistry, automationManager, sessionLister: sessionBroker, sessionIntake: sessionBroker, workingDirectory, memoryRegistry, pairingTokens, sessionLiveTurnControls, powerManager, memoryGovernor, voiceSetup, relayAvailable: () => configManager.get('relay.enabled') === true, pairingWebOrigin: () => resolvePairingWebOrigin(configManager).origin, disposal: disposalScope.registry, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
|
|
614
|
+
// surface-scoped: continuity's recovery-file check must read the SAME paths
|
|
615
|
+
// the app writes with, not the unscoped legacy pair.
|
|
607
616
|
const integrationHelpers = new IntegrationHelperService({
|
|
608
|
-
surface,
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
617
|
+
surface, configManager, automationManager, approvalBroker, sessionBroker, distributedRuntime,
|
|
618
|
+
remoteRunnerRegistry, remoteSupervisor, panelManager, localUserAuthManager, providerRegistry,
|
|
619
|
+
serviceRegistry, subscriptionManager, secretsManager,
|
|
620
|
+
runtimeStore: options.runtimeStore, runtimeBus: options.runtimeBus,
|
|
612
621
|
getConversationTitle: options.getConversationTitle,
|
|
613
|
-
automationManager,
|
|
614
|
-
approvalBroker,
|
|
615
|
-
sessionBroker,
|
|
616
|
-
distributedRuntime,
|
|
617
|
-
remoteRunnerRegistry,
|
|
618
|
-
remoteSupervisor,
|
|
619
|
-
panelManager,
|
|
620
|
-
localUserAuthManager,
|
|
621
|
-
providerRegistry,
|
|
622
|
-
serviceRegistry,
|
|
623
|
-
subscriptionManager,
|
|
624
|
-
secretsManager,
|
|
625
622
|
});
|
|
626
623
|
// A loopback fetch that isn't allow-listed asks once through the approval
|
|
627
624
|
// broker; "allow for this project" persists and later fetches never ask. Built
|
package/src/shell/ui-openers.ts
CHANGED
|
@@ -394,6 +394,13 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
394
394
|
commandContext.openSettingsModal = (target?: string) => {
|
|
395
395
|
input.modalOpened('settings');
|
|
396
396
|
input.settingsModal.open(configManager, featureFlags, subscriptionManager, serviceRegistry, mcpRegistry, secretsManager, {
|
|
397
|
+
// The Connections category asks the daemon handlers directly, through the
|
|
398
|
+
// same in-process catalog `/mail` and `/calendar` use, so the workspace
|
|
399
|
+
// reports the daemon's state rather than a local guess about it.
|
|
400
|
+
...(commandContext.workspace?.gatewayMethods
|
|
401
|
+
? { gatewayMethods: commandContext.workspace.gatewayMethods }
|
|
402
|
+
: {}),
|
|
403
|
+
requestRender: render,
|
|
397
404
|
onSettingApplied: (change) => {
|
|
398
405
|
// forced dark/light applies immediately (rebuild palettes + full
|
|
399
406
|
// repaint); auto only re-probes at startup, so it takes effect next launch.
|
|
@@ -48,6 +48,63 @@ const SETTINGS_BEHAVIOR_BASELINE = 184;
|
|
|
48
48
|
* `src/test/verification/feature-knob-settings-persistence.test.ts`. They are
|
|
49
49
|
* therefore genuinely behavior-verified locally and counted as such here.
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* The daemon's own mailbox and calendar keys (`surfaces.email.*`,
|
|
53
|
+
* `surfaces.calendar.*`), promoted into CONFIG_SCHEMA so the settings modal —
|
|
54
|
+
* which renders from that schema — can actually show them. Before, the
|
|
55
|
+
* daemon's handlers named these keys in their own error messages
|
|
56
|
+
* ("Set surfaces.calendar.caldavUrl and surfaces.calendar.caldavUser") while
|
|
57
|
+
* the UI that told an operator to set them could not display one of them.
|
|
58
|
+
*
|
|
59
|
+
* Declaring 25 keys raised the settings inventory `total` with no matching
|
|
60
|
+
* behavior coverage, which dropped `localBehaviorPercent` below its floor —
|
|
61
|
+
* the same arrival shape as the two lists above.
|
|
62
|
+
*
|
|
63
|
+
* THIS CONSTANT IS NOT A DIAL. Every key below is counted because
|
|
64
|
+
* `src/test/verification/daemon-mailbox-settings-persistence.test.ts` runs the
|
|
65
|
+
* same four-part persistence contract the other counted sets are counted for:
|
|
66
|
+
* schema default exposure, `set()` through the key's own validator to disk,
|
|
67
|
+
* reload into a fresh ConfigManager with read-back equality, and
|
|
68
|
+
* reset-to-default that also survives reload. That test also asserts this list
|
|
69
|
+
* is exactly the `surfaces.email.*` + `surfaces.calendar.*` schema key set and
|
|
70
|
+
* overlaps neither other counted set, so nothing is double-counted and nothing
|
|
71
|
+
* drifts in uncounted.
|
|
72
|
+
*
|
|
73
|
+
* PER-KEY EVIDENCE. All 25 are read by a LIVE consumer, which is why they were
|
|
74
|
+
* declared at all: the SDK's mail and calendar gateway compositions
|
|
75
|
+
* (platform/email/surface-config.ts and platform/calendar/caldav-gateway-config.ts)
|
|
76
|
+
* resolve every one of them when the daemon serves `email.*` and `calendar.*`.
|
|
77
|
+
* The three password keys additionally resolve through the daemon secret tier
|
|
78
|
+
* rather than from config, and carry no secret value in config themselves.
|
|
79
|
+
*/
|
|
80
|
+
export const DAEMON_MAILBOX_LOCAL_SETTINGS = [
|
|
81
|
+
'surfaces.email.host',
|
|
82
|
+
'surfaces.email.user',
|
|
83
|
+
'surfaces.email.username',
|
|
84
|
+
'surfaces.email.from',
|
|
85
|
+
'surfaces.email.password',
|
|
86
|
+
'surfaces.email.imapHost',
|
|
87
|
+
'surfaces.email.imapPort',
|
|
88
|
+
'surfaces.email.imapUser',
|
|
89
|
+
'surfaces.email.imapPassword',
|
|
90
|
+
'surfaces.email.imap.host',
|
|
91
|
+
'surfaces.email.imap.port',
|
|
92
|
+
'surfaces.email.imap.user',
|
|
93
|
+
'surfaces.email.imap.password',
|
|
94
|
+
'surfaces.email.imap.secure',
|
|
95
|
+
'surfaces.email.imap.mailbox',
|
|
96
|
+
'surfaces.email.imap.draftsMailbox',
|
|
97
|
+
'surfaces.email.smtp.host',
|
|
98
|
+
'surfaces.email.smtp.port',
|
|
99
|
+
'surfaces.email.smtp.password',
|
|
100
|
+
'surfaces.email.smtp.secure',
|
|
101
|
+
'surfaces.calendar.caldavUrl',
|
|
102
|
+
'surfaces.calendar.caldavUser',
|
|
103
|
+
'surfaces.calendar.caldavPassword',
|
|
104
|
+
'surfaces.calendar.defaultCalendarId',
|
|
105
|
+
'surfaces.calendar.calendars',
|
|
106
|
+
] as const;
|
|
107
|
+
|
|
51
108
|
export const FEATURE_KNOB_LOCAL_SETTINGS = [
|
|
52
109
|
'provider.optimizerMode',
|
|
53
110
|
'provider.optimizerPinnedModel',
|
|
@@ -247,6 +304,7 @@ const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE
|
|
|
247
304
|
+ FEATURE_KNOB_LOCAL_SETTINGS.length
|
|
248
305
|
+ DEVICE_AND_TRIGGER_LOCAL_SETTINGS.length
|
|
249
306
|
+ CLUSTER_GROUP_LOCAL_SETTINGS.length
|
|
307
|
+
+ DAEMON_MAILBOX_LOCAL_SETTINGS.length
|
|
250
308
|
+ ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
|
|
251
309
|
|
|
252
310
|
const EXTERNAL_SLASH_COMMANDS = new Set([
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '1.
|
|
9
|
+
let _version = '1.25.0';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
// Only trust a version read from OUR OWN package.json. A Bun single-file
|