@pellux/goodvibes-daemon 1.28.19 → 1.28.20
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 +85 -63
- package/README.md +13 -13
- package/bin/launcher-support.js +5 -5
- package/package.json +6 -4
- package/scripts/postinstall.js +8 -8
- package/src/cli/command-catalog.ts +22 -22
- package/src/cli/completion.ts +4 -4
- package/src/cli/help.ts +5 -5
- package/src/cli/index.ts +3 -3
- package/src/cli/parser.ts +2 -2
- package/src/cli/surface-catalog.ts +1 -1
- package/src/cli/types.ts +2 -2
- package/src/cluster/daemon-ws-call.ts +5 -5
- package/src/cluster/raw-reply-route.ts +5 -5
- package/src/config/checkpoint-settings.ts +7 -7
- package/src/config/config-key-guard.ts +22 -0
- package/src/config/run-daemon-config-migration.ts +3 -3
- package/src/config/secret-config.ts +7 -7
- package/src/config/surface.ts +3 -3
- package/src/core/pairing-banner.ts +5 -5
- package/src/daemon/cli.ts +45 -43
- package/src/daemon/config-command.ts +15 -15
- package/src/daemon/handlers/context.ts +1 -1
- package/src/daemon/handlers/contracts.ts +4 -4
- package/src/daemon/handlers/credentials.ts +1 -1
- package/src/daemon/handlers/drafts/draft-store.ts +3 -3
- package/src/daemon/handlers/drafts/register.ts +4 -4
- package/src/daemon/handlers/inbox/aggregator.ts +8 -8
- package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
- package/src/daemon/handlers/inbox/index.ts +7 -7
- package/src/daemon/handlers/inbox/mapping.ts +2 -2
- package/src/daemon/handlers/inbox/poller.ts +5 -5
- package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
- package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
- package/src/daemon/handlers/inbox/providers/email.ts +3 -3
- package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
- package/src/daemon/handlers/index.ts +18 -8
- package/src/daemon/handlers/payments/card-store.ts +486 -0
- package/src/daemon/handlers/payments/index.ts +32 -0
- package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
- package/src/daemon/handlers/payments/register.ts +391 -0
- package/src/daemon/handlers/register.ts +3 -3
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
- package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
- package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
- package/src/daemon/handlers/remote/backends/types.ts +2 -2
- package/src/daemon/handlers/remote/dispatcher.ts +3 -3
- package/src/daemon/handlers/remote/index.ts +1 -1
- package/src/daemon/handlers/remote/peer-registry.ts +62 -13
- package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
- package/src/daemon/handlers/routing/index.ts +1 -1
- package/src/daemon/handlers/routing/route-store.ts +1 -1
- package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
- package/src/daemon/handlers/sqlite-store.ts +9 -9
- package/src/daemon/handlers/triage/index.ts +1 -1
- package/src/daemon/handlers/triage/integration.ts +3 -3
- package/src/daemon/handlers/triage/pipeline.ts +2 -2
- package/src/daemon/handlers/triage/scorer.ts +2 -2
- package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
- package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
- package/src/daemon/handlers/triage/tagger/index.ts +1 -1
- package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
- package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
- package/src/daemon/handlers/triage/types.ts +2 -2
- package/src/daemon/lifecycle.ts +5 -5
- package/src/daemon/local-daemon-state.ts +7 -7
- package/src/daemon/pair-command.ts +14 -14
- package/src/daemon/provision-wake-model.ts +5 -5
- package/src/daemon/send/channels.ts +7 -7
- package/src/daemon/send/command.ts +11 -11
- package/src/daemon/send/composition.ts +5 -5
- package/src/daemon/send/failure-text.ts +6 -6
- package/src/daemon/send/inert-text.ts +18 -18
- package/src/daemon/send/stdin.ts +3 -3
- package/src/daemon/service-commands.ts +32 -32
- package/src/daemon/sessions-command.ts +7 -7
- package/src/daemon/status-command.ts +22 -22
- package/src/daemon/webui-command.ts +14 -14
- package/src/runtime/boot-tasks.ts +1 -1
- package/src/runtime/cluster-composition.ts +9 -9
- package/src/runtime/cluster-group-composition.ts +7 -7
- package/src/runtime/conversation-rewind-port.ts +8 -8
- package/src/runtime/credential-composition.ts +2 -2
- package/src/runtime/daemon-handler-composition.ts +44 -4
- package/src/runtime/device-posture-composition.ts +10 -10
- package/src/runtime/disposal-wiring.ts +8 -8
- package/src/runtime/fleet-needs-input-push.ts +4 -4
- package/src/runtime/fleet-services.ts +1 -1
- package/src/runtime/hosted-session-composition.ts +13 -13
- package/src/runtime/index.ts +1 -1
- package/src/runtime/knowledge-services.ts +2 -2
- package/src/runtime/legacy-daemon-migration.ts +43 -43
- package/src/runtime/legacy-daemon-reconcile.ts +30 -30
- package/src/runtime/mail-composition.ts +6 -6
- package/src/runtime/notification-dispatch.ts +7 -7
- package/src/runtime/payments-composition.ts +143 -0
- package/src/runtime/plugin-composition.ts +7 -7
- package/src/runtime/runtime-services-types.ts +9 -9
- package/src/runtime/services.ts +29 -27
- package/src/runtime/trigger-services.ts +1 -1
- package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
- package/src/runtime/trust/trust-gated-approvals.ts +9 -9
- package/src/runtime/update-check.ts +4 -4
- package/src/runtime/workspace-checkpointing.ts +6 -6
- package/src/testing/daemon-fixture.ts +11 -11
- package/src/testing/hosted-session-failures.ts +4 -4
- package/src/version.ts +2 -2
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Teardown for every poller the daemon's runtime graph starts.
|
|
5
5
|
*
|
|
6
|
-
* The mechanics
|
|
7
|
-
* all-required owner list
|
|
6
|
+
* The mechanics, the ordered, best-effort, idempotent scope and the
|
|
7
|
+
* all-required owner list, live in the SDK (`platform/runtime/disposal`), so
|
|
8
8
|
* the daemon and the SDK's own composition root cannot drift into two different
|
|
9
9
|
* ideas of what "stop the graph" means. What lives here is only the mapping from
|
|
10
10
|
* the daemon's assembled graph onto that list, plus the four pollers the daemon
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Ownership note, and the reason any of this exists: this product builds the
|
|
20
20
|
* graph and hands the SAME object to `DaemonServer`. The SDK facade disposes
|
|
21
21
|
* only a graph it constructed itself, so it deliberately leaves this one alone
|
|
22
|
-
|
|
22
|
+
*, nothing upstream will ever stop these pollers. The shutdown paths in
|
|
23
23
|
* daemon/cli.ts and the one-shot CLI commands are the only things that can.
|
|
24
24
|
*
|
|
25
25
|
* The owner type below is declared structurally rather than imported from
|
|
@@ -44,7 +44,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
|
|
|
44
44
|
/**
|
|
45
45
|
* Daemon-only: the wake-word recovery sweep and a pending boot provision
|
|
46
46
|
* Started only when an entrypoint opted into boot
|
|
47
|
-
* provisioning, and a no-op otherwise
|
|
47
|
+
* provisioning, and a no-op otherwise, but it is on this list unconditionally,
|
|
48
48
|
* because "the graph did not start it this time" is not a reason for the
|
|
49
49
|
* teardown path to have no way to stop it.
|
|
50
50
|
*/
|
|
@@ -53,7 +53,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
|
|
|
53
53
|
* Daemon-only: the product handler surfaces (daemon-handler-composition.ts).
|
|
54
54
|
*
|
|
55
55
|
* `unregister()` detaches the gateway handlers AND stops the two pollers this
|
|
56
|
-
* product's inbox surface owns
|
|
56
|
+
* product's inbox surface owns, the retention sweep inside `InboxCursorStore`
|
|
57
57
|
* and the per-provider `InboundPoller` intervals. The SDK does not know either
|
|
58
58
|
* exists, so if this surface does not stop them nothing does.
|
|
59
59
|
*/
|
|
@@ -68,7 +68,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* The poller owners this daemon holds that are NOT reachable from the
|
|
71
|
-
* assembled graph
|
|
71
|
+
* assembled graph, handles the factory keeps as locals.
|
|
72
72
|
*/
|
|
73
73
|
export interface RuntimeDisposalExtras {
|
|
74
74
|
/** Handle returned by `ConfigManager.watchConfigFiles()`. */
|
|
@@ -79,7 +79,7 @@ export interface RuntimeDisposalExtras {
|
|
|
79
79
|
* Register the stop call for every poller the graph started.
|
|
80
80
|
*
|
|
81
81
|
* `services` is the fully-assembled graph, which already exposes each poller
|
|
82
|
-
* owner as a field
|
|
82
|
+
* owner as a field, so a poller whose owner reaches the public surface is
|
|
83
83
|
* wired by name rather than by threading another local out of the factory.
|
|
84
84
|
*/
|
|
85
85
|
export function registerDaemonRuntimePollers(
|
|
@@ -93,7 +93,7 @@ export function registerDaemonRuntimePollers(
|
|
|
93
93
|
registry.add('wake-word housekeeping', services.stopWakeHousekeeping);
|
|
94
94
|
// Registered LAST so it tears down FIRST (the scope unwinds in reverse), which
|
|
95
95
|
// is the order daemon/cli.ts already used by hand: release the handler surfaces
|
|
96
|
-
|
|
96
|
+
//, closing the inbox store and stopping its poll timers, before the pollers
|
|
97
97
|
// the rest of the graph owns. Being on this list is what makes a plain
|
|
98
98
|
// `dispose()` total: every shutdown path stops these, not just the one that
|
|
99
99
|
// remembered to call `unregister()` itself.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fleet-needs-input-push.ts
|
|
2
|
+
* fleet-needs-input-push.ts, wires the fleet "needs-input" browser-push fan-out
|
|
3
3
|
* that the SDK's registerGatewayVerbGroups (reached through the terminal-shell's
|
|
4
4
|
* attachWsOnlyGatewayVerbHandlers wrapper) gates on two deps: `runtimeBus` and
|
|
5
5
|
* `sessionPresence` (see push/service.ts's attachFleetNeedsInputSource). When a
|
|
6
6
|
* fleet node blocks on the operator, it pushes an "Input needed" notification
|
|
7
|
-
* carrying the session/node deep link to every registered device
|
|
7
|
+
* carrying the session/node deep link to every registered device, suppressed
|
|
8
8
|
* while an operator surface is already attached to that node's session.
|
|
9
9
|
*
|
|
10
10
|
* TWO seams have to be live for a real push, not just descriptor presence:
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* runtime bus's 'fleet' domain in the first place. That is
|
|
13
13
|
* attachFleetEmitBridge, diffing the process registry's own coalesced
|
|
14
14
|
* snapshot tick into per-node lifecycle events. The terminal-shell package's
|
|
15
|
-
* createArchivableFleetRegistry only builds the registry
|
|
16
|
-
* attach the bridge
|
|
15
|
+
* createArchivableFleetRegistry only builds the registry, it does not
|
|
16
|
+
* attach the bridge, so the composition root that owns both the registry
|
|
17
17
|
* and the bus (this file) is the one place responsible for it. The SDK's
|
|
18
18
|
* own daemon composition (platform/runtime/services.ts) attaches this same
|
|
19
19
|
* bridge right after building its process registry.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fleet-services.ts
|
|
2
|
+
* fleet-services.ts, the shared, archive-aware fleet registry construction,
|
|
3
3
|
* lifted out of services.ts so the composition root stays under the file-size
|
|
4
4
|
* cap. Mirrors the sibling create*Services helpers (durability, code-index,
|
|
5
5
|
* workstream): a single dependency-injected call that services.ts makes once.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hosted-session-composition.ts
|
|
2
|
+
* hosted-session-composition.ts, what this daemon states so it may host
|
|
3
3
|
* conversation loops.
|
|
4
4
|
*
|
|
5
5
|
* The engine is the SDK's (`@pellux/goodvibes-sdk/platform/hosted-sessions`):
|
|
6
6
|
* lifecycle, the detach policy, the bounded disk state, the verbs, the
|
|
7
7
|
* lifecycle channel. It is off until a product says how a workspace FLOOR is
|
|
8
|
-
* built, and that is deliberate
|
|
8
|
+
* built, and that is deliberate, the floor's `requestApproval` seam is where a
|
|
9
9
|
* product's trust posture lives, and no default can stand in for a decision
|
|
10
10
|
* about who may write files and run commands in a directory a client named over
|
|
11
11
|
* the wire.
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* session hosted in another directory has to be gated by that directory's
|
|
21
21
|
* decision, not by the daemon's.
|
|
22
22
|
*
|
|
23
|
-
* Everything else in a floor comes from `createClientRuntimeServices
|
|
24
|
-
* same composition a terminal runs
|
|
23
|
+
* Everything else in a floor comes from `createClientRuntimeServices`, the
|
|
24
|
+
* same composition a terminal runs, so a hosted turn's tools, hooks, plugins
|
|
25
25
|
* and model stack are the ones the platform already has, not a second set.
|
|
26
26
|
*
|
|
27
27
|
* ── The exec posture a hosted turn runs under ──────────────────────────────
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
* "The same composition a terminal runs" is what BUILDS the exec tool, and it
|
|
30
30
|
* builds it the same way here: the `sandbox.*` config and the `exec-sandbox`
|
|
31
31
|
* gate this daemon already reads produce the same bubblewrap boundary a local
|
|
32
|
-
* exec gets
|
|
32
|
+
* exec gets, network, PID, UTS and IPC namespaced, the system read-only, /tmp
|
|
33
33
|
* and $HOME masked, the workspace writable, and `sandbox.egressAllowlist` the
|
|
34
|
-
* one way network comes back
|
|
34
|
+
* one way network comes back, with the self-labelling note on every result.
|
|
35
35
|
*
|
|
36
36
|
* What was NOT the same was the fallback. When no boundary could be applied,
|
|
37
37
|
* the exec tool ran the command on the host and said so, which is right for a
|
|
38
38
|
* terminal (a person asked, a person is reading) and wrong for a turn nobody is
|
|
39
39
|
* watching. A hosted conversational turn reached the whole host that way: the
|
|
40
|
-
* full process table, the owner's /proc, and his tmux session
|
|
40
|
+
* full process table, the owner's /proc, and his tmux session, where it typed.
|
|
41
41
|
*
|
|
42
42
|
* So this daemon states the posture rather than inheriting a default:
|
|
43
43
|
*
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* REQUIRED: a command that cannot be contained is refused, naming why, and
|
|
47
47
|
* running on the host is not a fallback available to it. The owner's
|
|
48
48
|
* terminal is denied outright, at the exec guard, regardless of boundary.
|
|
49
|
-
* - WORKSTREAM
|
|
50
|
-
* need the machine itself
|
|
49
|
+
* - WORKSTREAM, a real work chain the owner authorized, which may genuinely
|
|
50
|
+
* need the machine itself, is a per-spawn grant this composition makes
|
|
51
51
|
* explicitly. There is no such spawn today, so the function below never
|
|
52
52
|
* returns it; when there is one, the grant is written HERE, in the daemon's
|
|
53
53
|
* own composition, where a reader can see which spawns hold it. Nothing on
|
|
@@ -75,13 +75,13 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
|
|
|
75
75
|
[
|
|
76
76
|
'You are a GoodVibes session hosted by the daemon rather than by a terminal.',
|
|
77
77
|
`Your working directory is ${input.workspaceRoot}.`,
|
|
78
|
-
'Someone may be attached and watching this turn, or may have detached and read it later
|
|
78
|
+
'Someone may be attached and watching this turn, or may have detached and read it later;',
|
|
79
79
|
'write for both. Say what you did and why; never report work you did not do.',
|
|
80
80
|
'Tool permissions are decided by whoever is attached: an ask you raise may take a while to be',
|
|
81
81
|
'answered, and an unanswered one is a refusal, not a reason to find another way round.',
|
|
82
82
|
].join(' '),
|
|
83
83
|
// The same contract every other conversational turn is held to, from the
|
|
84
|
-
// SDK rather than restated here
|
|
84
|
+
// SDK rather than restated here, a second copy is a copy that drifts.
|
|
85
85
|
CONVERSATIONAL_DIAGNOSIS_SECTION,
|
|
86
86
|
].join('\n\n');
|
|
87
87
|
}
|
|
@@ -92,7 +92,7 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
|
|
|
92
92
|
* Every session this daemon hosts is a conversation, so every one of them is
|
|
93
93
|
* contained (see the module header). Stated on the floor rather than left to
|
|
94
94
|
* the engine's default so that the day a workstream spawn exists, the grant is
|
|
95
|
-
* a visible change to THIS function and not an option someone set elsewhere
|
|
95
|
+
* a visible change to THIS function and not an option someone set elsewhere,
|
|
96
96
|
* and it sits beside the trust gate, which is the other statement this daemon
|
|
97
97
|
* makes about how much authority a hosted run carries.
|
|
98
98
|
*/
|
|
@@ -162,7 +162,7 @@ export function createHostedSessionOptions(services: RuntimeServices): DaemonHos
|
|
|
162
162
|
// boot (the persisted discovery cache) and from the LAN scan; a floor
|
|
163
163
|
// builds its own registry and would otherwise be the only place on this
|
|
164
164
|
// box where they are not routable. Servers found by a scan that finishes
|
|
165
|
-
// AFTER a floor is built reach the next floor, not this one
|
|
165
|
+
// AFTER a floor is built reach the next floor, not this one, stated
|
|
166
166
|
// rather than hidden, because a wrong claim here would look like a model
|
|
167
167
|
// that exists everywhere except in hosted sessions.
|
|
168
168
|
const discovered = services.providerRegistry.listDiscoveredServers();
|
package/src/runtime/index.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Type-only: the runtime namespace objects must never be READ at module scope.
|
|
16
16
|
// An eager `export const X = ns.X` compiles to a top-level property read off a
|
|
17
17
|
// lazy namespace object, and Bun's single-file compiler orders module bodies
|
|
18
|
-
// nondeterministically
|
|
18
|
+
// nondeterministically, on some builds the read lands before the defining
|
|
19
19
|
// module and the binary dies at load. Values below are grouped live re-exports
|
|
20
20
|
// from the SDK's registered subpaths instead; the toolchain post-build-smoke
|
|
21
21
|
// scans compiled artifacts for the eager pattern and fails the build if one
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// knowledge-services.ts
|
|
2
|
+
// knowledge-services.ts, the knowledge/wiki + home-graph stack
|
|
3
3
|
//
|
|
4
4
|
// Constructs the three KnowledgeStores (regular wiki, agent, home-graph), their
|
|
5
5
|
// semantic services, the ingest services, the home-graph service, and the
|
|
6
6
|
// project-planning + work-plan stores. Extracted into its own module rather
|
|
7
7
|
// than built inline in services.ts, which sits at the architecture check's
|
|
8
|
-
// 800-line cap (scripts/check-architecture.ts)
|
|
8
|
+
// 800-line cap (scripts/check-architecture.ts), new/large construction blocks
|
|
9
9
|
// get their own module and a single wiring call there (mirrors
|
|
10
10
|
// createWorkstreamServices / createDurabilityServices / createCodeIndexServices).
|
|
11
11
|
//
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* CLI (`src/daemon/service-commands.ts`, the `migrate-service` subcommand)
|
|
4
4
|
* and by this daemon's own boot-time reconcile (`legacy-daemon-reconcile.ts`).
|
|
5
5
|
* The terminal app ships an independent implementation of the same
|
|
6
|
-
* detect/migrate contract for its own onboarding guided UX
|
|
6
|
+
* detect/migrate contract for its own onboarding guided UX, not shared
|
|
7
7
|
* code, but the two are meant to agree on what "legacy" means and how a
|
|
8
8
|
* migration is carried out.
|
|
9
9
|
*
|
|
10
10
|
* NAMING, load-bearing: this module's identifiers say "legacy" because the
|
|
11
11
|
* engine migrates AWAY from the `goodvibes-daemon.service` unit name toward
|
|
12
|
-
* the runtime-managed unit
|
|
12
|
+
* the runtime-managed unit, that name was used by an older install script/
|
|
13
13
|
* release (scripts/install.sh now creates `goodvibes.service` and treats
|
|
14
14
|
* `goodvibes-daemon.service` as the retired name it migrates existing hosts
|
|
15
15
|
* away from; see migrate_legacy_installer_unit there). An already-installed
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* never labels it legacy or implies it should be removed unless the user is
|
|
19
19
|
* explicitly migrating.
|
|
20
20
|
*
|
|
21
|
-
* This lives under `src/runtime
|
|
21
|
+
* This lives under `src/runtime/`, not `src/daemon/`, because both
|
|
22
22
|
* `src/daemon/service-commands.ts` (the CLI subcommand) and
|
|
23
23
|
* `src/runtime/legacy-daemon-reconcile.ts` (the boot-time reconcile) need it,
|
|
24
24
|
* and `src/runtime/**` is this repository's shared layer both can import.
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
* - ADOPT-OR-WARN, NEVER KILL. If the legacy unit file is simply absent but
|
|
38
38
|
* something is already listening on the configured host:port (this dev
|
|
39
39
|
* host's real case: a manually `nohup`'d daemon with no unit at all),
|
|
40
|
-
* that is an unidentified process, not a managed unit
|
|
40
|
+
* that is an unidentified process, not a managed unit, nothing to stop
|
|
41
41
|
* or disable, and this module never attempts to kill it.
|
|
42
42
|
* - Every action (legacy stop/disable, unit-file removal, daemon-reload)
|
|
43
43
|
* goes through the injectable `actionRunner`/`legacyUnitFileRemove` seams
|
|
44
|
-
* tests use
|
|
44
|
+
* tests use, no code path here bypasses them, so the migration is
|
|
45
45
|
* exercised deterministically via fakes and never touches a real running
|
|
46
46
|
* service in tests.
|
|
47
47
|
*/
|
|
@@ -56,7 +56,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
56
56
|
import { runDaemonConfigMigration } from '../config/run-daemon-config-migration.ts';
|
|
57
57
|
import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../config/surface.ts';
|
|
58
58
|
|
|
59
|
-
/** Structurally derived from `PlatformServiceManager`'s own constructor
|
|
59
|
+
/** Structurally derived from `PlatformServiceManager`'s own constructor, the
|
|
60
60
|
* SDK's public `platform/daemon` entry point only re-exports the class and
|
|
61
61
|
* `ManagedServiceStatus`, not the options/definition/action-runner interfaces
|
|
62
62
|
* by name, so we pull their shapes off the class itself rather than reaching
|
|
@@ -66,7 +66,7 @@ type ManagedServiceDefinition = NonNullable<ManagedServiceManagerOptions['defini
|
|
|
66
66
|
export type ManagedServiceActionRunner = NonNullable<ManagedServiceManagerOptions['actionRunner']>;
|
|
67
67
|
type ManagedServiceActionResult = ReturnType<ManagedServiceActionRunner>;
|
|
68
68
|
|
|
69
|
-
// The one unit name/description this tool manages
|
|
69
|
+
// The one unit name/description this tool manages, used by the daemon CLI
|
|
70
70
|
// (`goodvibes-daemon install-service|uninstall-service|service-status|migrate-service`).
|
|
71
71
|
// The terminal app's own onboarding UX builds the exact same service
|
|
72
72
|
// definition independently, so a migration triggered from either surface
|
|
@@ -80,7 +80,7 @@ export const MANAGED_SERVICE_DESCRIPTION = 'GoodVibes daemon (shared session bro
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Follow-up: resolve the unit name the SDK's `PlatformServiceManager`
|
|
83
|
-
* would actually manage, from config alone
|
|
83
|
+
* would actually manage, from config alone, for callers that need the
|
|
84
84
|
* honest display name BEFORE any manager/status exists (the onboarding
|
|
85
85
|
* wizard's detection banner resolves this at snapshot-collection time and
|
|
86
86
|
* carries it on `OnboardingLegacyDaemonSnapshot.trackedServiceName`).
|
|
@@ -101,17 +101,17 @@ export function resolveConfiguredServiceName(config: { get(key: string): unknown
|
|
|
101
101
|
export interface BuildManagedDaemonServiceManagerParams {
|
|
102
102
|
readonly binaryPath: string;
|
|
103
103
|
/**
|
|
104
|
-
* The GoodVibes tree home
|
|
104
|
+
* The GoodVibes tree home, GOODVIBES_HOME-overridable, used to root the
|
|
105
105
|
* ConfigManager and the daemon's own `--daemon-home` state directory. NEVER
|
|
106
106
|
* used for unit-file path resolution: see `unitHomeDir` for that. A unit
|
|
107
107
|
* path search rooted here would look for `~/.config/systemd/user/` under
|
|
108
108
|
* whatever GOODVIBES_HOME points at instead of the real login home systemd
|
|
109
|
-
* actually reads
|
|
109
|
+
* actually reads, the same class of bug the boot-time reconcile in
|
|
110
110
|
* `src/daemon/cli.ts` already guards against with the identical split.
|
|
111
111
|
*/
|
|
112
112
|
readonly homeDir: string;
|
|
113
113
|
/**
|
|
114
|
-
* The LOGIN user's home
|
|
114
|
+
* The LOGIN user's home, where `~/.config/systemd/user/` (or the launchd/
|
|
115
115
|
* Windows equivalent) actually lives, regardless of any GOODVIBES_HOME/
|
|
116
116
|
* GOODVIBES_DAEMON_HOME override in effect. Threaded through to
|
|
117
117
|
* `PlatformServiceManager`'s own `homeDirectory` option, which resolves the
|
|
@@ -122,7 +122,7 @@ export interface BuildManagedDaemonServiceManagerParams {
|
|
|
122
122
|
readonly unitHomeDir: string;
|
|
123
123
|
readonly host: string;
|
|
124
124
|
readonly port: number;
|
|
125
|
-
/** Defaults to `homeDir
|
|
125
|
+
/** Defaults to `homeDir`, overridable so tests can scope both to one tempdir. */
|
|
126
126
|
readonly workingDirectory?: string | undefined;
|
|
127
127
|
/** Injected in tests; a real `ConfigManager` rooted at `homeDir` otherwise. */
|
|
128
128
|
readonly configManager?: ConfigManager | undefined;
|
|
@@ -131,12 +131,12 @@ export interface BuildManagedDaemonServiceManagerParams {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* Build the ONE `PlatformServiceManager` this tool manages
|
|
134
|
+
* Build the ONE `PlatformServiceManager` this tool manages, the single
|
|
135
135
|
* source of truth, in this repository, for the unit's definition
|
|
136
136
|
* (`ExecStart` command/args, name, description). Both
|
|
137
137
|
* `src/daemon/service-commands.ts` (the CLI) and this daemon's own
|
|
138
138
|
* boot-time reconcile call this so every path in this repository installs
|
|
139
|
-
* the identical unit
|
|
139
|
+
* the identical unit, no risk of consumers drifting apart.
|
|
140
140
|
*/
|
|
141
141
|
export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServiceManagerParams): PlatformServiceManager {
|
|
142
142
|
const workingDirectory = params.workingDirectory ?? params.homeDir;
|
|
@@ -152,7 +152,7 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
|
|
|
152
152
|
// already configured for hostMode=network / a non-default port) keeps its
|
|
153
153
|
// endpoint without a unit rewrite. Baking endpoint values here is what
|
|
154
154
|
// silently re-pinned custom-configured hosts back to the values current at
|
|
155
|
-
// install time. scripts/install.sh writes the same shape
|
|
155
|
+
// install time. scripts/install.sh writes the same shape, the two paths
|
|
156
156
|
// must produce the identical running daemon (see the installer parity test).
|
|
157
157
|
// `params.host`/`params.port` remain inputs because the migration engine
|
|
158
158
|
// still needs them for its read-only port-liveness probe.
|
|
@@ -161,15 +161,15 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
|
|
|
161
161
|
description: MANAGED_SERVICE_DESCRIPTION,
|
|
162
162
|
workingDirectory,
|
|
163
163
|
command: params.binaryPath,
|
|
164
|
-
// `--daemon-home` names the daemon's own STATE directory
|
|
165
|
-
// operator-tokens.json, auth-users.json and daemon-settings.json
|
|
164
|
+
// `--daemon-home` names the daemon's own STATE directory, the one holding
|
|
165
|
+
// operator-tokens.json, auth-users.json and daemon-settings.json, which is
|
|
166
166
|
// `<home>/.goodvibes/daemon`. This baked the USER HOME, so a serviced
|
|
167
167
|
// daemon filed its identity a level above where every reader in this
|
|
168
168
|
// repository looks: the SDK's platform/config goodvibes-home resolves the flag AS the state
|
|
169
169
|
// directory, cli/service-posture.ts already writes the state directory into
|
|
170
170
|
// GOODVIBES_DAEMON_HOME for the unit it installs, and runtime/bootstrap.ts
|
|
171
171
|
// reads the companion token from the state directory. On a normal machine
|
|
172
|
-
// the mismatch is invisible from the outside
|
|
172
|
+
// the mismatch is invisible from the outside, the daemon simply mints a
|
|
173
173
|
// second operator-tokens.json in the home directory and the client keeps
|
|
174
174
|
// reading the empty one under .goodvibes/daemon.
|
|
175
175
|
args: ['--daemon-home', join(params.homeDir, '.goodvibes', 'daemon')],
|
|
@@ -179,7 +179,7 @@ export function buildManagedDaemonServiceManager(params: BuildManagedDaemonServi
|
|
|
179
179
|
return new PlatformServiceManager(configManager, {
|
|
180
180
|
workingDirectory,
|
|
181
181
|
// Unit paths resolve from the LOGIN home, never the (possibly
|
|
182
|
-
// GOODVIBES_HOME-relocated) tree home above
|
|
182
|
+
// GOODVIBES_HOME-relocated) tree home above, see `unitHomeDir`'s doc.
|
|
183
183
|
homeDirectory: params.unitHomeDir,
|
|
184
184
|
definitionOverride: definition,
|
|
185
185
|
defaultServiceName: MANAGED_SERVICE_NAME,
|
|
@@ -250,7 +250,7 @@ export function legacyUnitPath(homeDir: string): string {
|
|
|
250
250
|
|
|
251
251
|
export interface DetectLegacyUnitInput {
|
|
252
252
|
/**
|
|
253
|
-
* The LOGIN user's home
|
|
253
|
+
* The LOGIN user's home, where the legacy unit file would actually live
|
|
254
254
|
* (`~/.config/systemd/user/goodvibes-daemon.service`), never the
|
|
255
255
|
* GOODVIBES_HOME-overridable tree home. See
|
|
256
256
|
* `BuildManagedDaemonServiceManagerParams.unitHomeDir` for the identical
|
|
@@ -266,7 +266,7 @@ export interface DetectLegacyUnitInput {
|
|
|
266
266
|
/**
|
|
267
267
|
* Read-only detection: does a legacy `goodvibes-daemon.service` unit file
|
|
268
268
|
* exist, and if so, is it currently active? Never stops, disables, or
|
|
269
|
-
* modifies anything
|
|
269
|
+
* modifies anything, a file-existence check plus a read-only
|
|
270
270
|
* `systemctl --user is-active` query through the injected actionRunner.
|
|
271
271
|
*/
|
|
272
272
|
export function detectLegacyUnit(input: DetectLegacyUnitInput): LegacyUnitInfo {
|
|
@@ -283,8 +283,8 @@ export function detectLegacyUnit(input: DetectLegacyUnitInput): LegacyUnitInfo {
|
|
|
283
283
|
/**
|
|
284
284
|
* The unit name `PlatformServiceManager` is ACTUALLY about to mutate can
|
|
285
285
|
* differ from `MANAGED_SERVICE_NAME` / `definitionOverride.name`. The SDK's
|
|
286
|
-
* internal `resolveServiceName()
|
|
287
|
-
* `status()` alike to compute the unit file PATH
|
|
286
|
+
* internal `resolveServiceName()`, used by `install()`, `uninstall()`, and
|
|
287
|
+
* `status()` alike to compute the unit file PATH, resolves from the
|
|
288
288
|
* `service.serviceName` CONFIG key first, falling back to the
|
|
289
289
|
* `defaultServiceName` this module passes only when that key is unset. It
|
|
290
290
|
* never consults `definitionOverride.name` for the path. So if a host's
|
|
@@ -313,11 +313,11 @@ export function resolveManagedUnitName(status: ManagedServiceStatus): string {
|
|
|
313
313
|
return basename(status.path).replace(/\.(service|plist)$/, '');
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
/** Honest one-line disclosure of the install-script unit's presence/state plus a manual migration hint
|
|
316
|
+
/** Honest one-line disclosure of the install-script unit's presence/state plus a manual migration hint, never auto-acted-on. */
|
|
317
317
|
export function legacyUnitNote(legacy: LegacyUnitInfo, trackedServiceName: string): string {
|
|
318
318
|
const stateWord = legacy.active ? 'installed and RUNNING' : 'installed (not currently active)';
|
|
319
319
|
return (
|
|
320
|
-
`note: a separate service named ${LEGACY_SERVICE_UNIT_NAME}.service is ${stateWord} at ${legacy.path}
|
|
320
|
+
`note: a separate service named ${LEGACY_SERVICE_UNIT_NAME}.service is ${stateWord} at ${legacy.path}; ` +
|
|
321
321
|
`that unit name was used by an older install script/release, while this tool manages ` +
|
|
322
322
|
`${trackedServiceName}.service and will not touch the other unit automatically. Keep whichever one you use; running ` +
|
|
323
323
|
`both would start two daemons competing for the same port. To retire the install-script unit in favor of this ` +
|
|
@@ -329,7 +329,7 @@ export function legacyUnitNote(legacy: LegacyUnitInfo, trackedServiceName: strin
|
|
|
329
329
|
* Hard ceiling on every systemctl invocation made through a DEFAULT action
|
|
330
330
|
* runner in this module. The reconcile below runs on the daemon's own startup
|
|
331
331
|
* path, and `spawnSync` without a timeout blocks the single JS event loop for
|
|
332
|
-
* as long as the child runs
|
|
332
|
+
* as long as the child runs, a wedged user D-Bus (a real incident class on
|
|
333
333
|
* this host) would freeze an already-listening daemon indefinitely. A timed-out
|
|
334
334
|
* call reports `status: null`, which every status check in this module treats
|
|
335
335
|
* as failure, so a wedge degrades to an honest refusal instead of a hang.
|
|
@@ -353,7 +353,7 @@ export function parseMainPid(result: { status?: number | null; stdout?: string |
|
|
|
353
353
|
* Read-only, best-effort TCP connect probe used ONLY by the legacy-absent
|
|
354
354
|
* branch to tell "nothing is listening on this port" apart from "an
|
|
355
355
|
* unmanaged process (e.g. a manual `nohup`) already owns it." Never used to
|
|
356
|
-
* identify or act on that process
|
|
356
|
+
* identify or act on that process, a positive result only produces a
|
|
357
357
|
* warning, never a kill. Tests always inject a fake `portProbe`; this default
|
|
358
358
|
* is never exercised against a real host in this repo's test suite.
|
|
359
359
|
*/
|
|
@@ -376,11 +376,11 @@ export function defaultPortProbe(host: string, port: number, timeoutMs = 750): P
|
|
|
376
376
|
export interface RunLegacyDaemonMigrationParams {
|
|
377
377
|
readonly host: string;
|
|
378
378
|
readonly port: number;
|
|
379
|
-
/** The unit name this tool manages (e.g. 'goodvibes')
|
|
379
|
+
/** The unit name this tool manages (e.g. 'goodvibes'), distinct from LEGACY_SERVICE_UNIT_NAME. */
|
|
380
380
|
readonly trackedServiceName: string;
|
|
381
381
|
/**
|
|
382
382
|
* Explicit consent to actually execute the migration. Without it, the
|
|
383
|
-
* result is a printed plan only
|
|
383
|
+
* result is a printed plan only, never auto-migrate.
|
|
384
384
|
*/
|
|
385
385
|
readonly confirmMigration?: boolean | undefined;
|
|
386
386
|
/** Injectable port-liveness check for the legacy-absent branch. Defaults to `defaultPortProbe`. */
|
|
@@ -396,7 +396,7 @@ export interface RunLegacyDaemonMigrationParams {
|
|
|
396
396
|
* legacy unit. Called immediately before the two mutation calls
|
|
397
397
|
* (`manager.install()`, and `manager.uninstall()` on the failed-health
|
|
398
398
|
* rollback path) that would otherwise write to or remove that path. This is
|
|
399
|
-
* an internal invariant check, not a normal user-facing error path
|
|
399
|
+
* an internal invariant check, not a normal user-facing error path, the
|
|
400
400
|
* pre-flight collision check in `runLegacyDaemonMigration` already returns
|
|
401
401
|
* before either call site is reached whenever this would trip, so tripping
|
|
402
402
|
* here means that earlier check regressed, not that the user did anything
|
|
@@ -406,7 +406,7 @@ function assertUnitIsNotLegacy(status: ManagedServiceStatus, legacy: LegacyUnitI
|
|
|
406
406
|
if (status.path === legacy.path || resolveManagedUnitName(status) === LEGACY_SERVICE_UNIT_NAME) {
|
|
407
407
|
throw new Error(
|
|
408
408
|
`refusing to ${action}: the resolved managed unit (${resolveManagedUnitName(status)} at ${status.path}) is the ` +
|
|
409
|
-
`install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit
|
|
409
|
+
`install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit; this should already have been caught by the pre-flight ` +
|
|
410
410
|
'collision check in runLegacyDaemonMigration',
|
|
411
411
|
);
|
|
412
412
|
}
|
|
@@ -431,7 +431,7 @@ export async function runLegacyDaemonMigration(
|
|
|
431
431
|
): Promise<LegacyDaemonMigrationResult> {
|
|
432
432
|
const { trackedServiceName } = params;
|
|
433
433
|
// Computed once, up front, and reused for every branch below (this is the
|
|
434
|
-
// exact same single call each branch made individually before
|
|
434
|
+
// exact same single call each branch made individually before, see the
|
|
435
435
|
// fix note on `resolveManagedUnitName` for why the name/path it reports
|
|
436
436
|
// can differ from `trackedServiceName`).
|
|
437
437
|
const currentStatus = manager.status();
|
|
@@ -448,9 +448,9 @@ export async function runLegacyDaemonMigration(
|
|
|
448
448
|
`migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found, but something is already ` +
|
|
449
449
|
`listening on ${params.host}:${params.port}.`,
|
|
450
450
|
"That looks like a process this tool doesn't manage (for example, a manually-started `nohup` daemon) rather " +
|
|
451
|
-
'than a systemd unit
|
|
451
|
+
'than a systemd unit; there is nothing here to stop or disable, and this tool will not attempt to kill an ' +
|
|
452
452
|
'unrecognized process.',
|
|
453
|
-
'Stop that process yourself, then re-run migrate-service or install-service once the port is free
|
|
453
|
+
'Stop that process yourself, then re-run migrate-service or install-service once the port is free; or, if ' +
|
|
454
454
|
"it's already the daemon you want running, leave it alone: a client surface can still reach it at this " +
|
|
455
455
|
"host:port directly, with no service unit required for this tool to manage.",
|
|
456
456
|
],
|
|
@@ -462,7 +462,7 @@ export async function runLegacyDaemonMigration(
|
|
|
462
462
|
exitCode: 0,
|
|
463
463
|
lines: [
|
|
464
464
|
`migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found and ${params.host}:${params.port} ` +
|
|
465
|
-
'is free
|
|
465
|
+
'is free; there is nothing to migrate.',
|
|
466
466
|
`Run install-service to set up the managed ${resolvedUnitName}.service directly.`,
|
|
467
467
|
],
|
|
468
468
|
status: currentStatus,
|
|
@@ -476,7 +476,7 @@ export async function runLegacyDaemonMigration(
|
|
|
476
476
|
lines: [
|
|
477
477
|
`migrate-service: this host's detected service platform is '${currentStatus.platform}', not systemd, but a ` +
|
|
478
478
|
`unit file with the install-script name exists at ${legacy.path}.`,
|
|
479
|
-
'That unit is systemd-specific and this tool only knows how to migrate a systemd unit today
|
|
479
|
+
'That unit is systemd-specific and this tool only knows how to migrate a systemd unit today; ' +
|
|
480
480
|
'nothing was changed.',
|
|
481
481
|
],
|
|
482
482
|
status: currentStatus,
|
|
@@ -486,7 +486,7 @@ export async function runLegacyDaemonMigration(
|
|
|
486
486
|
// Before any mutation, confirm the unit PlatformServiceManager is
|
|
487
487
|
// actually about to install/uninstall isn't the legacy unit itself. This
|
|
488
488
|
// happens when the host's `service.serviceName` config key is set to the
|
|
489
|
-
// legacy unit's own name
|
|
489
|
+
// legacy unit's own name, see `resolveManagedUnitName`'s doc comment for
|
|
490
490
|
// why the SDK resolves mutation paths from that config key rather than
|
|
491
491
|
// from the definition this engine passes. Without this check, `install()`
|
|
492
492
|
// below would overwrite the legacy unit file, a failed-health rollback
|
|
@@ -516,12 +516,12 @@ export async function runLegacyDaemonMigration(
|
|
|
516
516
|
exitCode: 0,
|
|
517
517
|
lines: [
|
|
518
518
|
legacyUnitNote(legacy, resolvedUnitName),
|
|
519
|
-
'migrate-service (dry run
|
|
519
|
+
'migrate-service (dry run, re-run with confirmation to execute): this would',
|
|
520
520
|
` 1. install and start the new ${resolvedUnitName}.service unit`,
|
|
521
521
|
' 2. verify it comes up healthy (a fresh, honest systemd is-active check)',
|
|
522
522
|
` 3. only if that succeeds, stop, disable, and remove the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit ` +
|
|
523
523
|
'and run `systemctl --user daemon-reload`',
|
|
524
|
-
'Nothing has been changed. Nothing is migrated automatically
|
|
524
|
+
'Nothing has been changed. Nothing is migrated automatically; re-run with explicit confirmation ' +
|
|
525
525
|
"(the CLI's -y/--yes flag) to execute this plan.",
|
|
526
526
|
],
|
|
527
527
|
status: currentStatus,
|
|
@@ -531,7 +531,7 @@ export async function runLegacyDaemonMigration(
|
|
|
531
531
|
// Consented: new-up-then-old-down. The legacy unit is not touched until the
|
|
532
532
|
// new unit is verified healthy.
|
|
533
533
|
// Belt-and-braces: the collision check above already returns before
|
|
534
|
-
// reaching here whenever the resolved unit is the legacy one
|
|
534
|
+
// reaching here whenever the resolved unit is the legacy one, this
|
|
535
535
|
// re-asserts the same invariant right at the mutation site so a future
|
|
536
536
|
// change to the check above can never silently reopen the hole.
|
|
537
537
|
assertUnitIsNotLegacy(currentStatus, legacy, 'install the new unit');
|
|
@@ -554,7 +554,7 @@ export async function runLegacyDaemonMigration(
|
|
|
554
554
|
assertUnitIsNotLegacy(installed, legacy, 'roll back (uninstall) the new unit');
|
|
555
555
|
const rollback = manager.uninstall();
|
|
556
556
|
const rollbackNote = rollback.actionError
|
|
557
|
-
? `rolling back the new unit ALSO hit an error (${rollback.actionError})
|
|
557
|
+
? `rolling back the new unit ALSO hit an error (${rollback.actionError}); remove ${installed.path} by hand.`
|
|
558
558
|
: 'the newly-written unit has been rolled back (removed).';
|
|
559
559
|
return {
|
|
560
560
|
ok: false,
|
|
@@ -569,7 +569,7 @@ export async function runLegacyDaemonMigration(
|
|
|
569
569
|
};
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
-
// New unit verified healthy
|
|
572
|
+
// New unit verified healthy, now, and only now, retire the legacy unit.
|
|
573
573
|
const run: ManagedServiceActionRunner = params.actionRunner ?? defaultActionRunner(SYSTEMCTL_TIMEOUT_MS);
|
|
574
574
|
const stopResult = run('systemctl', ['--user', 'stop', `${LEGACY_SERVICE_UNIT_NAME}.service`]);
|
|
575
575
|
const disableResult = run('systemctl', ['--user', 'disable', `${LEGACY_SERVICE_UNIT_NAME}.service`]);
|
|
@@ -596,7 +596,7 @@ export async function runLegacyDaemonMigration(
|
|
|
596
596
|
);
|
|
597
597
|
}
|
|
598
598
|
if (removeError) {
|
|
599
|
-
lines.push(`note: could not remove the install-script unit file at ${legacy.path}: ${removeError}
|
|
599
|
+
lines.push(`note: could not remove the install-script unit file at ${legacy.path}: ${removeError}; remove it by hand.`);
|
|
600
600
|
} else {
|
|
601
601
|
lines.push(`the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit has been stopped, disabled, and removed.`);
|
|
602
602
|
}
|