@pellux/goodvibes-tui 1.24.2 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/bin/goodvibes-daemon +24 -0
- package/docs/foundation-artifacts/operator-contract.json +18234 -12253
- package/package.json +8 -7
- package/src/cli/help.ts +10 -0
- package/src/cli/redaction.ts +68 -3
- package/src/config/goodvibes-home.ts +149 -0
- package/src/config/secret-config.ts +64 -8
- package/src/core/turn-event-wiring.ts +7 -16
- package/src/core/workstream-notification.ts +69 -0
- package/src/daemon/cli.ts +62 -7
- package/src/daemon/handlers/credentials.ts +15 -5
- package/src/daemon/handlers/index.ts +7 -6
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +332 -0
- package/src/daemon/send/composition.ts +99 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/input/commands/calendar-runtime.ts +245 -0
- package/src/input/commands/config.ts +68 -2
- package/src/input/commands/connection-status.ts +258 -0
- package/src/input/commands/local-provider-runtime.ts +9 -1
- package/src/input/commands/mail-runtime.ts +267 -0
- package/src/input/commands/operator-runtime.ts +6 -1
- package/src/input/commands/payment-card-intake.ts +200 -0
- package/src/input/commands/profile-render.ts +250 -0
- package/src/input/commands/profile-runtime.ts +362 -0
- package/src/input/commands/profile-types.ts +381 -0
- package/src/input/commands/review-runtime.ts +13 -1
- package/src/input/commands.ts +11 -0
- package/src/input/composer-editor.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +13 -1
- package/src/input/payments-config.ts +167 -0
- package/src/input/provider-key-intake.ts +10 -2
- package/src/input/settings-modal-commit.ts +146 -0
- package/src/input/settings-modal-connections.ts +102 -0
- package/src/input/settings-modal-data.ts +30 -0
- package/src/input/settings-modal-reset.ts +6 -2
- package/src/input/settings-modal-secrets.ts +9 -2
- package/src/input/settings-modal-types.ts +19 -3
- package/src/input/settings-modal.ts +54 -107
- package/src/main.ts +7 -2
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/settings-modal-connections.ts +63 -0
- package/src/renderer/settings-modal-helpers.ts +6 -0
- package/src/renderer/settings-modal.ts +21 -1
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap.ts +2 -2
- package/src/runtime/context.ts +9 -0
- package/src/runtime/credential-composition.ts +54 -0
- package/src/runtime/daemon-handler-composition.ts +0 -4
- package/src/runtime/install-self-check-startup.ts +23 -0
- package/src/runtime/mail-composition.ts +65 -0
- package/src/runtime/onboarding/types.ts +6 -1
- package/src/runtime/path-shadow-startup.ts +243 -0
- package/src/runtime/path-shadow.ts +418 -0
- package/src/runtime/reachability-notice.ts +100 -0
- package/src/runtime/runtime-services-types.ts +243 -0
- package/src/runtime/services.ts +30 -224
- package/src/shell/ui-openers.ts +7 -0
- package/src/verification/verification-ledger.ts +158 -0
- package/src/version.ts +1 -1
- package/src/daemon/handlers/calendar/caldav-client.ts +0 -662
- package/src/daemon/handlers/calendar/ics.ts +0 -556
- package/src/daemon/handlers/calendar/index.ts +0 -316
- package/src/daemon/handlers/email/config.ts +0 -164
- package/src/daemon/handlers/email/imap-connector.ts +0 -441
- package/src/daemon/handlers/email/imap-parsing.ts +0 -499
- package/src/daemon/handlers/email/index.ts +0 -43
- package/src/daemon/handlers/email/read-handlers.ts +0 -80
- package/src/daemon/handlers/email/runtime.ts +0 -140
- package/src/daemon/handlers/email/smtp-connector.ts +0 -557
- package/src/daemon/handlers/email/validation.ts +0 -147
- package/src/daemon/handlers/email/write-handlers.ts +0 -133
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"build:all-shell": "bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:macos-x64 && bun run build:macos-arm64 && bun run build:windows",
|
|
44
44
|
"typecheck": "bunx tsc --noEmit",
|
|
45
45
|
"typecheck:test": "bunx tsc --noEmit -p tsconfig.test.json",
|
|
46
|
+
"typecheck:coverage": "bun run scripts/typecheck-coverage-rule.ts",
|
|
46
47
|
"test": "bun run scripts/run-tests.ts",
|
|
47
48
|
"test:coverage": "bun run scripts/coverage-gate.ts",
|
|
48
49
|
"version": "bun run scripts/prebuild.ts",
|
|
@@ -106,8 +107,8 @@
|
|
|
106
107
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
107
108
|
"@ast-grep/napi": "^0.42.0",
|
|
108
109
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
109
|
-
"@pellux/goodvibes-sdk": "1.
|
|
110
|
-
"@pellux/goodvibes-terminal-shell": "1.
|
|
110
|
+
"@pellux/goodvibes-sdk": "1.19.2",
|
|
111
|
+
"@pellux/goodvibes-terminal-shell": "1.19.2",
|
|
111
112
|
"bash-language-server": "^5.6.0",
|
|
112
113
|
"fuse.js": "^7.1.0",
|
|
113
114
|
"graphql": "^16.13.2",
|
|
@@ -133,10 +134,10 @@
|
|
|
133
134
|
"typescript": "^5.9.3"
|
|
134
135
|
},
|
|
135
136
|
"optionalDependencies": {
|
|
136
|
-
"@pellux/goodvibes-tui-linux-x64": "1.
|
|
137
|
-
"@pellux/goodvibes-tui-linux-arm64": "1.
|
|
138
|
-
"@pellux/goodvibes-tui-darwin-x64": "1.
|
|
139
|
-
"@pellux/goodvibes-tui-darwin-arm64": "1.
|
|
137
|
+
"@pellux/goodvibes-tui-linux-x64": "1.26.0",
|
|
138
|
+
"@pellux/goodvibes-tui-linux-arm64": "1.26.0",
|
|
139
|
+
"@pellux/goodvibes-tui-darwin-x64": "1.26.0",
|
|
140
|
+
"@pellux/goodvibes-tui-darwin-arm64": "1.26.0"
|
|
140
141
|
},
|
|
141
142
|
"trustedDependencies": [],
|
|
142
143
|
"overrides": {
|
package/src/cli/help.ts
CHANGED
|
@@ -341,6 +341,16 @@ export function renderGoodVibesDaemonHelp(binary = 'goodvibes-daemon'): string {
|
|
|
341
341
|
' service-status Show whether the daemon service is installed / enabled / active',
|
|
342
342
|
' migrate-service Guided migration from an install-script goodvibes-daemon.service unit; prints a plan',
|
|
343
343
|
' unless run with -y/--yes (never auto-migrates)',
|
|
344
|
+
' send [message] Send a message to one of your configured channels — Telegram, ntfy,',
|
|
345
|
+
' Discord, Slack, Google Chat, Signal, WhatsApp, iMessage, Teams,',
|
|
346
|
+
' BlueBubbles, Mattermost, Matrix or a webhook. Takes the message as',
|
|
347
|
+
' an argument or on stdin, so it composes with other tooling.',
|
|
348
|
+
' --channel <id> picks the channel; with none named it uses your one',
|
|
349
|
+
' configured channel and says which. --to <address> targets a specific',
|
|
350
|
+
' topic / chat / room within it, --title <text> sets a title, and',
|
|
351
|
+
' --list shows every channel with where it would send. A channel that',
|
|
352
|
+
' is off is refused rather than redirected to the default, and a',
|
|
353
|
+
' failed send exits non-zero with the provider\'s own error.',
|
|
344
354
|
' cluster <command> Share inbound channel work with your other machines on this network.',
|
|
345
355
|
' status | create | join | key | nodes | forget <machine> | rotate [--now]',
|
|
346
356
|
' | leave | rename | groups',
|
package/src/cli/redaction.ts
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
export const REDACTED_VALUE = '<redacted>';
|
|
2
2
|
|
|
3
|
+
// Matches a config path whose LAST SEGMENT is exactly one of these words. That
|
|
4
|
+
// is the whole reach of this pattern, and it is why the declared list below
|
|
5
|
+
// exists: `surfaces.calendar.caldavPassword` does not end in a `.password`
|
|
6
|
+
// segment, it ends in a `caldavPassword` segment, so this pattern answers false
|
|
7
|
+
// for it — as it does for `imapPassword`, `appPassword` and `authToken`.
|
|
3
8
|
const SENSITIVE_PATH_PATTERN = /(^|\.)(apiKey|accessToken|botToken|appToken|signingSecret|webhookSecret|verifyToken|verificationToken|secret|password|token|keyFile)$/i;
|
|
9
|
+
|
|
10
|
+
// Credential-bearing config keys that are sensitive by NAME rather than by the
|
|
11
|
+
// generic trailing-word list above. Every one of these was carried in the clear
|
|
12
|
+
// by a support bundle before it was listed here, because the pattern matches a
|
|
13
|
+
// trailing WORD and these names carry the credential word in the middle or
|
|
14
|
+
// prefixed by a protocol ("caldavPassword", "imapPassword", "appPassword",
|
|
15
|
+
// "authToken").
|
|
16
|
+
//
|
|
17
|
+
// The `*Ref` Cloudflare keys normally hold a `goodvibes://secrets/...`
|
|
18
|
+
// reference, which shouldRedactValue already lets through as safe. They are
|
|
19
|
+
// listed as a BACKSTOP, for the same reason the mail keys are worth listing
|
|
20
|
+
// twice over: it costs nothing, and it is what stands between a future code
|
|
21
|
+
// path that writes a literal token there and that token reaching a file the
|
|
22
|
+
// owner emails to someone for support.
|
|
23
|
+
const SENSITIVE_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
24
|
+
// Mailbox / CalDAV — the credentials the daemon polls mail and calendar with.
|
|
25
|
+
'surfaces.email.password',
|
|
26
|
+
'surfaces.email.imapPassword',
|
|
27
|
+
'surfaces.email.imap.password',
|
|
28
|
+
'surfaces.email.smtp.password',
|
|
29
|
+
'surfaces.calendar.caldavPassword',
|
|
30
|
+
// Telephony.
|
|
31
|
+
'surfaces.telephony.authToken',
|
|
32
|
+
'surfaces.msteams.appPassword',
|
|
33
|
+
// Cloudflare token references.
|
|
34
|
+
'cloudflare.apiTokenRef',
|
|
35
|
+
'cloudflare.tunnelTokenRef',
|
|
36
|
+
'cloudflare.accessServiceTokenRef',
|
|
37
|
+
'cloudflare.workerTokenRef',
|
|
38
|
+
'cloudflare.workerClientTokenRef',
|
|
39
|
+
// Cluster shared phrase.
|
|
40
|
+
'cluster.secret',
|
|
41
|
+
// Payment card material. ADDED to this set, never substituted for it: this
|
|
42
|
+
// set is a hardcoded list with no delegation, so replacing it wholesale with
|
|
43
|
+
// another product's would silently stop redacting whatever that one omits.
|
|
44
|
+
// The four names below end in no word the suffix pattern knows —
|
|
45
|
+
// "cardNumber", "cardExpiry" and "cardholderName" match none of them — which
|
|
46
|
+
// is the whole reason a declared list exists rather than a naming habit.
|
|
47
|
+
// (The `.map` below lowercases every entry; the lookup lowercases too.)
|
|
48
|
+
'payments.cardNumber',
|
|
49
|
+
'payments.cardExpiry',
|
|
50
|
+
'payments.cardCvv',
|
|
51
|
+
'payments.cardholderName',
|
|
52
|
+
].map((key) => key.toLowerCase()));
|
|
53
|
+
|
|
4
54
|
const SECRET_LIKE_TEXT_PATTERNS: readonly RegExp[] = [
|
|
5
55
|
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
6
56
|
/\bghp_[A-Za-z0-9_]{16,}\b/g,
|
|
@@ -13,7 +63,7 @@ const SECRET_LIKE_TEXT_PATTERNS: readonly RegExp[] = [
|
|
|
13
63
|
];
|
|
14
64
|
|
|
15
65
|
export function isSensitiveConfigPath(path: string): boolean {
|
|
16
|
-
return SENSITIVE_PATH_PATTERN.test(path);
|
|
66
|
+
return SENSITIVE_PATH_PATTERN.test(path) || SENSITIVE_CONFIG_KEYS.has(path.toLowerCase());
|
|
17
67
|
}
|
|
18
68
|
|
|
19
69
|
export function isRedactedValue(value: unknown): boolean {
|
|
@@ -25,9 +75,14 @@ export interface RedactedConfigResult<T> {
|
|
|
25
75
|
readonly redactedPaths: readonly string[];
|
|
26
76
|
}
|
|
27
77
|
|
|
78
|
+
// Redaction rule for sensitive config paths:
|
|
79
|
+
// - Non-string values: redact if truthy (i.e. non-null, non-undefined, non-zero, non-false).
|
|
80
|
+
// Rationale: zero and false are never meaningful secrets; null/undefined mean absent.
|
|
81
|
+
// - String values: redact non-empty strings that are not goodvibes:// secret refs.
|
|
82
|
+
// Rationale: empty string means unset; secret refs are safe placeholders, not raw values.
|
|
28
83
|
function shouldRedactValue(path: string, value: unknown): boolean {
|
|
29
84
|
if (!isSensitiveConfigPath(path)) return false;
|
|
30
|
-
if (typeof value !== 'string') return value !==
|
|
85
|
+
if (typeof value !== 'string') return value !== null && value !== undefined && Boolean(value);
|
|
31
86
|
if (value.trim().length === 0) return false;
|
|
32
87
|
if (value.startsWith('goodvibes://secrets/')) return false;
|
|
33
88
|
return true;
|
|
@@ -64,7 +119,17 @@ export function redactConfig<T>(config: T): RedactedConfigResult<T> {
|
|
|
64
119
|
}
|
|
65
120
|
|
|
66
121
|
export function redactText(input: string): string {
|
|
67
|
-
|
|
122
|
+
// Assignment form: keyword=value — anchored so 'monkey=' and 'donkey=' do NOT match.
|
|
123
|
+
// Matches: token=, access_token=, api_key=, api-key=, secret=, password= and colon form token: value
|
|
124
|
+
let output = input
|
|
125
|
+
.replace(
|
|
126
|
+
/(?<![A-Za-z])(?:access_token|api[_-]?key|secret|password|token)\s*=\s*([^ \t\r\n"'`]+)/gi,
|
|
127
|
+
(m, val) => m.slice(0, m.length - val.length) + REDACTED_VALUE,
|
|
128
|
+
)
|
|
129
|
+
.replace(
|
|
130
|
+
/(?<![A-Za-z])(?:access_token|api[_-]?key|secret|password|token)\s*:\s*([^ \t\r\n"'`]+)/gi,
|
|
131
|
+
(m, val) => m.slice(0, m.length - val.length) + REDACTED_VALUE,
|
|
132
|
+
);
|
|
68
133
|
for (const pattern of SECRET_LIKE_TEXT_PATTERNS) {
|
|
69
134
|
output = output.replace(pattern, REDACTED_VALUE);
|
|
70
135
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goodvibes-home.ts — where this process's GoodVibes tree lives, resolved in
|
|
3
|
+
* ONE place for every entry point.
|
|
4
|
+
*
|
|
5
|
+
* ## The defect this closes
|
|
6
|
+
*
|
|
7
|
+
* `GOODVIBES_HOME` relocates the tree root. It was read by exactly one file —
|
|
8
|
+
* `src/daemon/cli.ts` — and the CLIENT entry point (`src/main.ts`) called
|
|
9
|
+
* `homedir()` unconditionally, on the same object literal where it did honour
|
|
10
|
+
* `GOODVIBES_WORKING_DIR`. So a harness that set `GOODVIBES_HOME` to a
|
|
11
|
+
* throwaway directory and then ran a client command got a process that read and
|
|
12
|
+
* wrote the REAL tree: settings, workspace state, and — because the daemon-tier
|
|
13
|
+
* secret store is derived from the tree root — `~/.goodvibes/daemon/secrets.enc`.
|
|
14
|
+
*
|
|
15
|
+
* That is not hypothetical. An isolated round stored two throwaway credentials
|
|
16
|
+
* and they landed in the owner's live secret store, where they had to be
|
|
17
|
+
* removed by hand. An isolation boundary a process can walk out of is not a
|
|
18
|
+
* boundary; the honest fix is that no entry point resolves a home of its own.
|
|
19
|
+
*
|
|
20
|
+
* ## What each variable means
|
|
21
|
+
*
|
|
22
|
+
* - `GOODVIBES_HOME` — the tree ROOT, the directory `.goodvibes/` sits under.
|
|
23
|
+
* Setting it relocates settings, workspace, discovery roots, and every tier
|
|
24
|
+
* of the secret store. This is what an isolated harness or a service unit
|
|
25
|
+
* sets.
|
|
26
|
+
* - `GOODVIBES_DAEMON_HOME` — the daemon's own IDENTITY directory (auth users,
|
|
27
|
+
* operator tokens, daemon settings). It falls under the tree root unless set
|
|
28
|
+
* separately, and it names only that directory: it is not a second way to
|
|
29
|
+
* move the tree. Both entry points resolve it the same way now, so a client
|
|
30
|
+
* given only this variable no longer computes the daemon-tier secret path
|
|
31
|
+
* from the real home while everything else points at a sandbox.
|
|
32
|
+
*
|
|
33
|
+
* Both readers take `env` as an argument rather than reaching for
|
|
34
|
+
* `process.env`, so the resolution is exercisable without a test having to
|
|
35
|
+
* mutate the environment of the process running it.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
40
|
+
|
|
41
|
+
/** The two roots every surface derives its storage from. */
|
|
42
|
+
export interface GoodVibesHomeOwnership {
|
|
43
|
+
/** The tree root: the directory `.goodvibes/` sits under. */
|
|
44
|
+
readonly homeDirectory: string;
|
|
45
|
+
/** The daemon's own identity directory, under the tree root by default. */
|
|
46
|
+
readonly daemonHomeDirectory: string;
|
|
47
|
+
/**
|
|
48
|
+
* True when either `GOODVIBES_HOME` or `GOODVIBES_DAEMON_HOME` named an
|
|
49
|
+
* override (a blank/whitespace-only value does not count — see
|
|
50
|
+
* resolveGoodVibesHome). Callers that need to know "is this process running
|
|
51
|
+
* out of a relocated tree or identity directory" (e.g. to refuse adopting
|
|
52
|
+
* the machine's real service unit) read this instead of re-reading the raw
|
|
53
|
+
* environment variable themselves — this module is the one reader.
|
|
54
|
+
*/
|
|
55
|
+
readonly isOverridden: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The tree root for this process.
|
|
60
|
+
*
|
|
61
|
+
* A blank or whitespace-only value is treated as absent rather than as the
|
|
62
|
+
* empty path, because `GOODVIBES_HOME=` in a unit file or a shell wrapper means
|
|
63
|
+
* "unset", and resolving it to `''` would put the whole tree at the filesystem
|
|
64
|
+
* root.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveGoodVibesHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
67
|
+
const override = env['GOODVIBES_HOME']?.trim();
|
|
68
|
+
if (override) return isAbsolute(override) ? override : resolve(process.cwd(), override);
|
|
69
|
+
// The login home comes from the injected env first so the whole resolution is
|
|
70
|
+
// exercisable without a test mutating the environment it is running in. This
|
|
71
|
+
// is not a behaviour change: on Linux and macOS `homedir()` already prefers
|
|
72
|
+
// $HOME, and where it does not (Windows, where HOME is usually unset) the
|
|
73
|
+
// fallback below is what answers.
|
|
74
|
+
const loginHome = env['HOME']?.trim();
|
|
75
|
+
return loginHome ? loginHome : homedir();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The daemon's identity directory.
|
|
80
|
+
*
|
|
81
|
+
* @param homeDirectory - The already-resolved tree root, so a caller cannot
|
|
82
|
+
* accidentally combine an overridden root with a default daemon home.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveGoodVibesDaemonHome(
|
|
85
|
+
homeDirectory: string,
|
|
86
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
87
|
+
): string {
|
|
88
|
+
const override = env['GOODVIBES_DAEMON_HOME']?.trim();
|
|
89
|
+
if (!override) return join(homeDirectory, '.goodvibes', 'daemon');
|
|
90
|
+
return isAbsolute(override) ? override : resolve(process.cwd(), override);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The `.goodvibes` directory itself — the tree, not the root it sits under.
|
|
95
|
+
*
|
|
96
|
+
* This exists because the two were being derived independently and did not
|
|
97
|
+
* agree. `GOODVIBES_HOME` meant the tree ROOT to the runtime
|
|
98
|
+
* (`<GOODVIBES_HOME>/.goodvibes/...`) and meant the `.goodvibes` DIRECTORY to
|
|
99
|
+
* the two reporting scripts, which read it as
|
|
100
|
+
* `process.env.GOODVIBES_HOME ?? join(homedir(), '.goodvibes')`. Set it to
|
|
101
|
+
* `/tmp/sandbox` to isolate a run and the runtime wrote
|
|
102
|
+
* `/tmp/sandbox/.goodvibes/`, while a script inspected `/tmp/sandbox` as though
|
|
103
|
+
* it were already the tree — so the two halves of one round disagreed about
|
|
104
|
+
* which files were even in scope.
|
|
105
|
+
*
|
|
106
|
+
* That is the shape of an incident that has already happened here: a
|
|
107
|
+
* home-redirection variable honoured by one entry point and ignored by another
|
|
108
|
+
* put two throwaway credentials in the owner's live secret store. One variable
|
|
109
|
+
* has one meaning now, and it is the runtime's, because the runtime is what
|
|
110
|
+
* writes secrets and because an isolation harness needs a single root that
|
|
111
|
+
* everything falls under.
|
|
112
|
+
*
|
|
113
|
+
* A caller that genuinely wants to name a `.goodvibes` directory directly —
|
|
114
|
+
* auditing a tree that is not under any home — passes it explicitly rather than
|
|
115
|
+
* through the environment. Both scripts keep a `--home` flag for exactly that.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveGoodVibesTreeDirectory(env: NodeJS.ProcessEnv = process.env): string {
|
|
118
|
+
return join(resolveGoodVibesHome(env), '.goodvibes');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Both roots at once — what an entry point wants. */
|
|
122
|
+
export function resolveGoodVibesHomeOwnership(
|
|
123
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
124
|
+
): GoodVibesHomeOwnership {
|
|
125
|
+
const homeDirectory = resolveGoodVibesHome(env);
|
|
126
|
+
return {
|
|
127
|
+
homeDirectory,
|
|
128
|
+
daemonHomeDirectory: resolveGoodVibesDaemonHome(homeDirectory, env),
|
|
129
|
+
isOverridden: Boolean(env['GOODVIBES_HOME']?.trim()) || Boolean(env['GOODVIBES_DAEMON_HOME']?.trim()),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when either root was relocated for this process.
|
|
135
|
+
*
|
|
136
|
+
* The daemon lifecycle asks this to decide whether it may adopt the machine's
|
|
137
|
+
* service unit: a process told to run out of somewhere else is a throwaway, and
|
|
138
|
+
* one that adopted the unit had systemd supervise it as the machine's daemon
|
|
139
|
+
* for five hours while it read the real home's credentials.
|
|
140
|
+
*
|
|
141
|
+
* It lives here because the two variables have exactly one reader, and the
|
|
142
|
+
* answer is a question ABOUT them. A caller checking `process.env` for itself is
|
|
143
|
+
* how the second meaning that started all of this gets reintroduced — so the
|
|
144
|
+
* question is answered here rather than asked again somewhere else.
|
|
145
|
+
*/
|
|
146
|
+
export function hasOverriddenGoodVibesHome(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
147
|
+
return (env['GOODVIBES_HOME']?.trim() ?? '') !== ''
|
|
148
|
+
|| (env['GOODVIBES_DAEMON_HOME']?.trim() ?? '') !== '';
|
|
149
|
+
}
|
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config';
|
|
1
|
+
import { isSecretRefInput, isDaemonOwnedConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import type { ConfigKey } from './index.ts';
|
|
3
3
|
import type { SecretScope, SecretStorageMedium } from './secrets.ts';
|
|
4
4
|
|
|
5
5
|
export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
|
|
6
|
+
// Mailbox and CalDAV credentials. Their own CONFIG_SCHEMA descriptions read
|
|
7
|
+
// "Stored in the daemon secret tier, never in config" — and until they were
|
|
8
|
+
// listed here that sentence was aspirational: the settings modal wrote them
|
|
9
|
+
// as plain strings into a config JSON file, because membership in this set is
|
|
10
|
+
// the thing that routes an edit through the secret manager instead.
|
|
11
|
+
//
|
|
12
|
+
// The daemon reads each of these back with
|
|
13
|
+
// resolveConfigSecret('<key>') → GOODVIBES_<KEY>, which is exactly the store
|
|
14
|
+
// key buildGoodVibesSecretKey() writes — see
|
|
15
|
+
// daemon/handlers/inbox/providers/email.ts.
|
|
16
|
+
'surfaces.email.password',
|
|
17
|
+
'surfaces.email.imapPassword',
|
|
18
|
+
'surfaces.email.imap.password',
|
|
19
|
+
'surfaces.email.smtp.password',
|
|
20
|
+
'surfaces.calendar.caldavPassword',
|
|
21
|
+
// Telephony delivery credentials — same shape, same file, same gap.
|
|
22
|
+
'surfaces.telephony.authToken',
|
|
23
|
+
'surfaces.telephony.token',
|
|
24
|
+
'surfaces.telephony.webhookSecret',
|
|
6
25
|
'surfaces.slack.signingSecret',
|
|
7
26
|
'surfaces.slack.botToken',
|
|
8
27
|
'surfaces.slack.appToken',
|
|
@@ -23,6 +42,16 @@ export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
|
|
|
23
42
|
'surfaces.bluebubbles.password',
|
|
24
43
|
'surfaces.mattermost.botToken',
|
|
25
44
|
'surfaces.matrix.accessToken',
|
|
45
|
+
// TUI-local synthetic sub-keys, one level under the SDK's real `payments`
|
|
46
|
+
// section (not yet a scalar CONFIG_SCHEMA entry — same situation as
|
|
47
|
+
// tts.speed, behavior.notifyAfterSeconds, etc. in settings-modal-data.ts),
|
|
48
|
+
// hence the cast. See input/payments-config.ts for why these are named flat
|
|
49
|
+
// (payments.cardNumber, not payments.card.number): a flat one-level leaf is
|
|
50
|
+
// the shape the real ConfigManager tolerates for an undeclared key.
|
|
51
|
+
'payments.cardNumber' as ConfigKey,
|
|
52
|
+
'payments.cardExpiry' as ConfigKey,
|
|
53
|
+
'payments.cardCvv' as ConfigKey,
|
|
54
|
+
'payments.cardholderName' as ConfigKey,
|
|
26
55
|
]);
|
|
27
56
|
|
|
28
57
|
export interface SecretBackedConfigUpdate {
|
|
@@ -96,6 +125,28 @@ export function buildSecretBackedConfigUpdate(configKey: ConfigKey, rawValue: st
|
|
|
96
125
|
};
|
|
97
126
|
}
|
|
98
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Where a secret-backed write lands when the caller did not name a scope.
|
|
130
|
+
*
|
|
131
|
+
* A daemon-owned config key (`surfaces.*`, `payments.*`, `controlPlane.*`, ...)
|
|
132
|
+
* names a credential the DAEMON executes with, not this interactive client, so
|
|
133
|
+
* its secret material belongs in the daemon-scoped tier the daemon actually
|
|
134
|
+
* reads — the same rule the SDK's config-ownership.ts already applies to the
|
|
135
|
+
* `goodvibes://` reference that points at it.
|
|
136
|
+
*
|
|
137
|
+
* Defaulting these to 'user' (the historical behavior here) split the pair: the
|
|
138
|
+
* reference landed in the daemon's own settings file, because ConfigManager
|
|
139
|
+
* routes daemon-owned keys there, while the value it pointed at sat in a tier
|
|
140
|
+
* the daemon never resolves. The surface reported success and the daemon found
|
|
141
|
+
* nothing. For the mailbox password that is the whole feature failing silently —
|
|
142
|
+
* the daemon is the process that polls IMAP and answers over Telegram, and it
|
|
143
|
+
* does so with every surface closed. A payment card entered through
|
|
144
|
+
* /payments card is the same shape of failure at purchase time.
|
|
145
|
+
*/
|
|
146
|
+
export function defaultSecretBackedScope(configKey: ConfigKey): SecretScope {
|
|
147
|
+
return isDaemonOwnedConfigKey(configKey) ? 'daemon' : 'user';
|
|
148
|
+
}
|
|
149
|
+
|
|
99
150
|
export async function persistSecretBackedConfigValue(
|
|
100
151
|
configManager: SecretBackedConfigManager,
|
|
101
152
|
secretsManager: SecretBackedSecretStore | null | undefined,
|
|
@@ -104,16 +155,21 @@ export async function persistSecretBackedConfigValue(
|
|
|
104
155
|
options: { readonly scope?: SecretScope } = {},
|
|
105
156
|
): Promise<string> {
|
|
106
157
|
const update = buildSecretBackedConfigUpdate(configKey, rawValue);
|
|
107
|
-
const scope = options.scope ??
|
|
158
|
+
const scope = options.scope ?? defaultSecretBackedScope(configKey);
|
|
159
|
+
const medium = getSecretWriteMedium(configManager.get('storage.secretPolicy'));
|
|
160
|
+
|
|
161
|
+
// 1. Validate config write first. If setDynamic throws, no secret is written (avoids orphans).
|
|
162
|
+
configManager.setDynamic(configKey, update.configValue);
|
|
163
|
+
|
|
164
|
+
// 2. Write new secret only after config accepted it.
|
|
108
165
|
if (update.secretKey && update.secretValue !== undefined && secretsManager) {
|
|
109
|
-
await secretsManager.set(update.secretKey, update.secretValue, {
|
|
110
|
-
scope,
|
|
111
|
-
medium: getSecretWriteMedium(configManager.get('storage.secretPolicy')),
|
|
112
|
-
});
|
|
166
|
+
await secretsManager.set(update.secretKey, update.secretValue, { scope, medium });
|
|
113
167
|
}
|
|
168
|
+
|
|
169
|
+
// 3. Clear old secret — pass the same medium so plaintext-medium secrets are found for deletion.
|
|
114
170
|
if (update.clearSecretKey && secretsManager?.delete) {
|
|
115
|
-
await secretsManager.delete(update.clearSecretKey, { scope });
|
|
171
|
+
await secretsManager.delete(update.clearSecretKey, { scope, medium });
|
|
116
172
|
}
|
|
117
|
-
|
|
173
|
+
|
|
118
174
|
return update.configValue;
|
|
119
175
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UiRuntimeEvents, RuntimeEventBus } from '@/runtime/index.ts';
|
|
2
2
|
import { buildPersistedSessionContext, persistConversation } from '@/runtime/index.ts';
|
|
3
3
|
import { buildCompactionReceiptBlock } from './compaction-receipt.ts';
|
|
4
|
-
import {
|
|
4
|
+
import { workstreamFailureNotification } from './workstream-notification.ts';
|
|
5
5
|
import { persistTurnAnchors, recordTurnAnchor, summarizeTurnLabel } from './rewind-turn-anchors.ts';
|
|
6
6
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
7
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -328,21 +328,12 @@ export function wireTurnEventHandlers(
|
|
|
328
328
|
unsubs.push(events.workflows.on('WORKFLOW_CHAIN_FAILED', (payload) => {
|
|
329
329
|
if (!shouldFireAlert(focusTracker, configGet, 'behavior.notifyOnChainFailure')) return;
|
|
330
330
|
try {
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
// chain
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
} else if (payload.failureKind === 'max_turns') {
|
|
338
|
-
// A turn-budget exhaustion is a spent ceiling, not an infrastructure
|
|
339
|
-
// failure — narrate the limit and where it came from, from the typed
|
|
340
|
-
// event fields, never a regex of the prose reason.
|
|
341
|
-
notifyCompletion('GoodVibes — WRFC chain hit its turn budget', `chain ${payload.chainId.slice(0, 12)} ${formatTurnBudgetOutcome({ limit: payload.turnLimit, source: payload.turnLimitSource })}`, FORCE_NOTIFY_DURATION_MS);
|
|
342
|
-
} else {
|
|
343
|
-
const kindLabel = payload.failureKind === 'transport' ? 'transient transport error' : payload.reason;
|
|
344
|
-
notifyCompletion('GoodVibes — WRFC chain failed', `chain ${payload.chainId.slice(0, 12)} failed: ${kindLabel}`, FORCE_NOTIFY_DURATION_MS);
|
|
345
|
-
}
|
|
331
|
+
// Title and body come from workstream-notification.ts, which is where
|
|
332
|
+
// the three branches (cancelled / turn budget / failed) are narrated and
|
|
333
|
+
// tested. A notification is a message to a person, so it carries neither
|
|
334
|
+
// the internal name for the machinery nor the chain id.
|
|
335
|
+
const notice = workstreamFailureNotification(payload);
|
|
336
|
+
notifyCompletion(notice.title, notice.body, FORCE_NOTIFY_DURATION_MS);
|
|
346
337
|
} catch (err) {
|
|
347
338
|
logger.debug('turn-event-wiring: chain-failure notify error', { error: String(err) });
|
|
348
339
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workstream-notification.ts — the desktop notification a finished workstream
|
|
3
|
+
* pushes, in words rather than identifiers.
|
|
4
|
+
*
|
|
5
|
+
* These three notifications used to read:
|
|
6
|
+
*
|
|
7
|
+
* GoodVibes — WRFC chain failed
|
|
8
|
+
* chain 7f3a91c02b4e failed: review rejected
|
|
9
|
+
*
|
|
10
|
+
* A desktop notification is a message TO a person, so the standing rule applies
|
|
11
|
+
* to it the same way it applies to a chat message: no internal name for the
|
|
12
|
+
* machinery, and no register id. `WRFC` is the first; `7f3a91c02b4e` is the
|
|
13
|
+
* second, and it is not something the reader can do anything with — it is not
|
|
14
|
+
* the commit, not the branch, not the session.
|
|
15
|
+
*
|
|
16
|
+
* What the reader can act on is why the work stopped, so that is what the body
|
|
17
|
+
* now leads with. Two workstreams that end at the same moment are told apart by
|
|
18
|
+
* their reasons, which is the same principle the channel renderer follows: in
|
|
19
|
+
* plain words, never by an opaque identifier.
|
|
20
|
+
*
|
|
21
|
+
* Split out of turn-event-wiring.ts as a pure function so the text is testable
|
|
22
|
+
* on its own — the wiring itself cannot be asserted against without mocking a
|
|
23
|
+
* process-global notifier.
|
|
24
|
+
*/
|
|
25
|
+
import { formatTurnBudgetOutcome } from './turn-budget-outcome.ts';
|
|
26
|
+
|
|
27
|
+
/** The fields of WORKFLOW_CHAIN_FAILED this narration reads. */
|
|
28
|
+
export interface WorkstreamFailureNarrationInput {
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
readonly failureKind?: 'transport' | 'other' | 'cancelled' | 'max_turns' | undefined;
|
|
31
|
+
readonly turnLimit?: number | undefined;
|
|
32
|
+
readonly turnLimitSource?: 'default' | 'spawn-override' | 'policy-bound' | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WorkstreamNotification {
|
|
36
|
+
readonly title: string;
|
|
37
|
+
readonly body: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Title and body for a workstream that reached a terminal state.
|
|
42
|
+
*
|
|
43
|
+
* An operator cancellation is an intended stop, not a failure — it is narrated
|
|
44
|
+
* as cancelled (the reason already carries the landed-work count from the
|
|
45
|
+
* workstream's edit ledger) so the notification never contradicts the cancelled
|
|
46
|
+
* workstream/owner/cohort surfaces. A turn-budget exhaustion is a spent ceiling
|
|
47
|
+
* rather than an infrastructure error, and its limit and source are read from
|
|
48
|
+
* the typed event fields, never from a regex of the prose reason.
|
|
49
|
+
*/
|
|
50
|
+
export function workstreamFailureNotification(
|
|
51
|
+
payload: WorkstreamFailureNarrationInput,
|
|
52
|
+
): WorkstreamNotification {
|
|
53
|
+
if (payload.failureKind === 'cancelled') {
|
|
54
|
+
return {
|
|
55
|
+
title: 'GoodVibes — workstream cancelled',
|
|
56
|
+
body: `Cancelled: ${payload.reason}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (payload.failureKind === 'max_turns') {
|
|
60
|
+
return {
|
|
61
|
+
title: 'GoodVibes — workstream hit its turn budget',
|
|
62
|
+
body: `The workstream ${formatTurnBudgetOutcome({ limit: payload.turnLimit, source: payload.turnLimitSource })}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
title: 'GoodVibes — workstream failed',
|
|
67
|
+
body: `Failed: ${payload.failureKind === 'transport' ? 'transient transport error' : payload.reason}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/daemon/cli.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
2
|
import {
|
|
4
3
|
ConfigManager,
|
|
5
4
|
deriveControlPlaneBaseUrl,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
} from '@pellux/goodvibes-sdk/platform/config';
|
|
9
8
|
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
10
9
|
import { formatProviderModel, getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
|
|
10
|
+
import { resolveGoodVibesHomeOwnership } from '../config/goodvibes-home.ts';
|
|
11
11
|
import { RuntimeEventBus, GlobalNetworkTransportInstaller } from '@/runtime/index.ts';
|
|
12
12
|
import { bindFeatureSettingsBridge, createFeatureFlagManager, deriveFeatureStates } from '@/runtime/index.ts';
|
|
13
13
|
import { createRuntimeStore } from '../runtime/store/index.ts';
|
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
persistProviders,
|
|
35
35
|
} from '@pellux/goodvibes-sdk/platform/discovery';
|
|
36
36
|
import { createSafeHostServeFactory } from './safe-serve.ts';
|
|
37
|
+
import { runSendCommand } from './send/command.ts';
|
|
38
|
+
import { createSendStack } from './send/composition.ts';
|
|
39
|
+
import { readAllStdin } from './send/stdin.ts';
|
|
37
40
|
import { isDaemonServiceSubcommand, resolveInstalledDaemonBinary, runDaemonServiceCli } from './service-commands.ts';
|
|
38
41
|
import { runClusterCommand } from '../cluster/commands.ts';
|
|
39
42
|
import { resolveConfiguredServiceName } from '../runtime/legacy-daemon-migration.ts';
|
|
@@ -59,6 +62,8 @@ type DaemonCliOwnership = {
|
|
|
59
62
|
readonly homeDirectory: string;
|
|
60
63
|
/** The daemon's OWN identity home (auth users, operator tokens, daemon settings). */
|
|
61
64
|
readonly daemonHomeDirectory: string;
|
|
65
|
+
/** True when GOODVIBES_HOME or GOODVIBES_DAEMON_HOME named an override — see goodvibes-home.ts. */
|
|
66
|
+
readonly isOverridden: boolean;
|
|
62
67
|
};
|
|
63
68
|
|
|
64
69
|
// CLI flag parsing delegated to shared module — see src/cli-flags.ts
|
|
@@ -83,15 +88,16 @@ type DaemonCliTokens = {
|
|
|
83
88
|
* daemon home then falls under it unless separately overridden.
|
|
84
89
|
*/
|
|
85
90
|
function resolveDaemonCliOwnership(): DaemonCliOwnership {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
// Both roots come from src/config/goodvibes-home.ts, which the CLIENT entry
|
|
92
|
+
// point also uses. They were resolved independently, and the client's copy
|
|
93
|
+
// simply did not read GOODVIBES_HOME — so a redirected client wrote into the
|
|
94
|
+
// real tree while the daemon honoured the redirect.
|
|
95
|
+
const { homeDirectory, daemonHomeDirectory, isOverridden } = resolveGoodVibesHomeOwnership();
|
|
91
96
|
return {
|
|
92
97
|
workingDirectory: process.env['GOODVIBES_WORKING_DIR'] ?? process.cwd(),
|
|
93
98
|
homeDirectory,
|
|
94
99
|
daemonHomeDirectory,
|
|
100
|
+
isOverridden,
|
|
95
101
|
};
|
|
96
102
|
}
|
|
97
103
|
|
|
@@ -137,6 +143,44 @@ async function main(): Promise<void> {
|
|
|
137
143
|
process.exit(result.exitCode);
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
// `send …` is intercepted here for both of the reasons `cluster` is, plus one
|
|
147
|
+
// of its own.
|
|
148
|
+
//
|
|
149
|
+
// Before the parser, because the message is arbitrary operator text: a
|
|
150
|
+
// message beginning with a dash, or one carrying `--port` inside it, must
|
|
151
|
+
// reach the channel rather than be eaten as a daemon flag.
|
|
152
|
+
//
|
|
153
|
+
// Before the runtime, because this composes only the services a delivery
|
|
154
|
+
// needs (see send/composition.ts) and must not start a second copy of the
|
|
155
|
+
// pollers, cluster election and LAN scan a running daemon already owns.
|
|
156
|
+
//
|
|
157
|
+
// And it must work when NO daemon is running, which is much of the point: the
|
|
158
|
+
// reason to message the owner is usually that something stopped.
|
|
159
|
+
if (rawArgs[0] === 'send') {
|
|
160
|
+
const ownership = resolveDaemonCliOwnership();
|
|
161
|
+
runDaemonConfigMigration(ownership.homeDirectory);
|
|
162
|
+
const stack = createSendStack({
|
|
163
|
+
workingDirectory: ownership.workingDirectory,
|
|
164
|
+
homeDirectory: ownership.homeDirectory,
|
|
165
|
+
daemonHomeDirectory: ownership.daemonHomeDirectory,
|
|
166
|
+
});
|
|
167
|
+
const result = await runSendCommand(rawArgs.slice(1), {
|
|
168
|
+
configManager: stack.configManager,
|
|
169
|
+
deliver: stack.deliver,
|
|
170
|
+
readStdin: readAllStdin,
|
|
171
|
+
stdinIsTty: process.stdin.isTTY === true,
|
|
172
|
+
});
|
|
173
|
+
for (const line of result.lines) {
|
|
174
|
+
// eslint-disable-next-line no-console
|
|
175
|
+
if (result.exitCode === 0) console.log(line);
|
|
176
|
+
else console.error(line);
|
|
177
|
+
}
|
|
178
|
+
// The activity log holds the OUTBOUND_HTTP record for the send that just
|
|
179
|
+
// happened (or did not); a process exiting this promptly would drop it.
|
|
180
|
+
flushActivityLogSync();
|
|
181
|
+
process.exit(result.exitCode);
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
// Parse CLI flags first so --daemon-home and --working-dir env vars are set
|
|
141
185
|
// before resolveDaemonCliOwnership() reads them.
|
|
142
186
|
const cli = parseGoodVibesCli(process.argv.slice(2), 'goodvibes-daemon');
|
|
@@ -169,7 +213,7 @@ async function main(): Promise<void> {
|
|
|
169
213
|
logger.info('daemon: --working-dir flag applied', { workingDir: cliFlags.workingDir });
|
|
170
214
|
}
|
|
171
215
|
|
|
172
|
-
const { workingDirectory: workingDir, homeDirectory, daemonHomeDirectory } = resolveDaemonCliOwnership();
|
|
216
|
+
const { workingDirectory: workingDir, homeDirectory, daemonHomeDirectory, isOverridden: hasOverriddenHome } = resolveDaemonCliOwnership();
|
|
173
217
|
runDaemonConfigMigration(homeDirectory);
|
|
174
218
|
const config = new ConfigManager({ workingDir, homeDir: homeDirectory, surfaceRoot: 'tui' });
|
|
175
219
|
new GlobalNetworkTransportInstaller().install(config);
|
|
@@ -314,6 +358,11 @@ async function main(): Promise<void> {
|
|
|
314
358
|
getConversationTitle: () => 'goodvibes daemon',
|
|
315
359
|
workingDir,
|
|
316
360
|
homeDirectory,
|
|
361
|
+
// Honour --daemon-home / GOODVIBES_DAEMON_HOME on the CREDENTIAL store, not
|
|
362
|
+
// just the identity directory. Without this a daemon told to run out of a
|
|
363
|
+
// temp tree still read the real home's daemon secrets, so "isolating" a
|
|
364
|
+
// test daemon left it holding the owner's live credentials.
|
|
365
|
+
daemonHomeDirectory,
|
|
317
366
|
// The standalone daemon observes externally-launched coding-agent sessions
|
|
318
367
|
// on the host read-only (fleet visibility + steer; never counted, never
|
|
319
368
|
// stopped). Daemon-side only — the interactive process reads this snapshot
|
|
@@ -361,6 +410,12 @@ async function main(): Promise<void> {
|
|
|
361
410
|
// undefined for a non-binary install, so a dev run stays host-managed and
|
|
362
411
|
// never swaps the interpreter. See lifecycle.ts.
|
|
363
412
|
updateArtifact: resolveDaemonUpdateArtifact({ version: VERSION }),
|
|
413
|
+
// A daemon running out of an overridden home must never adopt the machine's
|
|
414
|
+
// service unit. One did: started from a scratchpad with --daemon-home, it
|
|
415
|
+
// found the unit not running, wrote its own scratchpad ExecStart into
|
|
416
|
+
// ~/.config/systemd/user/goodvibes.service and exited, and systemd then
|
|
417
|
+
// supervised the throwaway as the machine's daemon for five hours.
|
|
418
|
+
hasOverriddenHome,
|
|
364
419
|
// The SAME coordinator this repository's inbox poller registered with (see
|
|
365
420
|
// runtime/cluster-composition.ts). The facade must reuse it rather than
|
|
366
421
|
// compose its own: two coordinators in one process are two nodes in the
|