@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.
- package/CHANGELOG.md +119 -63
- package/README.md +24 -19
- package/bin/launcher-support.js +5 -5
- package/package.json +6 -4
- package/scripts/postinstall.js +8 -8
- package/src/cli/command-catalog.ts +22 -22
- package/src/cli/completion.ts +4 -4
- package/src/cli/help.ts +5 -5
- package/src/cli/index.ts +3 -3
- package/src/cli/parser.ts +2 -2
- package/src/cli/surface-catalog.ts +1 -1
- package/src/cli/types.ts +2 -2
- package/src/cluster/daemon-ws-call.ts +5 -5
- package/src/cluster/raw-reply-route.ts +5 -5
- package/src/config/checkpoint-settings.ts +7 -7
- package/src/config/config-key-guard.ts +22 -0
- package/src/config/run-daemon-config-migration.ts +3 -3
- package/src/config/secret-config.ts +7 -7
- package/src/config/surface.ts +3 -3
- package/src/core/pairing-banner.ts +5 -5
- package/src/daemon/cli.ts +45 -43
- package/src/daemon/config-command.ts +15 -15
- package/src/daemon/handlers/context.ts +1 -1
- package/src/daemon/handlers/contracts.ts +19 -4
- package/src/daemon/handlers/credentials.ts +1 -1
- package/src/daemon/handlers/drafts/draft-store.ts +3 -3
- package/src/daemon/handlers/drafts/register.ts +4 -4
- package/src/daemon/handlers/inbox/aggregator.ts +8 -8
- package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
- package/src/daemon/handlers/inbox/index.ts +7 -7
- package/src/daemon/handlers/inbox/mapping.ts +2 -2
- package/src/daemon/handlers/inbox/poller.ts +5 -5
- package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
- package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
- package/src/daemon/handlers/inbox/providers/email.ts +3 -3
- package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
- package/src/daemon/handlers/index.ts +18 -8
- package/src/daemon/handlers/payments/address-store.ts +54 -0
- package/src/daemon/handlers/payments/budget-store.ts +356 -0
- package/src/daemon/handlers/payments/card-store.ts +486 -0
- package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
- package/src/daemon/handlers/payments/index.ts +38 -0
- package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
- package/src/daemon/handlers/payments/notifier.ts +112 -0
- package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
- package/src/daemon/handlers/payments/register.ts +518 -0
- package/src/daemon/handlers/register.ts +3 -3
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
- package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
- package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
- package/src/daemon/handlers/remote/backends/types.ts +2 -2
- package/src/daemon/handlers/remote/dispatcher.ts +3 -3
- package/src/daemon/handlers/remote/index.ts +1 -1
- package/src/daemon/handlers/remote/peer-registry.ts +62 -13
- package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
- package/src/daemon/handlers/routing/index.ts +1 -1
- package/src/daemon/handlers/routing/route-store.ts +1 -1
- package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
- package/src/daemon/handlers/sqlite-store.ts +9 -9
- package/src/daemon/handlers/triage/index.ts +1 -1
- package/src/daemon/handlers/triage/integration.ts +3 -3
- package/src/daemon/handlers/triage/pipeline.ts +2 -2
- package/src/daemon/handlers/triage/scorer.ts +2 -2
- package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
- package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
- package/src/daemon/handlers/triage/tagger/index.ts +1 -1
- package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
- package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
- package/src/daemon/handlers/triage/types.ts +2 -2
- package/src/daemon/lifecycle.ts +5 -5
- package/src/daemon/local-daemon-state.ts +7 -7
- package/src/daemon/pair-command.ts +14 -14
- package/src/daemon/provision-wake-model.ts +5 -5
- package/src/daemon/send/channels.ts +7 -7
- package/src/daemon/send/command.ts +11 -11
- package/src/daemon/send/composition.ts +5 -5
- package/src/daemon/send/failure-text.ts +6 -6
- package/src/daemon/send/inert-text.ts +18 -18
- package/src/daemon/send/stdin.ts +3 -3
- package/src/daemon/service-commands.ts +32 -32
- package/src/daemon/sessions-command.ts +7 -7
- package/src/daemon/status-command.ts +22 -22
- package/src/daemon/webui-command.ts +14 -14
- package/src/runtime/boot-tasks.ts +1 -1
- package/src/runtime/browser-checkout-seam-holder.ts +55 -0
- package/src/runtime/cluster-composition.ts +9 -9
- package/src/runtime/cluster-group-composition.ts +7 -7
- package/src/runtime/conversation-rewind-port.ts +8 -8
- package/src/runtime/credential-composition.ts +2 -2
- package/src/runtime/daemon-handler-composition.ts +61 -4
- package/src/runtime/device-posture-composition.ts +10 -10
- package/src/runtime/disposal-wiring.ts +8 -8
- package/src/runtime/fleet-needs-input-push.ts +4 -4
- package/src/runtime/fleet-services.ts +1 -1
- package/src/runtime/hosted-session-composition.ts +13 -13
- package/src/runtime/index.ts +1 -1
- package/src/runtime/knowledge-services.ts +2 -2
- package/src/runtime/legacy-daemon-migration.ts +43 -43
- package/src/runtime/legacy-daemon-reconcile.ts +30 -30
- package/src/runtime/mail-composition.ts +6 -6
- package/src/runtime/notification-dispatch.ts +7 -7
- package/src/runtime/payments-composition.ts +187 -0
- package/src/runtime/plugin-composition.ts +7 -7
- package/src/runtime/runtime-services-types.ts +9 -9
- package/src/runtime/services.ts +41 -32
- package/src/runtime/trigger-services.ts +1 -1
- package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
- package/src/runtime/trust/trust-gated-approvals.ts +9 -9
- package/src/runtime/update-check.ts +4 -4
- package/src/runtime/workspace-checkpointing.ts +6 -6
- package/src/testing/daemon-fixture.ts +11 -11
- package/src/testing/hosted-session-failures.ts +4 -4
- package/src/version.ts +2 -2
|
@@ -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
|
+
}
|