@pellux/goodvibes-daemon 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/bin/goodvibes-daemon +100 -0
  5. package/bin/launcher-support.js +226 -0
  6. package/package.json +96 -0
  7. package/scripts/check-bun.sh +20 -0
  8. package/scripts/postinstall.js +244 -0
  9. package/src/cli/command-catalog.ts +828 -0
  10. package/src/cli/completion.ts +299 -0
  11. package/src/cli/help.ts +167 -0
  12. package/src/cli/index.ts +21 -0
  13. package/src/cli/parser.ts +55 -0
  14. package/src/cli/surface-catalog.ts +26 -0
  15. package/src/cli/types.ts +63 -0
  16. package/src/cluster/daemon-ws-call.ts +235 -0
  17. package/src/cluster/raw-reply-route.ts +111 -0
  18. package/src/config/checkpoint-settings.ts +113 -0
  19. package/src/config/run-daemon-config-migration.ts +47 -0
  20. package/src/config/secret-config.ts +175 -0
  21. package/src/config/secrets.ts +71 -0
  22. package/src/config/surface.ts +24 -0
  23. package/src/core/pairing-banner.ts +82 -0
  24. package/src/daemon/cli.ts +878 -0
  25. package/src/daemon/config-command.ts +281 -0
  26. package/src/daemon/handlers/context.ts +29 -0
  27. package/src/daemon/handlers/contracts.ts +43 -0
  28. package/src/daemon/handlers/credentials.ts +139 -0
  29. package/src/daemon/handlers/drafts/draft-store.ts +427 -0
  30. package/src/daemon/handlers/drafts/index.ts +17 -0
  31. package/src/daemon/handlers/drafts/register.ts +331 -0
  32. package/src/daemon/handlers/errors.ts +18 -0
  33. package/src/daemon/handlers/inbox/aggregator.ts +375 -0
  34. package/src/daemon/handlers/inbox/cursor-store.ts +512 -0
  35. package/src/daemon/handlers/inbox/index.ts +221 -0
  36. package/src/daemon/handlers/inbox/mapping.ts +192 -0
  37. package/src/daemon/handlers/inbox/poller.ts +239 -0
  38. package/src/daemon/handlers/inbox/provider-adapter.ts +171 -0
  39. package/src/daemon/handlers/inbox/providers/discord.ts +276 -0
  40. package/src/daemon/handlers/inbox/providers/email.ts +176 -0
  41. package/src/daemon/handlers/inbox/providers/imap-client.ts +300 -0
  42. package/src/daemon/handlers/inbox/providers/route-util.ts +24 -0
  43. package/src/daemon/handlers/inbox/providers/slack.ts +287 -0
  44. package/src/daemon/handlers/index.ts +117 -0
  45. package/src/daemon/handlers/register.ts +180 -0
  46. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +143 -0
  47. package/src/daemon/handlers/remote/backends/docker.ts +79 -0
  48. package/src/daemon/handlers/remote/backends/index.ts +40 -0
  49. package/src/daemon/handlers/remote/backends/local-process.ts +113 -0
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +127 -0
  51. package/src/daemon/handlers/remote/backends/ssh.ts +126 -0
  52. package/src/daemon/handlers/remote/backends/types.ts +97 -0
  53. package/src/daemon/handlers/remote/dispatcher.ts +181 -0
  54. package/src/daemon/handlers/remote/index.ts +120 -0
  55. package/src/daemon/handlers/remote/peer-registry.ts +357 -0
  56. package/src/daemon/handlers/remote/service.ts +191 -0
  57. package/src/daemon/handlers/routing/inbox-bridge.ts +71 -0
  58. package/src/daemon/handlers/routing/index.ts +261 -0
  59. package/src/daemon/handlers/routing/route-store.ts +319 -0
  60. package/src/daemon/handlers/routing/routing-resolver.ts +75 -0
  61. package/src/daemon/handlers/sqlite-store.ts +303 -0
  62. package/src/daemon/handlers/triage/index.ts +57 -0
  63. package/src/daemon/handlers/triage/integration.ts +213 -0
  64. package/src/daemon/handlers/triage/pipeline.ts +274 -0
  65. package/src/daemon/handlers/triage/scorer.ts +287 -0
  66. package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
  67. package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
  68. package/src/daemon/handlers/triage/tagger/index.ts +184 -0
  69. package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
  70. package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
  71. package/src/daemon/handlers/triage/types.ts +50 -0
  72. package/src/daemon/lifecycle.ts +41 -0
  73. package/src/daemon/local-daemon-state.ts +233 -0
  74. package/src/daemon/pair-command.ts +301 -0
  75. package/src/daemon/provision-wake-model.ts +81 -0
  76. package/src/daemon/send/channels.ts +200 -0
  77. package/src/daemon/send/command.ts +333 -0
  78. package/src/daemon/send/composition.ts +100 -0
  79. package/src/daemon/send/failure-text.ts +93 -0
  80. package/src/daemon/send/inert-text.ts +225 -0
  81. package/src/daemon/send/stdin.ts +24 -0
  82. package/src/daemon/service-commands.ts +530 -0
  83. package/src/daemon/sessions-command.ts +209 -0
  84. package/src/daemon/status-command.ts +481 -0
  85. package/src/daemon/webui-command.ts +339 -0
  86. package/src/runtime/boot-tasks.ts +110 -0
  87. package/src/runtime/cluster-composition.ts +124 -0
  88. package/src/runtime/cluster-group-composition.ts +284 -0
  89. package/src/runtime/conversation-rewind-port.ts +171 -0
  90. package/src/runtime/credential-composition.ts +54 -0
  91. package/src/runtime/daemon-handler-composition.ts +76 -0
  92. package/src/runtime/device-posture-composition.ts +115 -0
  93. package/src/runtime/disposal-wiring.ts +101 -0
  94. package/src/runtime/fleet-needs-input-push.ts +61 -0
  95. package/src/runtime/fleet-services.ts +41 -0
  96. package/src/runtime/hosted-session-composition.ts +128 -0
  97. package/src/runtime/index.ts +100 -0
  98. package/src/runtime/knowledge-services.ts +101 -0
  99. package/src/runtime/legacy-daemon-migration.ts +605 -0
  100. package/src/runtime/legacy-daemon-reconcile.ts +448 -0
  101. package/src/runtime/mail-composition.ts +65 -0
  102. package/src/runtime/notification-dispatch.ts +86 -0
  103. package/src/runtime/plugin-composition.ts +111 -0
  104. package/src/runtime/runtime-services-types.ts +268 -0
  105. package/src/runtime/services.ts +756 -0
  106. package/src/runtime/trigger-services.ts +62 -0
  107. package/src/runtime/trust/checkpoint-eligibility.ts +138 -0
  108. package/src/runtime/trust/trust-gated-approvals.ts +169 -0
  109. package/src/runtime/update-check.ts +61 -0
  110. package/src/runtime/workspace-checkpointing.ts +116 -0
  111. package/src/testing/daemon-fixture.ts +276 -0
  112. package/src/testing/hosted-session-failures.ts +92 -0
  113. package/src/version.ts +26 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * device-posture-composition.ts — the paired-phone feature inside THIS daemon.
3
+ *
4
+ * A phone pairs with whichever daemon the person runs. Until this module
5
+ * existed this daemon had no device posture at all: `device.nodes.maxPaired`
6
+ * was enforced at the pairing path (SDK-side) and `device.capabilities.mode`
7
+ * was an onboarding toggle, while the other eleven `device.*` keys were
8
+ * recorded, read back, and governed nothing — the capability service they
9
+ * describe was never built here, so there was nothing for them to govern.
10
+ *
11
+ * The feature itself is platform-owned (`platform/devices`): the settings→policy
12
+ * mapping, the grants ledger, the capture store, the housekeeping sweeps, the
13
+ * confirmation flow, and the `phone` tool. This module supplies the three seams
14
+ * that are actually ours and nothing else:
15
+ *
16
+ * - the peer transport — the same DistributedRuntimeManager the remote surface
17
+ * pairs devices onto, so a phone paired here is a node here,
18
+ * - the approval path — the shared approval broker, so the confirmation
19
+ * appears wherever the person is looking (terminal, web app, companion),
20
+ * - the storage root and the live config manager.
21
+ *
22
+ * Constructing this starts nothing. `startHousekeeping()` is called from the
23
+ * bootstrap tail: grants and captures both outlive a restart, so a grant whose
24
+ * phone is gone, or a capture torn by a crash, is reaped BEFORE the first
25
+ * request of this run is served — and the periodic sweep after it is what keeps
26
+ * a long-running daemon from going days without one. A failed sweep is logged
27
+ * and the app still runs: housekeeping failing is a reason to say so, not a
28
+ * reason to refuse to start.
29
+ */
30
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
31
+ import { createDevicePostureRuntime, registerDevicePhoneTool } from '@pellux/goodvibes-sdk/platform/devices';
32
+ import type {
33
+ DeviceApprovalBridge,
34
+ DevicePeerTransport,
35
+ DevicePhoneToolRegistry,
36
+ DevicePostureRuntime,
37
+ } from '@pellux/goodvibes-sdk/platform/devices';
38
+ import { registerDevicesGatewayMethods } from '@pellux/goodvibes-sdk/platform/control-plane';
39
+ import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
40
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
41
+
42
+ /** Who this surface records in the device audit trail. */
43
+ export const DAEMON_DEVICE_ACTOR = 'daemon:phone-tool';
44
+
45
+ export interface DevicePostureCompositionOptions {
46
+ readonly configManager: ConfigManager;
47
+ /** The runtime devices pair onto; its listPeers/invokePeer are the transport. */
48
+ readonly distributedRuntime: DevicePeerTransport;
49
+ readonly approvals: DeviceApprovalBridge;
50
+ /** Surface-scoped directory the grants ledger, captures and disclosure live in. */
51
+ readonly stateDirectory: string;
52
+ /**
53
+ * Binding the catalog turns the whole devices.* family from
54
+ * cataloged-but-unhandled into real handlers: nodes.list, grants.*,
55
+ * housekeeping.run, and — since a surface with no device runtime of its own
56
+ * could read the grants and never open a camera — capability.request and
57
+ * artifacts.list/read. That is what makes the paired phone usable from the web
58
+ * app and the companion rather than only through this process's own tool.
59
+ *
60
+ * The runtime is handed over whole because the verbs and the tool must reach
61
+ * the SAME service: a second path to a phone would be a second place the
62
+ * confirmation prompt and the durable grants could be decided differently.
63
+ */
64
+ readonly gatewayMethods?: GatewayMethodCatalog | undefined;
65
+ readonly getSessionId?: (() => string | undefined) | undefined;
66
+ }
67
+
68
+ export interface DevicePostureServices {
69
+ readonly devicePosture: DevicePostureRuntime;
70
+ }
71
+
72
+ /** Build the device posture runtime and bind the device verbs to it. */
73
+ export function createDevicePostureServices(options: DevicePostureCompositionOptions): DevicePostureServices {
74
+ const devicePosture = createDevicePostureRuntime({
75
+ transport: options.distributedRuntime,
76
+ approvals: options.approvals,
77
+ config: options.configManager,
78
+ stateDirectory: options.stateDirectory,
79
+ actor: DAEMON_DEVICE_ACTOR,
80
+ ...(options.getSessionId ? { getSessionId: options.getSessionId } : {}),
81
+ });
82
+ if (options.gatewayMethods) registerDevicesGatewayMethods(options.gatewayMethods, devicePosture);
83
+ return { devicePosture };
84
+ }
85
+
86
+ /**
87
+ * Everything a host with a tool registry has to do once it is up: register the
88
+ * `phone` tool — the only path that reaches the capability service, so without it
89
+ * the posture keys govern nothing a session can observe — and start housekeeping.
90
+ *
91
+ * One call, because these two belong to the same feature and a host that did the
92
+ * first and forgot the second would serve requests while never reaping a grant
93
+ * whose phone is gone.
94
+ */
95
+ export function installDevicePosture(
96
+ toolRegistry: DevicePhoneToolRegistry,
97
+ devicePosture: DevicePostureRuntime,
98
+ ): void {
99
+ registerDevicePhoneTool(toolRegistry, devicePosture);
100
+ startDeviceHousekeeping(devicePosture);
101
+ }
102
+
103
+ /**
104
+ * The recovery sweep plus the periodic timer. Separate from construction so
105
+ * composing a runtime in a test starts no timer and touches no disk, and
106
+ * separate from the call above so this daemon — which registers no tools —
107
+ * still sweeps.
108
+ */
109
+ export function startDeviceHousekeeping(devicePosture: DevicePostureRuntime): void {
110
+ void devicePosture.startHousekeeping().catch((error: unknown) => {
111
+ logger.warn('Device housekeeping sweep failed at startup', {
112
+ error: error instanceof Error ? error.message : String(error),
113
+ });
114
+ });
115
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * disposal-wiring.ts
3
+ *
4
+ * Teardown for every poller the daemon's runtime graph starts.
5
+ *
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
+ * the daemon and the SDK's own composition root cannot drift into two different
9
+ * ideas of what "stop the graph" means. What lives here is only the mapping from
10
+ * the daemon's assembled graph onto that list, plus the four pollers the daemon
11
+ * owns that the SDK's composition does not have at all: the crash-residue sweep,
12
+ * the device housekeeping sweep, the wake-model recovery sweep, and the product
13
+ * handler surfaces with their inbox timers. Calling the SDK's list alone would
14
+ * silently leave every one of them running.
15
+ *
16
+ * It is a separate module because `runtime/services.ts` sits against the repo's
17
+ * 800-line source cap (check-architecture.ts) with no room for the wiring.
18
+ *
19
+ * Ownership note, and the reason any of this exists: this product builds the
20
+ * graph and hands the SAME object to `DaemonServer`. The SDK facade disposes
21
+ * only a graph it constructed itself, so it deliberately leaves this one alone
22
+ * — nothing upstream will ever stop these pollers. The shutdown paths in
23
+ * daemon/cli.ts and the one-shot CLI commands are the only things that can.
24
+ *
25
+ * The owner type below is declared structurally rather than imported from
26
+ * services.ts: that module imports this one, and a type-only edge is still a
27
+ * cycle to the architecture check.
28
+ */
29
+
30
+ import {
31
+ createDisposalScope,
32
+ registerRuntimePollers,
33
+ type DisposalRegistry,
34
+ type RuntimePollerOwners,
35
+ } from '@pellux/goodvibes-sdk/platform/runtime/disposal';
36
+
37
+ /** Re-exported so the composition root reaches the whole seam through one import. */
38
+ export { createDisposalScope };
39
+
40
+ /** The assembled graph, narrowed to the poller owners it exposes as fields. */
41
+ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'stopConfigWatch'> {
42
+ /** Daemon-only: the repeating crash-residue sweep. */
43
+ readonly stopDurabilityHousekeeping: () => void;
44
+ /**
45
+ * Daemon-only: the wake-word recovery sweep and a pending boot provision
46
+ * Started only when an entrypoint opted into boot
47
+ * provisioning, and a no-op otherwise — but it is on this list unconditionally,
48
+ * because "the graph did not start it this time" is not a reason for the
49
+ * teardown path to have no way to stop it.
50
+ */
51
+ readonly stopWakeHousekeeping: () => void;
52
+ /**
53
+ * Daemon-only: the product handler surfaces (daemon-handler-composition.ts).
54
+ *
55
+ * `unregister()` detaches the gateway handlers AND stops the two pollers this
56
+ * product's inbox surface owns — the retention sweep inside `InboxCursorStore`
57
+ * and the per-provider `InboundPoller` intervals. The SDK does not know either
58
+ * exists, so if this surface does not stop them nothing does.
59
+ */
60
+ readonly daemonHandlers: { readonly unregister: () => void };
61
+ /**
62
+ * This daemon only: the paired-phone feature's housekeeping timer
63
+ * (device-posture-composition.ts). Started by whichever entry point boots the
64
+ * host, so the stop belongs on this list rather than in one shutdown path.
65
+ */
66
+ readonly devicePosture: { readonly stopHousekeeping: () => void };
67
+ }
68
+
69
+ /**
70
+ * The poller owners this daemon holds that are NOT reachable from the
71
+ * assembled graph — handles the factory keeps as locals.
72
+ */
73
+ export interface RuntimeDisposalExtras {
74
+ /** Handle returned by `ConfigManager.watchConfigFiles()`. */
75
+ readonly stopConfigWatch: () => void;
76
+ }
77
+
78
+ /**
79
+ * Register the stop call for every poller the graph started.
80
+ *
81
+ * `services` is the fully-assembled graph, which already exposes each poller
82
+ * owner as a field — so a poller whose owner reaches the public surface is
83
+ * wired by name rather than by threading another local out of the factory.
84
+ */
85
+ export function registerDaemonRuntimePollers(
86
+ registry: DisposalRegistry,
87
+ services: DaemonRuntimePollerOwners,
88
+ extras: RuntimeDisposalExtras,
89
+ ): void {
90
+ registerRuntimePollers(registry, { ...services, stopConfigWatch: extras.stopConfigWatch });
91
+ registry.add('durability housekeeping', services.stopDurabilityHousekeeping);
92
+ registry.add('device housekeeping', () => services.devicePosture.stopHousekeeping());
93
+ registry.add('wake-word housekeeping', services.stopWakeHousekeeping);
94
+ // Registered LAST so it tears down FIRST (the scope unwinds in reverse), which
95
+ // is the order daemon/cli.ts already used by hand: release the handler surfaces
96
+ // — closing the inbox store and stopping its poll timers — before the pollers
97
+ // the rest of the graph owns. Being on this list is what makes a plain
98
+ // `dispose()` total: every shutdown path stops these, not just the one that
99
+ // remembered to call `unregister()` itself.
100
+ registry.add('daemon handler surfaces', () => services.daemonHandlers.unregister());
101
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * fleet-needs-input-push.ts — wires the fleet "needs-input" browser-push fan-out
3
+ * that the SDK's registerGatewayVerbGroups (reached through the terminal-shell's
4
+ * attachWsOnlyGatewayVerbHandlers wrapper) gates on two deps: `runtimeBus` and
5
+ * `sessionPresence` (see push/service.ts's attachFleetNeedsInputSource). When a
6
+ * fleet node blocks on the operator, it pushes an "Input needed" notification
7
+ * carrying the session/node deep link to every registered device — suppressed
8
+ * while an operator surface is already attached to that node's session.
9
+ *
10
+ * TWO seams have to be live for a real push, not just descriptor presence:
11
+ * 1. Fleet lifecycle deltas (FLEET_NODE_BLOCKED_ON_USER etc.) have to reach the
12
+ * runtime bus's 'fleet' domain in the first place. That is
13
+ * attachFleetEmitBridge, diffing the process registry's own coalesced
14
+ * snapshot tick into per-node lifecycle events. The terminal-shell package's
15
+ * createArchivableFleetRegistry only builds the registry — it does not
16
+ * attach the bridge — so the composition root that owns both the registry
17
+ * and the bus (this file) is the one place responsible for it. The SDK's
18
+ * own daemon composition (platform/runtime/services.ts) attaches this same
19
+ * bridge right after building its process registry.
20
+ * 2. registerGatewayVerbGroups has to be handed `runtimeBus` (to subscribe onto
21
+ * that domain) and `sessionPresence` (to suppress a push when an operator is
22
+ * already looking at the session).
23
+ *
24
+ * sessionPresence is the SDK's own hasFreshSurfaceParticipant: a session counts
25
+ * as attended when any participant heartbeated within the SDK's freshness
26
+ * window. This is the one implementation, imported, so every surface's
27
+ * push/suppress behavior agrees.
28
+ */
29
+ import { attachFleetEmitBridge } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
30
+ import type { ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
31
+ import { hasFreshSurfaceParticipant, SURFACE_ROUTE_FRESHNESS_MS, type SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
32
+ import type { RuntimeEventBus } from '@/runtime/index.ts';
33
+
34
+ export interface FleetNeedsInputPushDeps {
35
+ readonly runtimeBus: RuntimeEventBus;
36
+ readonly sessionPresence: { isAttached(sessionId: string): boolean };
37
+ }
38
+
39
+ /**
40
+ * Attach the fleet emit-bridge (registry snapshot deltas -> runtime bus `fleet`
41
+ * domain) and build the presence lookup, returning both as the deps
42
+ * `attachWsOnlyGatewayVerbHandlers` spreads into its call so the needs-input
43
+ * push source registers for real instead of gracefully degrading to absent.
44
+ */
45
+ export function wireFleetNeedsInputPush(input: {
46
+ readonly registry: ArchivableProcessRegistry;
47
+ readonly runtimeBus: RuntimeEventBus;
48
+ readonly sessionBroker: Pick<SharedSessionBroker, 'getSession'>;
49
+ }): FleetNeedsInputPushDeps {
50
+ attachFleetEmitBridge({ registry: input.registry, bus: input.runtimeBus });
51
+ const sessionBroker = input.sessionBroker;
52
+ return {
53
+ runtimeBus: input.runtimeBus,
54
+ sessionPresence: {
55
+ isAttached: (sessionId: string) => {
56
+ const session = sessionBroker.getSession(sessionId);
57
+ return session !== null && hasFreshSurfaceParticipant(session, Date.now(), SURFACE_ROUTE_FRESHNESS_MS);
58
+ },
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * fleet-services.ts — the shared, archive-aware fleet registry construction,
3
+ * lifted out of services.ts so the composition root stays under the file-size
4
+ * cap. Mirrors the sibling create*Services helpers (durability, code-index,
5
+ * workstream): a single dependency-injected call that services.ts makes once.
6
+ *
7
+ * Also owns the daemon-side observed foreign-agent source: when
8
+ * observeExternalAgents is on (the standalone daemon), externally-launched
9
+ * coding-agent sessions observed read-only on the host fold in as
10
+ * 'observed-external' rows (visibility + steer; never counted against
11
+ * fleet.maxSize, never stopped). The interactive process leaves it off and
12
+ * reads the daemon's snapshot, so it never double-scans. Absence ⇒ a quiet
13
+ * empty set. Mirrors the SDK's own createRuntimeServices wiring.
14
+ */
15
+ import { computeUsageCostUsd, type ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
16
+ import { type ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
17
+ import { ObservedAgentSource } from '@pellux/goodvibes-sdk/platform/runtime/fleet/observed';
18
+ import { createArchivableFleetRegistry, type ProcessRegistryDeps } from '@pellux/goodvibes-terminal-shell';
19
+
20
+ export interface FleetServicesDeps
21
+ extends Omit<ProcessRegistryDeps, 'observedAgents' | 'priceUsage'> {
22
+ /** Turns on the daemon-side observed foreign-agent source (daemon only). */
23
+ readonly observeExternalAgents?: boolean | undefined;
24
+ /** Backs the honest pricing resolver folded into priceUsage below. */
25
+ readonly providerRegistry: Pick<ProviderRegistry, 'resolveModelPricing'>;
26
+ }
27
+
28
+ export function createFleetServices(
29
+ deps: FleetServicesDeps,
30
+ ): { processRegistry: ArchivableProcessRegistry } {
31
+ const { observeExternalAgents, providerRegistry, ...registryDeps } = deps;
32
+ const observedAgents = observeExternalAgents ? new ObservedAgentSource() : undefined;
33
+ const processRegistry = createArchivableFleetRegistry({
34
+ ...registryDeps,
35
+ observedAgents, // Daemon-side observed foreign-agent rows (undefined in the interactive process)
36
+ // Honest-unpriced through the ONE pricing resolver (manual -> registration -> provider-served
37
+ // -> catalog -> unknown); unknown/subscription yields null, never $0. Mirrors the SDK composition.
38
+ priceUsage: (model, usage) => (model ? computeUsageCostUsd(providerRegistry.resolveModelPricing(model), usage) : null),
39
+ });
40
+ return { processRegistry };
41
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * hosted-session-composition.ts — what this daemon states so it may host
3
+ * conversation loops.
4
+ *
5
+ * The engine is the SDK's (`@pellux/goodvibes-sdk/platform/hosted-sessions`):
6
+ * lifecycle, the detach policy, the bounded disk state, the verbs, the
7
+ * lifecycle channel. It is off until a product says how a workspace FLOOR is
8
+ * built, and that is deliberate — the floor's `requestApproval` seam is where a
9
+ * product's trust posture lives, and no default can stand in for a decision
10
+ * about who may write files and run commands in a directory a client named over
11
+ * the wire.
12
+ *
13
+ * This daemon's answer is the gate it already had: the workspace trust
14
+ * question is put in front of every ask a hosted run makes (trust/trust-gated-
15
+ * approvals.ts): an undecided workspace raises the question as an ordinary
16
+ * approval record for whichever surface is attached to answer, a trusted one is
17
+ * exactly as permissive as before, and a restricted one refuses non-read
18
+ * categories without asking. What this file adds is PER-WORKSPACE scope: the
19
+ * daemon's own gate reads `<its cwd>/.goodvibes/<surface>/trust.json`, and a
20
+ * session hosted in another directory has to be gated by that directory's
21
+ * decision, not by the daemon's.
22
+ *
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
+ * and model stack are the ones the platform already has, not a second set.
26
+ */
27
+ import { createShellPathService } from '@/runtime/index.ts';
28
+ import { createClientRuntimeServices } from '@pellux/goodvibes-sdk/platform/runtime/client-services';
29
+ import { createRuntimeStore } from '@pellux/goodvibes-sdk/platform/runtime/store';
30
+ import { createLaunchTolerantProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
31
+ import type { DaemonHostedSessionsOptions } from '@pellux/goodvibes-sdk/platform/daemon';
32
+ import type { HostedWorkspaceFloor } from '@pellux/goodvibes-sdk/platform/hosted-sessions';
33
+ import { operations } from '@pellux/goodvibes-sdk/platform/runtime';
34
+ const { WorkspaceTrustManager } = operations;
35
+ import { createWorkspaceTrustDecisionAsk, trustGatedApprovalRaiser, type ApprovalRaise } from './trust/trust-gated-approvals.ts';
36
+ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../config/surface.ts';
37
+ import type { RuntimeServices } from './runtime-services-types.ts';
38
+
39
+ /** The operator policy a session hosted by THIS daemon runs under. */
40
+ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
41
+ return [
42
+ 'You are a GoodVibes session hosted by the daemon rather than by a terminal.',
43
+ `Your working directory is ${input.workspaceRoot}.`,
44
+ 'Someone may be attached and watching this turn, or may have detached and read it later —',
45
+ 'write for both. Say what you did and why; never report work you did not do.',
46
+ 'Tool permissions are decided by whoever is attached: an ask you raise may take a while to be',
47
+ 'answered, and an unanswered one is a refusal, not a reason to find another way round.',
48
+ ].join(' ');
49
+ }
50
+
51
+ /**
52
+ * Build the hosted-session options this daemon passes to `DaemonServer`.
53
+ *
54
+ * `services` is the daemon's own graph: its config manager, event bus, secrets
55
+ * home and approval broker are shared with every floor, so a hosted session
56
+ * reads the same settings, publishes on the same bus and raises asks into the
57
+ * same broker every other surface is watching.
58
+ */
59
+ export function createHostedSessionOptions(services: RuntimeServices): DaemonHostedSessionsOptions {
60
+ /**
61
+ * One trust manager per workspace, cached: `load()` is idempotent but a
62
+ * manager per floor keeps the decision file and the in-memory answer for a
63
+ * workspace in one place, so two sessions in one directory cannot end up
64
+ * having been asked the trust question twice.
65
+ */
66
+ const trustByWorkspace = new Map<string, operations.WorkspaceTrustManager>();
67
+
68
+ const gateFor = (workspaceRoot: string): ApprovalRaise => {
69
+ const shellPaths = createShellPathService({
70
+ workingDirectory: workspaceRoot,
71
+ homeDirectory: services.homeDirectory,
72
+ });
73
+ let trust = trustByWorkspace.get(workspaceRoot);
74
+ if (!trust) {
75
+ trust = new WorkspaceTrustManager({ shellPaths, surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT });
76
+ trustByWorkspace.set(workspaceRoot, trust);
77
+ }
78
+ const raise: ApprovalRaise = (input) => services.approvalBroker.requestApproval(input);
79
+ return trustGatedApprovalRaiser(
80
+ trust,
81
+ raise,
82
+ // The question names the workspace it is about, which is the whole point
83
+ // of scoping it here: a daemon hosting three sessions in three
84
+ // directories asks three separate questions, each answerable on its own.
85
+ createWorkspaceTrustDecisionAsk({ requestApproval: raise, workingDirectory: workspaceRoot }),
86
+ );
87
+ };
88
+
89
+ return {
90
+ floorFactory: ({ workspaceRoot }): HostedWorkspaceFloor => {
91
+ const floor = createClientRuntimeServices({
92
+ configManager: services.configManager,
93
+ // The daemon's own bus: a hosted turn's stream events reach the
94
+ // control-plane SSE subscribers through the same path every other
95
+ // runtime event takes, stamped with the hosted session's id.
96
+ runtimeBus: services.runtimeBus,
97
+ // A store of its own. The daemon's runtime store describes the daemon's
98
+ // single local runtime, and a hosted session is not that runtime.
99
+ runtimeStore: createRuntimeStore(),
100
+ featureFlags: services.featureFlags,
101
+ surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT,
102
+ workingDir: workspaceRoot,
103
+ homeDirectory: services.homeDirectory,
104
+ requestApproval: gateFor(workspaceRoot),
105
+ // A workspace with broken or absent provider credentials must degrade,
106
+ // not take the daemon down on a create call.
107
+ providerRegistryFactory: createLaunchTolerantProviderRegistry,
108
+ });
109
+ // The machine's own local models. The daemon's registry learned them at
110
+ // boot (the persisted discovery cache) and from the LAN scan; a floor
111
+ // builds its own registry and would otherwise be the only place on this
112
+ // box where they are not routable. Servers found by a scan that finishes
113
+ // AFTER a floor is built reach the next floor, not this one — stated
114
+ // rather than hidden, because a wrong claim here would look like a model
115
+ // that exists everywhere except in hosted sessions.
116
+ const discovered = services.providerRegistry.listDiscoveredServers();
117
+ if (discovered.length > 0) floor.providerRegistry.registerDiscoveredProviders([...discovered]);
118
+ return {
119
+ services: floor,
120
+ // The daemon has a real review-chain controller; a hosted session's
121
+ // orchestrator lists through it rather than reporting none.
122
+ wrfcController: services.wrfcController,
123
+ dispose: (): void => floor.dispose(),
124
+ };
125
+ },
126
+ systemPrompt: hostedSystemPrompt,
127
+ };
128
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Runtime barrel for the daemon product.
3
+ *
4
+ * The SDK groups its runtime seams into namespace exports (`bootstrap`,
5
+ * `observability`, `operations`, `security`, `shell`, `state`, `transport`,
6
+ * `ui`). This file names the ones the daemon composition actually uses so the
7
+ * composition modules import them as plain symbols, and so there is exactly one
8
+ * place to look when the SDK moves one.
9
+ *
10
+ * It re-exports and nothing else: no local runtime entry points, no wrappers, no
11
+ * behavior. Anything that needs behavior belongs in its own module or in the
12
+ * SDK.
13
+ */
14
+
15
+ import {
16
+ bootstrap,
17
+ observability,
18
+ operations,
19
+ security,
20
+ shell,
21
+ transport,
22
+ ui,
23
+ } from '@pellux/goodvibes-sdk/platform/runtime';
24
+
25
+ // State, the event bus and the read-model store.
26
+ export * from '@pellux/goodvibes-sdk/platform/runtime/state';
27
+ export * from '@pellux/goodvibes-sdk/platform/runtime/store';
28
+ export * from '@pellux/goodvibes-sdk/platform/runtime/feature-flags';
29
+ export * from '@pellux/goodvibes-sdk/platform/runtime/settings';
30
+ export * from '@pellux/goodvibes-sdk/platform/runtime/sandbox';
31
+
32
+ // Shell paths + the surface-scoped storage handle.
33
+ export const createShellPathService = shell.createShellPathService;
34
+ export type ShellPathService = shell.ShellPathService;
35
+ export const createSessionSurface = operations.createSessionSurface;
36
+ export type SessionSurface = operations.SessionSurface;
37
+ export const WorktreeRegistry = shell.WorktreeRegistry;
38
+ export type WorktreeRegistry = shell.WorktreeRegistry;
39
+
40
+ // Remote execution + the distributed runtime.
41
+ export const DistributedRuntimeManager = operations.DistributedRuntimeManager;
42
+ export type DistributedRuntimeManager = operations.DistributedRuntimeManager;
43
+ export const RemoteRunnerRegistry = operations.RemoteRunnerRegistry;
44
+ export type RemoteRunnerRegistry = operations.RemoteRunnerRegistry;
45
+ export const RemoteSupervisor = operations.RemoteSupervisor;
46
+ export type RemoteSupervisor = operations.RemoteSupervisor;
47
+
48
+ // Boot helpers the daemon runs itself (the facade does not know about them).
49
+ export const syncConfiguredServices = bootstrap.synchronizeConfiguredServices;
50
+
51
+ // Observability.
52
+ export const TelemetryApiService = observability.TelemetryApiService;
53
+ export type TelemetryApiService = observability.TelemetryApiService;
54
+ export const ComponentHealthMonitor = observability.ComponentHealthMonitor;
55
+ export type ComponentHealthMonitor = observability.ComponentHealthMonitor;
56
+ export const IdempotencyStore = observability.IdempotencyStore;
57
+ export type IdempotencyStore = observability.IdempotencyStore;
58
+
59
+ // Security.
60
+ export const PolicyRuntimeState = security.PolicyRuntimeState;
61
+ export type PolicyRuntimeState = security.PolicyRuntimeState;
62
+
63
+ // Integration helpers (surface-scoped continuity reads) and the no-op screen
64
+ // stand-ins. The daemon facade's service-graph contract names a panel manager
65
+ // and a keybindings manager because a surface that has a screen supplies real
66
+ // ones; the daemon has no screen, and the SDK ships the honest no-ops for
67
+ // exactly this case rather than leaving a hole a fake would fill.
68
+ export const IntegrationHelperService = ui.IntegrationHelperService;
69
+ export type IntegrationHelperService = ui.IntegrationHelperService;
70
+ export const createNoopPanelManager = ui.createNoopPanelManager;
71
+ export const createNoopKeybindingsManager = ui.createNoopKeybindingsManager;
72
+ export type PanelManagerLike = ui.PanelManagerLike;
73
+ export type KeybindingsManagerLike = ui.KeybindingsManagerLike;
74
+
75
+ // Notification routing types (the router itself is imported from its own subpath).
76
+ export type Notification = ui.Notification;
77
+ export type RoutingDecision = ui.RoutingDecision;
78
+
79
+ // Outbound network transport installation (proxy/TLS policy from settings).
80
+ export const GlobalNetworkTransportInstaller = transport.GlobalNetworkTransportInstaller;
81
+ export type GlobalNetworkTransportInstaller = transport.GlobalNetworkTransportInstaller;
82
+
83
+
84
+ // Runtime event payload unions, re-exported so a consumer names one import path
85
+ // for the bus and the shapes that travel on it.
86
+ export type {
87
+ AgentEvent,
88
+ CommunicationEvent,
89
+ OpsEvent,
90
+ OrchestrationEvent,
91
+ PermissionEvent,
92
+ ProviderEvent,
93
+ RouteEvent,
94
+ SessionEvent,
95
+ TaskEvent,
96
+ ToolEvent,
97
+ TransportEvent,
98
+ TurnEvent,
99
+ WorkflowEvent,
100
+ } from '@pellux/goodvibes-sdk/events';
@@ -0,0 +1,101 @@
1
+ // ---------------------------------------------------------------------------
2
+ // knowledge-services.ts — the knowledge/wiki + home-graph stack
3
+ //
4
+ // Constructs the three KnowledgeStores (regular wiki, agent, home-graph), their
5
+ // semantic services, the ingest services, the home-graph service, and the
6
+ // project-planning + work-plan stores. Extracted into its own module rather
7
+ // than built inline in services.ts, which sits at the architecture check's
8
+ // 800-line cap (scripts/check-architecture.ts) — new/large construction blocks
9
+ // get their own module and a single wiring call there (mirrors
10
+ // createWorkstreamServices / createDurabilityServices / createCodeIndexServices).
11
+ //
12
+ // The semantic services + ingest services receive the memory-governor
13
+ // backpressure seams (isBackgroundPaused for the knowledge-self-improvement job
14
+ // + admitExpensiveWork for the critical-tier refusal), mirroring the SDK's own
15
+ // createRuntimeServices. The web-knowledge gap repairer is wired by the caller
16
+ // AFTER webSearchService exists (it is constructed later in services.ts), using
17
+ // the semantic + ingest handles this returns.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import {
21
+ HomeGraphService,
22
+ GOODVIBES_AGENT_KNOWLEDGE_DB_FILE,
23
+ HOME_GRAPH_KNOWLEDGE_EXTENSION,
24
+ KnowledgeService,
25
+ KnowledgeSemanticService,
26
+ KnowledgeStore,
27
+ ProjectPlanningService,
28
+ createProviderBackedKnowledgeSemanticLlm,
29
+ projectPlanningProjectIdFromPath,
30
+ } from '@pellux/goodvibes-sdk/platform/knowledge';
31
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
32
+ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
33
+ import type { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
34
+ import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
35
+ import type { RuntimeEventBus } from '@/runtime/index.ts';
36
+ import { WorkPlanStore } from '@pellux/goodvibes-sdk/platform/workflow';
37
+ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../config/surface.ts';
38
+
39
+ const REGULAR_KNOWLEDGE_DB_FILE = 'knowledge-wiki.sqlite';
40
+ const HOME_GRAPH_KNOWLEDGE_DB_FILE = 'knowledge-home-graph.sqlite';
41
+
42
+ export interface KnowledgeServicesDeps {
43
+ readonly configManager: ConfigManager;
44
+ readonly providerRegistry: ProviderRegistry;
45
+ readonly artifactStore: ArtifactStore;
46
+ readonly memoryRegistry: MemoryRegistry;
47
+ readonly runtimeBus: RuntimeEventBus;
48
+ readonly workingDirectory: string;
49
+ readonly homeDirectory: string;
50
+ /** True while the 'knowledge-self-improvement' job is governor-paused. */
51
+ readonly isBackgroundPaused: () => boolean;
52
+ /** Critical-tier admission gate for expensive knowledge work. */
53
+ readonly admitExpensiveWork: (label: string) => { allowed: boolean; reason?: string | undefined };
54
+ }
55
+
56
+ export interface KnowledgeServices {
57
+ readonly knowledgeStore: KnowledgeStore;
58
+ readonly agentKnowledgeStore: KnowledgeStore;
59
+ readonly homeGraphKnowledgeStore: KnowledgeStore;
60
+ readonly knowledgeSemanticService: KnowledgeSemanticService;
61
+ readonly homeGraphSemanticService: KnowledgeSemanticService;
62
+ readonly agentKnowledgeSemanticService: KnowledgeSemanticService;
63
+ readonly knowledgeService: KnowledgeService;
64
+ readonly agentKnowledgeService: KnowledgeService;
65
+ readonly homeGraphService: HomeGraphService;
66
+ readonly projectPlanningService: ProjectPlanningService;
67
+ readonly projectPlanningProjectId: string;
68
+ readonly workPlanStore: WorkPlanStore;
69
+ }
70
+
71
+ /** Construct the knowledge/wiki + home-graph stack with governor backpressure wired in. */
72
+ export function createKnowledgeServices(deps: KnowledgeServicesDeps): KnowledgeServices {
73
+ const { configManager, providerRegistry, artifactStore, memoryRegistry, runtimeBus, isBackgroundPaused, admitExpensiveWork } = deps;
74
+ const knowledgeStore = new KnowledgeStore({ configManager, dbFileName: REGULAR_KNOWLEDGE_DB_FILE });
75
+ const agentKnowledgeStore = new KnowledgeStore({ configManager, dbFileName: GOODVIBES_AGENT_KNOWLEDGE_DB_FILE });
76
+ const homeGraphKnowledgeStore = new KnowledgeStore({ configManager, dbFileName: HOME_GRAPH_KNOWLEDGE_DB_FILE });
77
+ const knowledgeSemanticLlm = createProviderBackedKnowledgeSemanticLlm(providerRegistry, { timeoutMs: 20_000, maxConcurrent: 1 });
78
+ const knowledgeSemanticService = new KnowledgeSemanticService(knowledgeStore, { llm: knowledgeSemanticLlm, maxLlmSourcesPerReindex: 3, isBackgroundPaused, admitExpensiveWork });
79
+ const homeGraphSemanticService = new KnowledgeSemanticService(homeGraphKnowledgeStore, { llm: knowledgeSemanticLlm, maxLlmSourcesPerReindex: 3, objectProfiles: HOME_GRAPH_KNOWLEDGE_EXTENSION.objectProfiles, isBackgroundPaused, admitExpensiveWork });
80
+ const agentKnowledgeSemanticService = new KnowledgeSemanticService(agentKnowledgeStore, { llm: knowledgeSemanticLlm, maxLlmSourcesPerReindex: 3, isBackgroundPaused, admitExpensiveWork });
81
+ const knowledgeService = new KnowledgeService(knowledgeStore, artifactStore, undefined, { memoryRegistry, runtimeBus, semanticService: knowledgeSemanticService, admitExpensiveWork });
82
+ knowledgeService.attachRuntimeBus(runtimeBus);
83
+ const agentKnowledgeService = new KnowledgeService(agentKnowledgeStore, artifactStore, undefined, { memoryRegistry, runtimeBus, semanticService: agentKnowledgeSemanticService, admitExpensiveWork });
84
+ agentKnowledgeService.attachRuntimeBus(runtimeBus);
85
+ const homeGraphService = new HomeGraphService(homeGraphKnowledgeStore, artifactStore, { semanticService: homeGraphSemanticService, admitExpensiveWork });
86
+ const projectPlanningProjectId = projectPlanningProjectIdFromPath(deps.workingDirectory);
87
+ const projectPlanningService = new ProjectPlanningService(knowledgeStore, { defaultProjectId: projectPlanningProjectId });
88
+ const workPlanStore = new WorkPlanStore({
89
+ homeDirectory: deps.homeDirectory,
90
+ surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT,
91
+ projectId: projectPlanningProjectId,
92
+ projectRoot: deps.workingDirectory,
93
+ source: GOODVIBES_DAEMON_SURFACE_ROOT,
94
+ });
95
+ return {
96
+ knowledgeStore, agentKnowledgeStore, homeGraphKnowledgeStore,
97
+ knowledgeSemanticService, homeGraphSemanticService, agentKnowledgeSemanticService,
98
+ knowledgeService, agentKnowledgeService, homeGraphService,
99
+ projectPlanningService, projectPlanningProjectId, workPlanStore,
100
+ };
101
+ }