@pellux/goodvibes-daemon 1.28.19 → 1.28.21

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 (114) hide show
  1. package/CHANGELOG.md +119 -63
  2. package/README.md +24 -19
  3. package/bin/launcher-support.js +5 -5
  4. package/package.json +6 -4
  5. package/scripts/postinstall.js +8 -8
  6. package/src/cli/command-catalog.ts +22 -22
  7. package/src/cli/completion.ts +4 -4
  8. package/src/cli/help.ts +5 -5
  9. package/src/cli/index.ts +3 -3
  10. package/src/cli/parser.ts +2 -2
  11. package/src/cli/surface-catalog.ts +1 -1
  12. package/src/cli/types.ts +2 -2
  13. package/src/cluster/daemon-ws-call.ts +5 -5
  14. package/src/cluster/raw-reply-route.ts +5 -5
  15. package/src/config/checkpoint-settings.ts +7 -7
  16. package/src/config/config-key-guard.ts +22 -0
  17. package/src/config/run-daemon-config-migration.ts +3 -3
  18. package/src/config/secret-config.ts +7 -7
  19. package/src/config/surface.ts +3 -3
  20. package/src/core/pairing-banner.ts +5 -5
  21. package/src/daemon/cli.ts +45 -43
  22. package/src/daemon/config-command.ts +15 -15
  23. package/src/daemon/handlers/context.ts +1 -1
  24. package/src/daemon/handlers/contracts.ts +19 -4
  25. package/src/daemon/handlers/credentials.ts +1 -1
  26. package/src/daemon/handlers/drafts/draft-store.ts +3 -3
  27. package/src/daemon/handlers/drafts/register.ts +4 -4
  28. package/src/daemon/handlers/inbox/aggregator.ts +8 -8
  29. package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
  30. package/src/daemon/handlers/inbox/index.ts +7 -7
  31. package/src/daemon/handlers/inbox/mapping.ts +2 -2
  32. package/src/daemon/handlers/inbox/poller.ts +5 -5
  33. package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
  34. package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
  35. package/src/daemon/handlers/inbox/providers/email.ts +3 -3
  36. package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
  37. package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
  38. package/src/daemon/handlers/index.ts +18 -8
  39. package/src/daemon/handlers/payments/address-store.ts +54 -0
  40. package/src/daemon/handlers/payments/budget-store.ts +356 -0
  41. package/src/daemon/handlers/payments/card-store.ts +486 -0
  42. package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
  43. package/src/daemon/handlers/payments/index.ts +38 -0
  44. package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
  45. package/src/daemon/handlers/payments/notifier.ts +112 -0
  46. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  47. package/src/daemon/handlers/payments/register.ts +518 -0
  48. package/src/daemon/handlers/register.ts +3 -3
  49. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  51. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  52. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  53. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  54. package/src/daemon/handlers/remote/index.ts +1 -1
  55. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  56. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  57. package/src/daemon/handlers/routing/index.ts +1 -1
  58. package/src/daemon/handlers/routing/route-store.ts +1 -1
  59. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  60. package/src/daemon/handlers/sqlite-store.ts +9 -9
  61. package/src/daemon/handlers/triage/index.ts +1 -1
  62. package/src/daemon/handlers/triage/integration.ts +3 -3
  63. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  64. package/src/daemon/handlers/triage/scorer.ts +2 -2
  65. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  66. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  67. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  68. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  69. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  70. package/src/daemon/handlers/triage/types.ts +2 -2
  71. package/src/daemon/lifecycle.ts +5 -5
  72. package/src/daemon/local-daemon-state.ts +7 -7
  73. package/src/daemon/pair-command.ts +14 -14
  74. package/src/daemon/provision-wake-model.ts +5 -5
  75. package/src/daemon/send/channels.ts +7 -7
  76. package/src/daemon/send/command.ts +11 -11
  77. package/src/daemon/send/composition.ts +5 -5
  78. package/src/daemon/send/failure-text.ts +6 -6
  79. package/src/daemon/send/inert-text.ts +18 -18
  80. package/src/daemon/send/stdin.ts +3 -3
  81. package/src/daemon/service-commands.ts +32 -32
  82. package/src/daemon/sessions-command.ts +7 -7
  83. package/src/daemon/status-command.ts +22 -22
  84. package/src/daemon/webui-command.ts +14 -14
  85. package/src/runtime/boot-tasks.ts +1 -1
  86. package/src/runtime/browser-checkout-seam-holder.ts +55 -0
  87. package/src/runtime/cluster-composition.ts +9 -9
  88. package/src/runtime/cluster-group-composition.ts +7 -7
  89. package/src/runtime/conversation-rewind-port.ts +8 -8
  90. package/src/runtime/credential-composition.ts +2 -2
  91. package/src/runtime/daemon-handler-composition.ts +61 -4
  92. package/src/runtime/device-posture-composition.ts +10 -10
  93. package/src/runtime/disposal-wiring.ts +8 -8
  94. package/src/runtime/fleet-needs-input-push.ts +4 -4
  95. package/src/runtime/fleet-services.ts +1 -1
  96. package/src/runtime/hosted-session-composition.ts +13 -13
  97. package/src/runtime/index.ts +1 -1
  98. package/src/runtime/knowledge-services.ts +2 -2
  99. package/src/runtime/legacy-daemon-migration.ts +43 -43
  100. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  101. package/src/runtime/mail-composition.ts +6 -6
  102. package/src/runtime/notification-dispatch.ts +7 -7
  103. package/src/runtime/payments-composition.ts +187 -0
  104. package/src/runtime/plugin-composition.ts +7 -7
  105. package/src/runtime/runtime-services-types.ts +9 -9
  106. package/src/runtime/services.ts +41 -32
  107. package/src/runtime/trigger-services.ts +1 -1
  108. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  109. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  110. package/src/runtime/update-check.ts +4 -4
  111. package/src/runtime/workspace-checkpointing.ts +6 -6
  112. package/src/testing/daemon-fixture.ts +11 -11
  113. package/src/testing/hosted-session-failures.ts +4 -4
  114. package/src/version.ts +2 -2
@@ -1,9 +1,9 @@
1
1
  /**
2
- * notification-dispatch.ts how this daemon dispatches notices without a screen.
2
+ * notification-dispatch.ts, how this daemon dispatches notices without a screen.
3
3
  *
4
4
  * The SDK's NotificationRouter decides where a domain notification goes and
5
- * collapses bursts and batches. Its three targets `conversation`,
6
- * `status_bar`, `panel_only` are all SCREEN targets: an inline conversation
5
+ * collapses bursts and batches. Its three targets, `conversation`,
6
+ * `status_bar`, `panel_only`, are all SCREEN targets: an inline conversation
7
7
  * line, a status bar, a panel. There is no channel member in that type, and
8
8
  * there never was. Which means the router is a surface mechanism end to end,
9
9
  * and this process has no screen.
@@ -11,7 +11,7 @@
11
11
  * This module used to wire the router to every curated domain anyway, writing
12
12
  * into a bounded ring whose `list()` had no caller anywhere in this repository.
13
13
  * Six domains of events, for the daemon's whole lifetime, into a buffer nobody
14
- * read and the type declaring it still described the ring as "the panel's
14
+ * read, and the type declaring it still described the ring as "the panel's
15
15
  * live producer", for a product with no panels. That silent-success failure
16
16
  * class is exactly what this module removes: the producer goes.
17
17
  *
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * One notice had no such path: memory pressure. The MemoryGovernor measures the
25
25
  * process it runs in, so the daemon's pressure is the daemon's own and no
26
- * surface can report it and the daemon that ran out of memory is exactly the
26
+ * surface can report it, and the daemon that ran out of memory is exactly the
27
27
  * one that cannot tell you afterwards. It now goes out over the operator's
28
28
  * configured notice destination (`notifications.webhookUrls`, the same list the
29
29
  * bus bridge uses), and says so at its own level in the activity log when no
@@ -56,7 +56,7 @@ export interface DaemonNoticeChannel {
56
56
  * one event type rather than a subscription to the domain: the tier change is
57
57
  * the operator's business and the churn is not.
58
58
  *
59
- * Delivery failure is logged, never thrown a webhook endpoint being down is
59
+ * Delivery failure is logged, never thrown, a webhook endpoint being down is
60
60
  * not a reason for the process reporting memory pressure to also crash.
61
61
  */
62
62
  export function wireMemoryPressureChannelNotice(
@@ -69,7 +69,7 @@ export function wireMemoryPressureChannelNotice(
69
69
  const level = memoryPressureLevel(payload);
70
70
  const line = memoryPressureLine(payload);
71
71
  if (!channel.isConfigured()) {
72
- // No destination configured. The notice still exists at its own
72
+ // No destination configured. The notice still exists, at its own
73
73
  // severity, where the operator looks when the daemon misbehaves.
74
74
  if (level === 'critical') logger.error(line);
75
75
  else if (level === 'warning') logger.warn(line);
@@ -0,0 +1,187 @@
1
+ /**
2
+ * payments-composition.ts, the payment capability inside THIS daemon.
3
+ *
4
+ * ── What was wrong ────────────────────────────────────────────────────────
5
+ *
6
+ * All seven `payments.*` verbs were cataloged, advertised with a real HTTP
7
+ * route, and answered 501 NOT_INVOKABLE to every caller. The route existed, the
8
+ * descriptor was honest about its shape, and nothing was attached, so the webui
9
+ * and the desktop app could read the contract and never use it. The terminal app
10
+ * did not paper over this: it has no payments handlers either; what it has is a
11
+ * settings surface (`/payments card`) that writes four flat config keys, which
12
+ * is a per-surface stopgap, not a served capability.
13
+ *
14
+ * ── What this composes ────────────────────────────────────────────────────
15
+ *
16
+ * The SDK owns the capability (`platform/payments`) and states, in
17
+ * card-material.ts, that the store behind it "is implemented by the daemon
18
+ * against its own secret store". It ships no such implementation. So this
19
+ * composes the two stores that were missing, from paths the daemon already owns:
20
+ *
21
+ * - card metadata at `<home>/.goodvibes/<surface>/control-plane/payments-cards.json`,
22
+ * through `controlPlaneStorePath`, the one resolver every control-plane
23
+ * store path goes through, so it lands beside pairing-tokens.json rather
24
+ * than in the unscoped orphan directory that resolver exists to prevent;
25
+ * - card MATERIAL one field per key in the DAEMON secret tier, written with an
26
+ * explicit `scope: 'daemon'`. Explicit because a payments card key is not in
27
+ * the SDK's daemon-needed registry, so an omitted scope would file the
28
+ * material in the surface tier while the daemon reads the daemon tier, the
29
+ * exact split secret-config.ts's `defaultSecretBackedScope` documents as the
30
+ * mailbox-password failure and names "the same shape of failure at purchase
31
+ * time" for a card;
32
+ * - the purchase audit ledger beside the card file.
33
+ *
34
+ * ── The budget ledger is durable ──────────────────────────────────────────
35
+ *
36
+ * `BudgetLedger` used to be constructed empty here and never persisted, correct
37
+ * only while checkout stayed unattached (the sole writer of a spend record was
38
+ * the checkout flow, so there was nothing to persist). Now that checkout is
39
+ * wired below, `DurableBudgetLedger`
40
+ * (daemon/handlers/payments/budget-store.ts) is used instead: it loads its
41
+ * state from `payments-budget.json` beside the card and purchase files at
42
+ * construction and writes back after every reservation, commit and release, so
43
+ * a daemon restarted mid-day does not hand back a budget it already spent.
44
+ *
45
+ * ── The checkout pair, over the sdk 2.0.19 browser-checkout seam ──────────
46
+ *
47
+ * `payments.checkout.begin`/`.fillCard` need a `CardMaterialRedactor` bound to
48
+ * the SAME browser engine the `browser.*` verbs drive, and this daemon does not
49
+ * build that engine, `composeDaemonBrowser` does (control-plane/routes/
50
+ * browser-composition.ts), inside `registerGatewayVerbGroups`, which THIS
51
+ * composition runs before (see runtime/services.ts). `checkoutSeam` is
52
+ * therefore a GETTER, not a value: services.ts passes `onBrowserCheckout` to
53
+ * `attachWsOnlyGatewayVerbHandlers` wired to fill the SAME holder this getter
54
+ * reads (runtime/browser-checkout-seam-holder.ts), and `register.ts`'s checkout
55
+ * handlers read it fresh on every call rather than once at composition time.
56
+ *
57
+ * The rest of `PaymentsGatewayServiceImpl`'s dependencies this composition owns
58
+ * outright: `configBackedAddressStore` reads the shipping/billing addresses the
59
+ * owner profile already writes into `payments.*Address.*` config keys (see
60
+ * that module's header for the exact defect this closes), and
61
+ * `channelBackedPaymentNotifier`/`createProviderBackedMerchantJudgeModel` adapt
62
+ * this daemon's channel router and provider registry to the ports the SDK
63
+ * declares. The untrusted-content ledger is the SAME process-wide singleton the
64
+ * browser composition binds its engine to (`getProcessUntrustedContentLedger`),
65
+ * never a private one, for the reason browser-composition.ts's header gives:
66
+ * a private ledger would make cross-capability derivation invisible.
67
+ *
68
+ * `channelBackedPaymentNotifier`'s own header names the one piece deliberately
69
+ * left for a later pass: no live inbound-reply correlation, so every purchase
70
+ * settles on the windows' own silence rules rather than an early answer. That
71
+ * is a scoped, disclosed gap, not a silent one.
72
+ */
73
+ import { controlPlaneStorePath } from '@pellux/goodvibes-sdk/platform/control-plane';
74
+ import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
75
+ import { createModelMerchantJudge, readCvvHandling } from '@pellux/goodvibes-sdk/platform/payments';
76
+ import type { BudgetLedger, PaymentsConfigReader } from '@pellux/goodvibes-sdk/platform/payments';
77
+ import { getProcessUntrustedContentLedger } from '@pellux/goodvibes-sdk/platform/security';
78
+ import type { ConfigManager, SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
79
+ import type { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
80
+ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
81
+ import type { ShellPathService } from '@/runtime/index.ts';
82
+ import {
83
+ DaemonCardStore,
84
+ DaemonPurchaseLedger,
85
+ DurableBudgetLedger,
86
+ channelBackedPaymentNotifier,
87
+ configBackedAddressStore,
88
+ createProviderBackedMerchantJudgeModel,
89
+ registerPaymentsMethods,
90
+ type CheckoutComposition,
91
+ type PaymentsSecretStore,
92
+ } from '../daemon/handlers/payments/index.ts';
93
+ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../config/surface.ts';
94
+ import type { BrowserCheckoutSeamHolder } from './browser-checkout-seam-holder.ts';
95
+
96
+ export interface PaymentsCompositionOptions {
97
+ readonly configManager: ConfigManager;
98
+ readonly shellPaths: ShellPathService;
99
+ readonly secretsManager: SecretsManager;
100
+ /** Binding the catalog is what turns the family from a 501 facade into handlers. */
101
+ readonly gatewayMethods: GatewayMethodCatalog;
102
+ /**
103
+ * Whether this node is the one currently allowed to spend.
104
+ *
105
+ * Reported by `payments.budget.status` and never defaulted, see the SDK's
106
+ * gates.ts: on a clustered install the wrong answer is a double-spend.
107
+ */
108
+ readonly isPaymentsLeader: () => boolean;
109
+ /** Where the checkout pair reads the browser-checkout seam; see this file's header. */
110
+ readonly checkoutSeam: BrowserCheckoutSeamHolder['get'];
111
+ /** Delivers a purchase notice; the SAME router every other channel send in this daemon uses. */
112
+ readonly channelDeliveryRouter: Pick<ChannelDeliveryRouter, 'deliver'>;
113
+ /** Judges an unfamiliar merchant's recourse through the currently configured model. */
114
+ readonly providerRegistry: Pick<ProviderRegistry, 'getCurrentModel' | 'getForModel'>;
115
+ }
116
+
117
+ export interface PaymentsServices {
118
+ readonly cards: DaemonCardStore;
119
+ readonly purchases: DaemonPurchaseLedger;
120
+ readonly budget: BudgetLedger;
121
+ /** Detaches the handlers. Held by the runtime disposal scope. */
122
+ readonly unregister: () => void;
123
+ }
124
+
125
+ /**
126
+ * The narrow secret port the card store gets: three operations over the daemon
127
+ * tier, and no way to reach any other credential in the process. The same
128
+ * treatment cluster-group-composition.ts gives the group key.
129
+ */
130
+ function daemonScopedSecrets(secretsManager: SecretsManager): PaymentsSecretStore {
131
+ return {
132
+ get: (key) => secretsManager.get(key),
133
+ set: async (key, value) => {
134
+ await secretsManager.set(key, value, { scope: 'daemon', medium: 'secure' });
135
+ },
136
+ delete: async (key) => {
137
+ await secretsManager.delete(key, { scope: 'daemon' });
138
+ },
139
+ };
140
+ }
141
+
142
+ /** Read live, per call: a budget raised five minutes ago applies to the next read. */
143
+ function livePaymentsConfig(configManager: ConfigManager): PaymentsConfigReader {
144
+ return { get: (key: string) => configManager.get(key as Parameters<ConfigManager['get']>[0]) };
145
+ }
146
+
147
+ /**
148
+ * Build the payment stores and bind the answerable verbs to them.
149
+ *
150
+ * The card and purchase stores read lazily and write only when a verb asks
151
+ * them to, so composing either by itself creates no file activity. The budget
152
+ * ledger is different: `DurableBudgetLedger`'s constructor reads
153
+ * `payments-budget.json` synchronously to load today's pools
154
+ * (daemon/handlers/payments/budget-store.ts), so constructing the result of
155
+ * THIS function does touch disk, once, for that one file, before any verb is
156
+ * ever called.
157
+ */
158
+ export function createPaymentsServices(options: PaymentsCompositionOptions): PaymentsServices {
159
+ const config = livePaymentsConfig(options.configManager);
160
+ const cards = new DaemonCardStore({
161
+ filePath: controlPlaneStorePath(options.shellPaths, GOODVIBES_DAEMON_SURFACE_ROOT, 'payments-cards.json'),
162
+ secrets: daemonScopedSecrets(options.secretsManager),
163
+ cvvHandling: () => readCvvHandling(config),
164
+ });
165
+ const purchases = new DaemonPurchaseLedger({
166
+ filePath: controlPlaneStorePath(options.shellPaths, GOODVIBES_DAEMON_SURFACE_ROOT, 'payments-purchases.json'),
167
+ });
168
+ const budget = new DurableBudgetLedger(
169
+ controlPlaneStorePath(options.shellPaths, GOODVIBES_DAEMON_SURFACE_ROOT, 'payments-budget.json'),
170
+ );
171
+ const checkout: CheckoutComposition = {
172
+ seam: options.checkoutSeam,
173
+ addresses: configBackedAddressStore(config),
174
+ notifier: channelBackedPaymentNotifier(config, options.channelDeliveryRouter),
175
+ merchantJudge: createModelMerchantJudge(createProviderBackedMerchantJudgeModel(options.providerRegistry)),
176
+ untrusted: getProcessUntrustedContentLedger(),
177
+ };
178
+ const unregister = registerPaymentsMethods(options.gatewayMethods, {
179
+ cards,
180
+ purchases,
181
+ budget,
182
+ config,
183
+ isPaymentsLeader: options.isPaymentsLeader,
184
+ checkout,
185
+ });
186
+ return { cards, purchases, budget, unregister };
187
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * plugin-composition.ts what a plugin dropped into the plugin directory gets
2
+ * plugin-composition.ts, what a plugin dropped into the plugin directory gets
3
3
  * to register when the host loading it is the daemon.
4
4
  *
5
5
  * A plugin registers into whatever registries the host hands its loader. Two
6
- * hosts load a plugin today the terminal app and this daemon and neither
6
+ * hosts load a plugin today, the terminal app and this daemon, and neither
7
7
  * holds every registry a plugin might want. The design (see
8
8
  * goodvibes-tui/docs/decisions/2026-07-30-plugin-registrations-split-verb-side-and-surface-side.md)
9
9
  * is: one plugin package, loaded by both hosts, each loading the registrations
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * The two surface-side kinds are the ones this host cannot serve. The SDK's
24
24
  * PluginLoaderDeps has no optional members and its API guards nothing, so a
25
- * host that passes nothing for a registry does not decline that kind it
25
+ * host that passes nothing for a registry does not decline that kind, it
26
26
  * throws a TypeError inside the plugin's own init, which the loader catches by
27
27
  * dropping THE WHOLE PLUGIN, including the halves this host could have run.
28
28
  * "Ignores the rest" therefore has to be a real registry that accepts the
@@ -67,8 +67,8 @@ export function createUnservedCommandRegistry(): CommandRegistryLike {
67
67
  *
68
68
  * The runs this daemon hosts build their tools through the agent orchestrator's
69
69
  * own registry, not this one, so a plugin tool registered here is cataloged and
70
- * unhandled. That is the recorded classification `registerTool` is on the
71
- * surface-side list and it was written when the daemon hosted no runs of its
70
+ * unhandled. That is the recorded classification, `registerTool` is on the
71
+ * surface-side list, and it was written when the daemon hosted no runs of its
72
72
  * own; the round that moves session hosting daemon-side is the one that
73
73
  * re-examines it. Until then, a registration here is honest rather than quiet.
74
74
  */
@@ -84,8 +84,8 @@ export class UnservedToolRegistry extends ToolRegistry {
84
84
  /**
85
85
  * Build the loader dependencies for this host.
86
86
  *
87
- * The delivery router is deliberately the manager's own the one replies leave
88
- * through so a registered strategy is a strategy that sends. Handing over a
87
+ * The delivery router is deliberately the manager's own, the one replies leave
88
+ * through, so a registered strategy is a strategy that sends. Handing over a
89
89
  * second router built from the same arguments is how a registration succeeds
90
90
  * and reaches nothing, which is the shape this whole seam is about.
91
91
  */
@@ -1,12 +1,12 @@
1
1
  /**
2
- * runtime-services-types.ts the public contract createRuntimeServices() takes
2
+ * runtime-services-types.ts, the public contract createRuntimeServices() takes
3
3
  * and returns.
4
4
  *
5
5
  * Split out of services.ts (the composition root that builds every one of
6
6
  * these fields) so the construction logic can stay under the repo's
7
7
  * architecture line-count gate without trimming 35 arbitrary lines to clear
8
8
  * the number. This module owns ONLY the shape of the input options and the
9
- * output surface no runtime code, no wiring order, nothing that constructs
9
+ * output surface, no runtime code, no wiring order, nothing that constructs
10
10
  * anything. services.ts re-exports both types from here, so no import site
11
11
  * anywhere else in the app had to change.
12
12
  */
@@ -76,7 +76,7 @@ export interface RuntimeServicesOptions {
76
76
  * Threaded into `SecretsManager` so the override MOVES the daemon-scoped
77
77
  * credential store; without it a daemon told to run out of a temp tree still
78
78
  * read the real home's daemon secrets, so an "isolated" test daemon held the
79
- * owner's live credentials. One name for one thing `resolveGoodVibesHomeOwnership`
79
+ * owner's live credentials. One name for one thing, `resolveGoodVibesHomeOwnership`
80
80
  * is the single reader that produces it.
81
81
  */
82
82
  readonly daemonHomeDirectory?: string | undefined;
@@ -104,7 +104,7 @@ export interface RuntimeServices {
104
104
  readonly homeDirectory: string;
105
105
  /**
106
106
  * The `.goodvibes/<surface root>/` segment this daemon's own state lives
107
- * under always GOODVIBES_DAEMON_SURFACE_ROOT here (config/surface.ts).
107
+ * under, always GOODVIBES_DAEMON_SURFACE_ROOT here (config/surface.ts).
108
108
  * Declared so a consumer ASKS for it instead of deriving a second one; the
109
109
  * unscoped pre-split control-plane store is what deriving it twice produced.
110
110
  */
@@ -162,7 +162,7 @@ export interface RuntimeServices {
162
162
  readonly hookWorkbench: HookWorkbench;
163
163
  readonly pluginManager: PluginManager;
164
164
  readonly workflow: WorkflowServices;
165
- /** Stream watchers, on-exit process triggers and condition checks, supervised as one see trigger-services.ts. */
165
+ /** Stream watchers, on-exit process triggers and condition checks, supervised as one, see trigger-services.ts. */
166
166
  readonly triggerManager: TriggerManager;
167
167
  readonly voiceProviders: VoiceProviderRegistry;
168
168
  readonly voiceService: VoiceService;
@@ -219,13 +219,13 @@ export interface RuntimeServices {
219
219
  readonly contextAccountingHolder: ContextAccountingHolder; // bound at bootstrap.ts; see context-accounting-source.ts
220
220
  readonly wrfcController: WrfcController;
221
221
  readonly processManager: ProcessManager;
222
- /** The phase/work-item orchestration engine the SDK's platform/orchestration. */
222
+ /** The phase/work-item orchestration engine, the SDK's platform/orchestration. */
223
223
  readonly orchestrationEngine: OrchestrationEngine;
224
224
  readonly workstreamCommands: WorkstreamCommandService;
225
225
  /** The repo source-tree code index. */
226
226
  readonly codeIndexStore: CodeIndexStore;
227
227
  readonly codeIndexReindexScheduler: CodeIndexReindexScheduler; // tool-site reindex
228
- /** Daily snapshots of every SQLite store this runtime writes, with bounded retention; unref'd timers (mirrors the SDK composition hosts that tear down a runtime stop() it themselves). */
228
+ /** Daily snapshots of every SQLite store this runtime writes, with bounded retention; unref'd timers (mirrors the SDK composition, hosts that tear down a runtime stop() it themselves). */
229
229
  readonly storeSnapshotScheduler: StoreSnapshotScheduler;
230
230
  readonly appendOnlyRetentionScheduler: operations.DurabilityServices['appendOnlyRetentionScheduler']; // periodic append-only sweep; unref'd timers, stop() on teardown
231
231
  /** Stops the recurring crash-residue sweep; idempotent, unref'd timer (hosts that tear a runtime down call it). */
@@ -241,7 +241,7 @@ export interface RuntimeServices {
241
241
  /** Controller the governor uses to pause/resume the deferrable background jobs under pressure. */
242
242
  readonly pauseController: PauseController;
243
243
  readonly sessionLiveTurnControls: SessionLiveTurnControlsHolder;
244
- /** Unified live process registry (agents, WRFC chains, workflows, watchers, background processes) backing the Fleet panel; archive-aware finished subtrees can be moved to the session archive view. */
244
+ /** Unified live process registry (agents, WRFC chains, workflows, watchers, background processes) backing the Fleet panel; archive-aware, finished subtrees can be moved to the session archive view. */
245
245
  readonly processRegistry: ArchivableProcessRegistry;
246
246
  readonly modeManager: ModeManager;
247
247
  readonly fileUndoManager: FileUndoManager;
@@ -250,7 +250,7 @@ export interface RuntimeServices {
250
250
  readonly checkpointsCurrentlyAllowed: () => boolean;
251
251
  /** Surface-scoped continuity reads (recovery-file presence, last-session pointer). */
252
252
  readonly integrationHelpers: IntegrationHelperService;
253
- /** Per-workspace trust gate restricts write/execute/delegate tools until the workspace is trusted. */
253
+ /** Per-workspace trust gate, restricts write/execute/delegate tools until the workspace is trusted. */
254
254
  readonly workspaceTrustManager: operations.WorkspaceTrustManager;
255
255
  /**
256
256
  * The permission manager the runs this daemon hosts ask through. Its ask seam
@@ -56,11 +56,12 @@ import { createTriggerServices } from './trigger-services.ts';
56
56
  import { createWorkstreamServices } from '@pellux/goodvibes-sdk/platform/orchestration';
57
57
  import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
58
58
  import { createDaemonHandlerComposition } from './daemon-handler-composition.ts';
59
+ import { createBrowserCheckoutSeamHolder } from './browser-checkout-seam-holder.ts';
59
60
  import { createDevicePostureServices } from './device-posture-composition.ts';
60
61
  // Re-exported so the daemon entrypoint reaches the housekeeping sweep through
61
62
  // the same module it already imports the runtime graph from. `installDevicePosture`
62
63
  // is deliberately NOT re-exported: it registers the phone TOOL into a tool
63
- // registry, and the daemon registers no tools the sweep is the half it needs.
64
+ // registry, and the daemon registers no tools, the sweep is the half it needs.
64
65
  export { startDeviceHousekeeping } from './device-posture-composition.ts';
65
66
  import { createClusterServices, startClusterServices } from './cluster-group-composition.ts';
66
67
  import { createWorkspaceTrustDecisionAsk, trustGatedApprovalRaiser } from './trust/trust-gated-approvals.ts';
@@ -69,7 +70,7 @@ import type { RuntimeServicesOptions, RuntimeServices } from './runtime-services
69
70
  export type { RuntimeServicesOptions, RuntimeServices } from './runtime-services-types.ts';
70
71
 
71
72
  /**
72
- * createRuntimeServices the daemon's service graph.
73
+ * createRuntimeServices, the daemon's service graph.
73
74
  *
74
75
  * This is the one composition root the daemon has. Capabilities a client and
75
76
  * the daemon both need live in the SDK and are composed from there (memory
@@ -81,7 +82,7 @@ export type { RuntimeServicesOptions, RuntimeServices } from './runtime-services
81
82
  */
82
83
  export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeServices {
83
84
  // The SDK's disposal scope and its all-required poller list, plus the four
84
- // pollers only the daemon has see disposal-wiring.ts.
85
+ // pollers only the daemon has, see disposal-wiring.ts.
85
86
  const disposalScope = createDisposalScope('RuntimeServices');
86
87
  const workingDirectory = options.workingDir;
87
88
  const homeDirectory = options.homeDirectory;
@@ -105,7 +106,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
105
106
  // so the scheduler gates and the knowledge background jobs can consult the pause
106
107
  // controller before the MemoryGovernor (constructed at the composition tail)
107
108
  // drives it. The admission gate is late-bound: expensive entry points capture
108
- // this closure now and the governor binds into it at the tail until then
109
+ // this closure now and the governor binds into it at the tail, until then
109
110
  // everything is admitted (the daemon is still booting).
110
111
  const cacheRegistry = new CacheRegistry();
111
112
  const pauseController = new PauseController();
@@ -149,7 +150,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
149
150
  // Launch-tolerant: a provider whose API key is absent from the environment is
150
151
  // constructed with a placeholder that is stripped immediately afterwards, so
151
152
  // it lands unconfigured instead of throwing during construction. The daemon
152
- // has the same must-boot property the agent has it is a supervised service,
153
+ // has the same must-boot property the agent has, it is a supervised service,
153
154
  // and a constructor that throws on a missing key turns one unset variable into
154
155
  // a crash loop with no screen to explain it.
155
156
  const providerRegistry = createLaunchTolerantProviderRegistry({
@@ -186,8 +187,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
186
187
  );
187
188
  const hookActivityTracker = new HookActivityTracker();
188
189
  // featureFlags is REQUIRED here in practice, even though the SDK types it
189
- // optional. isFeatureGateEnabled(null, ...) is permissive by design a narrow
190
- // embed with no manager wired gets the capability rather than a silent off
190
+ // optional. isFeatureGateEnabled(null, ...) is permissive by design, a narrow
191
+ // embed with no manager wired gets the capability rather than a silent off,
191
192
  // so omitting it did not disable the watcher framework when watchers.enabled
192
193
  // is turned off; it made the setting configure nothing.
193
194
  const watcherRegistry = new WatcherRegistry({
@@ -235,9 +236,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
235
236
  task,
236
237
  // Conversation first: a follow-up message in a session gets an answer, not
237
238
  // a write-review-fix-confirm chain with a reviewer, quality gates and a
238
- // second agent. A chain opens only for an explicit authorization marker
239
+ // second agent. A chain opens only for an explicit authorization marker,
239
240
  // the channel confirmation the owner gave, or the schedule/trigger that
240
- // was confirmed when it was created or for a follow-up typed on a local
241
+ // was confirmed when it was created, or for a follow-up typed on a local
241
242
  // surface. Both `conversationGate.mode` and the gated-surfaces list are
242
243
  // read live.
243
244
  ...continuationChainOptions(input, {
@@ -256,7 +257,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
256
257
  // It also supplies the run's context, which is why the bare
257
258
  // `context: shared-session:<id>` line that used to sit below is gone.
258
259
  // Spread BEFORE the routing builder so an explicit tool list coming from
259
- // a routing intent still wins that builder emits `tools` only when it
260
+ // a routing intent still wins, that builder emits `tools` only when it
260
261
  // actually has one.
261
262
  ...conversationalTurnSpawnOptions(input, { configReader: configManager }),
262
263
  // Spawn routing through the SDK's shared model-reference resolver
@@ -286,7 +287,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
286
287
  // through. Without the secrets manager it cannot resolve a
287
288
  // goodvibes://secrets/... credential, so Telegram accepted every inbound
288
289
  // message and dropped every reply with "Missing Telegram bot token" while
289
- // ntfy which needs no secret worked.
290
+ // ntfy, which needs no secret, worked.
290
291
  secretsManager,
291
292
  serviceRegistry,
292
293
  runtimeBus: options.runtimeBus,
@@ -326,7 +327,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
326
327
  return record.id;
327
328
  },
328
329
  });
329
- // Knowledge/wiki + home-graph stack (governor backpressure wired in) see knowledge-services.ts.
330
+ // Knowledge/wiki + home-graph stack (governor backpressure wired in), see knowledge-services.ts.
330
331
  const {
331
332
  knowledgeStore, agentKnowledgeStore, homeGraphKnowledgeStore,
332
333
  knowledgeSemanticService, homeGraphSemanticService, agentKnowledgeSemanticService,
@@ -382,21 +383,32 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
382
383
  });
383
384
 
384
385
  // Which machines on this network are "us", and which of them reads the shared
385
- // inbox. Both inert until startCluster() no socket, no key material read;
386
+ // inbox. Both inert until startCluster(), no socket, no key material read;
386
387
  // see cluster-group-composition.ts for why they are built together.
387
388
  const { clusterGroup, clusterCoordinator } = createClusterServices({
388
389
  configManager, shellPaths, secretsManager,
389
390
  });
391
+ // ONE router, not two (a second built from the same four arguments would
392
+ // differ from the one replies leave through). Moved up from its original
393
+ // spot near the ws-only verb options (still consumed there): the payments
394
+ // composition below needs it too.
395
+ const channelDeliveryRouter = deliveryManager.getDeliveryRouter();
390
396
  // Daemon handler surfaces (see daemon-handler-composition.ts); the inbox
391
- // poller registers itself with the coordinator rather than starting eagerly.
397
+ // poller registers itself with the coordinator rather than starting eagerly,
398
+ // and the payments family stops being a cataloged 501 facade there.
399
+ // browserCheckoutSeam fills once `onBrowserCheckout` fires below, after this
400
+ // composition; read lazily, per call. See browser-checkout-seam-holder.ts.
401
+ const browserCheckoutSeam = createBrowserCheckoutSeamHolder();
392
402
  const daemonHandlers = createDaemonHandlerComposition({
393
403
  gatewayMethods,
394
404
  secretsManager,
395
405
  configManager,
396
406
  workingDirectory,
397
407
  homeDirectory,
408
+ shellPaths,
398
409
  distributedRuntime,
399
410
  clusterCoordinator,
411
+ checkoutSeam: browserCheckoutSeam.get, channelDeliveryRouter, providerRegistry,
400
412
  });
401
413
 
402
414
  // Remote runners and the sandboxes tool calls are confined to; see
@@ -424,12 +436,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
424
436
  const policyRuntimeState = new PolicyRuntimeState();
425
437
  const fileCache = new FileStateCache();
426
438
  const projectIndex = new ProjectIndex(workingDirectory);
427
- // ONE router, not two. This was a second ChannelDeliveryRouter built from the
428
- // same four arguments AutomationDeliveryManager builds its own from, so the
429
- // router the gateway verbs held and the router replies actually leave through
430
- // were different objects — and a delivery strategy registered on one was
431
- // invisible to the other. The manager's is the one that replies; it is the one.
432
- const channelDeliveryRouter = deliveryManager.getDeliveryRouter();
439
+ // channelDeliveryRouter now built earlier, near clusterCoordinator above.
433
440
  const processManager = new ProcessManager();
434
441
  // The phase/work-item orchestration engine, constructed before the process
435
442
  // registry so its fleet nodes (workstream/phase/work-item) can be folded in
@@ -447,7 +454,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
447
454
  });
448
455
  const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler };
449
456
  // The trigger family: stream watchers, on-exit process triggers, condition
450
- // checks fed to the fleet below as its trigger supervisor, so a trigger
457
+ // checks, fed to the fleet below as its trigger supervisor, so a trigger
451
458
  // is visible and steerable like every other running thing.
452
459
  const triggerManager = createTriggerServices({
453
460
  configManager, shellPaths, surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT,
@@ -477,7 +484,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
477
484
  acpHost, // Folds live hosted-agent sessions into the fleet as 'acp' rows
478
485
  });
479
486
  const modeManager = new ModeManager({ featureFlags }); const fileUndoManager = new FileUndoManager();
480
- // Checkpoints, gated on live workspace registration see workspace-checkpointing.ts.
487
+ // Checkpoints, gated on live workspace registration, see workspace-checkpointing.ts.
481
488
  const checkpointing = createWorkspaceCheckpointing({
482
489
  workspaceRoot: workingDirectory, surface, runtimeBus: options.runtimeBus, configManager, shellPaths,
483
490
  });
@@ -487,7 +494,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
487
494
  // admitted (mirrors the SDK's own createRuntimeServices idle gate).
488
495
  const { memoryConsolidationScheduler, powerManager, sessionLiveTurnControls } = wireIdlePowerAndLiveTurn({ configManager, memoryRegistry, runtimeBus: options.runtimeBus, isIdle: () => sessionBroker.countBusySessions() === 0 && !pauseController.isPaused('memory-consolidation') && admitExpensiveWork('memory consolidation').allowed, snapshotTick: () => storeSnapshotScheduler.tick(), heartbeat: async () => { await automationManager.triggerHeartbeat({ source: 'wake-catchup' }); }, powerSeam: options.powerSeam });
489
496
 
490
- // Construct + start the MemoryGovernor (default ON a safety feature) with the
497
+ // Construct + start the MemoryGovernor (default ON, a safety feature) with the
491
498
  // standard KNOWN cache adapters (knowledge stores + shared session broker),
492
499
  // then late-bind the admission gate the expensive entry points captured
493
500
  // earlier. The SDK owns this wiring.
@@ -514,7 +521,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
514
521
  });
515
522
  admitExpensiveWorkRef.current = (label) => memoryGovernor.admitExpensiveWork(label);
516
523
 
517
- // Managed local-voice provisioning (voice.local.status/install) single-flight
524
+ // Managed local-voice provisioning (voice.local.status/install), single-flight
518
525
  // one-act install + no-network status.
519
526
  const { voiceSetup, stopWakeHousekeeping } = wireVoiceSetup({ configManager, shellPaths, voiceProviders, admitExpensiveWork,
520
527
  // Boot provisioning of the wake-word model + its recovery sweep, opted into
@@ -531,6 +538,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
531
538
  // is what let these stores write to the unscoped orphan directory.
532
539
  surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT,
533
540
  homeDirectory, emailServiceDeps, describeEmailConfigProblem, processRegistry,
541
+ onBrowserCheckout: browserCheckoutSeam.set,
534
542
  // The registration-gated surface, not the raw manager: an explicit create in
535
543
  // an unregistered workspace refuses with something actionable.
536
544
  workspaceCheckpointManager: checkpointing.gatewayManager,
@@ -543,11 +551,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
543
551
  // to the owner principal instead of unknown.
544
552
  channelPolicy,
545
553
  approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input),
546
- // approvals.raise a surface CREATING an ask in this broker. Without it the
554
+ // approvals.raise, a surface CREATING an ask in this broker. Without it the
547
555
  // verb is cataloged and unhandled, and a client whose prompt runs outside
548
556
  // this process has no way to raise one.
549
557
  approvalRaise: approvalBroker,
550
- // credentials.set / credentials.delete a credential written THROUGH the
558
+ // credentials.set / credentials.delete, a credential written THROUGH the
551
559
  // control plane, so a client with no access to the daemon's settings file can
552
560
  // configure one. The value lands in the daemon's secret tier and the verb
553
561
  // never echoes it back.
@@ -562,6 +570,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
562
570
  disposal: disposalScope.registry,
563
571
  ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }),
564
572
  });
573
+ disposalScope.registry.add('browser checkout seam holder', () => browserCheckoutSeam.clear()); // newer than 'browser sessions' above, so runs first
565
574
  // A loopback fetch that isn't allow-listed asks once through the approval
566
575
  // broker; "allow for this project" persists and later fetches never ask. Built
567
576
  // once and shared with the tool registry so both ask alike.
@@ -573,15 +582,15 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
573
582
  const execPromptAnswerHandler = buildExecPromptAnswerHandler({ requestApproval: (input) => approvalBroker.requestApproval(input) });
574
583
  // Tool asks from the runs this daemon HOSTS. Without a manager here, the
575
584
  // background permission gate short-circuits to approved and every hosted
576
- // write, command and delegation ran ungated the workspace trust decision
585
+ // write, command and delegation ran ungated, the workspace trust decision
577
586
  // was read by nobody in this process.
578
587
  //
579
588
  // The ask seam is the trust gate wrapping the approval broker: a workspace
580
589
  // with no decision yet has the question raised as an approval record and
581
- // answered by whichever surface is attached (trust-gated-approvals.ts)
590
+ // answered by whichever surface is attached (trust-gated-approvals.ts),
582
591
  // there is no screen here to show a modal on, so the raise replaces it. The
583
- // manager's own layers permission mode, policy, session cache, durable
584
- // user rules still run first and are unchanged.
592
+ // manager's own layers, permission mode, policy, session cache, durable
593
+ // user rules, still run first and are unchanged.
585
594
  const permissionManager = createBrokeredPermissionManager({
586
595
  requestApproval: trustGatedApprovalRaiser(
587
596
  workspaceTrustManager,
@@ -648,7 +657,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
648
657
  // whole high-churn 'ops' domain, and sent over the SAME WebhookNotifier the
649
658
  // notification verbs keep live and boot-tasks attaches to the bus. There is
650
659
  // no panel notification router here: its targets are all screen targets and
651
- // this product has no screen see notification-dispatch.ts.
660
+ // this product has no screen, see notification-dispatch.ts.
652
661
  wireMemoryPressureChannelNotice(options.runtimeBus, webhookNotifier);
653
662
 
654
663
  // In-process config changes become key-level events on the `config` domain, so
@@ -780,7 +789,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
780
789
  // Cancels the agent runs this graph was hosting. By dispose() time the fleet
781
790
  // registry, orchestration engine, process registry and bus these runs report
782
791
  // through are already down, so a run still described as "running" is orphaned
783
- // rather than preserved and this is the only shutdown-reachable way to
792
+ // rather than preserved, and this is the only shutdown-reachable way to
784
793
  // abort its in-flight provider call instead of letting it sleep out a retry
785
794
  // backoff nobody is waiting on.
786
795
  cancelHostedAgentRuns: () => cancelAllAgentRuns(agentManager),
@@ -11,7 +11,7 @@ import type { ShellPathService } from '@/runtime/index.ts';
11
11
  *
12
12
  * This daemon composes the full family and feeds the manager to the fleet as
13
13
  * its trigger supervisor, so a trigger defined against the daemon fires
14
- * reliably. The daemon is the right process to own it it is the one that
14
+ * reliably. The daemon is the right process to own it, it is the one that
15
15
  * stays running.
16
16
  *
17
17
  * Two things about the shape are load-bearing: