@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
@@ -0,0 +1,112 @@
1
+ /**
2
+ * notifier.ts, sending a purchase notice over this daemon's channels.
3
+ *
4
+ * `createChannelPaymentNotifier` (platform/payments/notice-delivery.ts) wants
5
+ * a router, a target per configured channel, and a `PaymentReplySource`. The
6
+ * first two are real: `payments.notifyChannels` names which
7
+ * `CommandAuthorityChannel`s to notify, and delivery goes out over this
8
+ * daemon's own `ChannelDeliveryRouter`, the SAME router every other channel
9
+ * send in this daemon uses (see services.ts's comment on why there is exactly
10
+ * one).
11
+ *
12
+ * ── What is deliberately NOT wired in this pass ───────────────────────────
13
+ *
14
+ * `PaymentReplySource.waitForAnswer` always resolves `null`. That is not a
15
+ * stub standing in for something broken, it is the documented meaning of
16
+ * SILENCE (platform/payments/windows.ts): an approval window's silence DENIES
17
+ * and a veto window's silence PROCEEDS, both already correct, tested behaviors
18
+ * the decision layer exercises with no reply source at all. What is missing is
19
+ * the OTHER path, an inbound reply on a channel resolving the window before its
20
+ * deadline, "approve"/"yes"/"stop" arriving back from wherever the notice went.
21
+ * Building that needs an inbound-message correlation path this daemon does not
22
+ * have yet (there is no `payments.*` counterpart to
23
+ * `tryResolveApprovalReplyFromChannel`/`tryResolveWorkProposalReplyFromChannel`
24
+ * in `platform/daemon/surface-actions.ts`, which resolve DIFFERENT kinds of
25
+ * reply against a DIFFERENT store). Wiring it is a distinct, sizeable piece of
26
+ * work and is left for a later pass, exactly like `describeSubmission` above
27
+ * it; every purchase in the meantime is decided by budget and by the windows'
28
+ * own silence rules, with the notice actually reaching the owner's configured
29
+ * channels.
30
+ */
31
+ import type { ChannelDeliveryRouter, ChannelDeliveryTarget } from '@pellux/goodvibes-sdk/platform/channels';
32
+ import {
33
+ createChannelPaymentNotifier,
34
+ parseCommandAuthorityChannel,
35
+ readNotifyChannels,
36
+ } from '@pellux/goodvibes-sdk/platform/payments';
37
+ import type {
38
+ PaymentNotifier,
39
+ PaymentNoticeRouter,
40
+ PaymentNoticeTarget,
41
+ PaymentReplySource,
42
+ PaymentsConfigReader,
43
+ } from '@pellux/goodvibes-sdk/platform/payments';
44
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
45
+
46
+ const PAYMENTS_NOTICE_JOB_ID = 'payments-notice';
47
+
48
+ /**
49
+ * The channel name (`payments.notifyChannels` entry) turned into the router's
50
+ * own addressing shape. `parseChannelDeliveryTarget` (platform/channels'
51
+ * internal delivery/types.ts) is not on the published subpath, so this mirrors
52
+ * its `surface` construction for the plain channel names `readNotifyChannels`
53
+ * produces (no `kind:address` suffix, `CommandAuthorityChannel` carries none).
54
+ */
55
+ function surfaceTarget(surfaceKind: string): ChannelDeliveryTarget {
56
+ return { kind: 'surface', surfaceKind: surfaceKind as ChannelDeliveryTarget['surfaceKind'] };
57
+ }
58
+
59
+ /** Adapts this daemon's router to the notifier's narrow, opaque-`request` shape. */
60
+ function daemonNoticeRouter(router: Pick<ChannelDeliveryRouter, 'deliver'>): PaymentNoticeRouter {
61
+ return {
62
+ deliver: async (request) => {
63
+ const merged = request as unknown as Record<string, unknown> & { readonly content: string };
64
+ return router.deliver({
65
+ target: merged['target'] as ChannelDeliveryTarget,
66
+ body: merged.content,
67
+ title: 'Purchase',
68
+ jobId: PAYMENTS_NOTICE_JOB_ID,
69
+ runId: `${PAYMENTS_NOTICE_JOB_ID}-${String(Date.now())}`,
70
+ includeLinks: false,
71
+ });
72
+ },
73
+ };
74
+ }
75
+
76
+ /** No live reply integration yet; see this module's header for why silence is still correct. */
77
+ const NO_REPLIES: PaymentReplySource = {
78
+ async waitForAnswer() {
79
+ return null;
80
+ },
81
+ };
82
+
83
+ export function channelBackedPaymentNotifier(
84
+ config: PaymentsConfigReader,
85
+ router: Pick<ChannelDeliveryRouter, 'deliver'>,
86
+ ): PaymentNotifier {
87
+ const targets: PaymentNoticeTarget[] = [];
88
+ for (const name of readNotifyChannels(config)) {
89
+ const channel = parseCommandAuthorityChannel(name);
90
+ if (channel === null) {
91
+ logger.warn('payments.notifyChannels names a channel this daemon does not recognise; it will not be notified', { channel: name });
92
+ continue;
93
+ }
94
+ targets.push({
95
+ channel,
96
+ request: { target: surfaceTarget(name) },
97
+ // No backfill path is wired (see this module's header): a notice missed
98
+ // while the daemon was down cannot be recovered by re-reading history it
99
+ // never asked this router to keep.
100
+ backfillable: false,
101
+ });
102
+ }
103
+
104
+ return createChannelPaymentNotifier({
105
+ router: daemonNoticeRouter(router),
106
+ targets,
107
+ replies: NO_REPLIES,
108
+ onDeliveryFailure: ({ channel, reason }) => {
109
+ logger.warn('A payments notice could not be delivered on a configured channel', { channel, reason });
110
+ },
111
+ });
112
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * purchase-ledger.ts, the durable audit ledger behind `payments.purchases.list`.
3
+ *
4
+ * The SDK declares `PurchaseLedger` as a one-method write port
5
+ * (platform/payments/checkout-flow.ts) and ships no implementation, for the same
6
+ * reason it ships no card store: the flow must be drivable in a test with an
7
+ * in-memory ledger, so the durable one belongs to whoever actually runs it. This
8
+ * is that one.
9
+ *
10
+ * Append-only by construction. There is no update and no delete: the row is the
11
+ * evidence a purchase happened, and a ledger a later call can edit is not
12
+ * evidence of anything. A refund is recorded on the row as `refundedAt` by the
13
+ * flow that observes it, and credits no pool, see the descriptor.
14
+ *
15
+ * ── `merchantDiscovered` ──────────────────────────────────────────────────
16
+ *
17
+ * The control-plane view requires it and `PurchaseRecord` does not carry it, so
18
+ * a row written by a flow that did not state it reads as `false`, "the owner
19
+ * named this storefront". That is the conservative direction: `true` is the
20
+ * flag that says a storefront was found while browsing, and inventing it would
21
+ * put a claim in the audit trail that nothing observed.
22
+ */
23
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
24
+ import { dirname } from 'node:path';
25
+ import { atomicWriteFileSync } from '@pellux/goodvibes-sdk/platform/config';
26
+ import type { PurchaseLedger, PurchaseRecord } from '@pellux/goodvibes-sdk/platform/payments';
27
+
28
+ /** A stored row: the SDK record plus the one view field it does not declare. */
29
+ export interface StoredPurchase extends PurchaseRecord {
30
+ readonly merchantDiscovered: boolean;
31
+ }
32
+
33
+ const PURCHASES_FILE_VERSION = 1;
34
+
35
+ /** Hard ceiling on rows the list verb will hand back in one call. */
36
+ export const MAX_PURCHASE_LIST_LIMIT = 500;
37
+
38
+ interface PurchasesFile {
39
+ readonly version: number;
40
+ readonly purchases: readonly StoredPurchase[];
41
+ }
42
+
43
+ function readPurchasesFile(filePath: string): StoredPurchase[] {
44
+ if (!existsSync(filePath)) return [];
45
+ try {
46
+ const parsed = JSON.parse(readFileSync(filePath, 'utf-8')) as Partial<PurchasesFile>;
47
+ return Array.isArray(parsed.purchases) ? [...parsed.purchases] : [];
48
+ } catch {
49
+ // A torn ledger file reads as an empty one rather than taking the daemon
50
+ // down. It is reported as empty, not as an error, because every other verb
51
+ // on this daemon is unrelated to it and refusing to boot would take them
52
+ // all with it.
53
+ return [];
54
+ }
55
+ }
56
+
57
+ export interface DaemonPurchaseLedgerOptions {
58
+ readonly filePath: string;
59
+ }
60
+
61
+ export interface PurchaseListQuery {
62
+ readonly limit: number;
63
+ readonly dayKey: string | undefined;
64
+ }
65
+
66
+ /**
67
+ * The daemon's purchase ledger.
68
+ *
69
+ * Implements the SDK's `PurchaseLedger` write port, so the checkout flow can be
70
+ * handed this object unchanged once `payments.checkout.begin` has a page driver
71
+ * to run against, and adds the read half the `payments.purchases.list` verb needs.
72
+ */
73
+ export class DaemonPurchaseLedger implements PurchaseLedger {
74
+ private readonly filePath: string;
75
+
76
+ constructor(options: DaemonPurchaseLedgerOptions) {
77
+ this.filePath = options.filePath;
78
+ }
79
+
80
+ /** Append one purchase. The only write this class performs. */
81
+ async record(entry: PurchaseRecord): Promise<void> {
82
+ const discovered = (entry as { merchantDiscovered?: unknown }).merchantDiscovered;
83
+ const row: StoredPurchase = { ...entry, merchantDiscovered: discovered === true };
84
+ const rows = [...readPurchasesFile(this.filePath), row];
85
+ mkdirSync(dirname(this.filePath), { recursive: true });
86
+ const contents: PurchasesFile = { version: PURCHASES_FILE_VERSION, purchases: rows };
87
+ atomicWriteFileSync(this.filePath, `${JSON.stringify(contents, null, 2)}\n`, { mode: 0o600 });
88
+ }
89
+
90
+ /**
91
+ * The most recent purchases first, optionally narrowed to one calendar day.
92
+ *
93
+ * `total` is the number of rows matching the FILTER, not the number returned,
94
+ * so a surface showing 100 of 340 can say so instead of implying there are
95
+ * only 100.
96
+ */
97
+ list(query: PurchaseListQuery): { purchases: readonly StoredPurchase[]; total: number } {
98
+ const rows = readPurchasesFile(this.filePath);
99
+ const matching = query.dayKey === undefined
100
+ ? rows
101
+ : rows.filter((row) => row.dayKey === query.dayKey);
102
+ const limit = Math.min(Math.max(1, Math.floor(query.limit)), MAX_PURCHASE_LIST_LIMIT);
103
+ return {
104
+ purchases: [...matching].reverse().slice(0, limit),
105
+ total: matching.length,
106
+ };
107
+ }
108
+ }