@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cluster-composition.ts
|
|
2
|
+
* cluster-composition.ts, this daemon's seat in the LAN leader election.
|
|
3
3
|
*
|
|
4
|
-
* When the same goodvibes install runs more than once on one network
|
|
5
|
-
* laptop and a desktop, or two processes on one machine
|
|
4
|
+
* When the same goodvibes install runs more than once on one network, a
|
|
5
|
+
* laptop and a desktop, or two processes on one machine, every copy
|
|
6
6
|
* independently reads the shared inbox, so one message is picked up twice and
|
|
7
7
|
* answered twice. The SDK's cluster coordinator elects exactly one node to be
|
|
8
8
|
* responsible for inbound consumption; everything else stays warm and silent.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* This file exists because this daemon does NOT get that for free from the
|
|
11
11
|
* SDK facade. The facade gates the consumers IT owns (Telegram
|
|
12
12
|
* ingress, the ntfy/Slack/Discord provider runtime), but this repository
|
|
13
|
-
* composes an inbound consumer of its own
|
|
14
|
-
* poller in daemon/handlers/inbox
|
|
13
|
+
* composes an inbound consumer of its own, the Slack/Discord/email inbox
|
|
14
|
+
* poller in daemon/handlers/inbox, and the facade knows nothing about it.
|
|
15
15
|
* A fix that only landed in the facade would leave the poller double-reading
|
|
16
16
|
* on exactly the machine this product runs on.
|
|
17
17
|
*
|
|
@@ -51,7 +51,7 @@ export interface GatedPollerControl {
|
|
|
51
51
|
* Constructing it is inert: no socket is opened and no state is written until
|
|
52
52
|
* `start()` runs, so composing a runtime in a test never joins a network.
|
|
53
53
|
*
|
|
54
|
-
* The version is THIS BINARY's version, not the SDK package's
|
|
54
|
+
* The version is THIS BINARY's version, not the SDK package's, the same
|
|
55
55
|
* distinction daemon/cli.ts already makes for the self-update artifact. It is
|
|
56
56
|
* the first ranking tier, so getting it wrong would let a stale build hold the
|
|
57
57
|
* role through an update.
|
|
@@ -62,7 +62,7 @@ export function createClusterComposition(options: {
|
|
|
62
62
|
* `readClusterSettings` asks for. The daemon passes a whole ConfigManager;
|
|
63
63
|
* the handler context carries only this slice, and a test standing in a
|
|
64
64
|
* minimal config object should not have to fabricate the full fifty-odd
|
|
65
|
-
* members to compose a coordinator
|
|
65
|
+
* members to compose a coordinator, a cast there would hide a real shape
|
|
66
66
|
* mismatch rather than document one.
|
|
67
67
|
*/
|
|
68
68
|
readonly configManager: Pick<ConfigManager, 'getCategory'>;
|
|
@@ -75,7 +75,7 @@ export function createClusterComposition(options: {
|
|
|
75
75
|
* the current group key. That is what stops a daemon belonging to somebody
|
|
76
76
|
* else on the same network from taking part in this election at all.
|
|
77
77
|
*
|
|
78
|
-
* Absent
|
|
78
|
+
* Absent, the plain default, means the coordinator opens its own socket and
|
|
79
79
|
* coordinates with anything that answers on the configured port.
|
|
80
80
|
*/
|
|
81
81
|
readonly transport?: ClusterTransport | undefined;
|
|
@@ -96,7 +96,7 @@ export function createClusterComposition(options: {
|
|
|
96
96
|
*
|
|
97
97
|
* One gate per account, not one for the poller. Each inbox account is its own
|
|
98
98
|
* surface in the election, so this laptop can hold the work Slack account
|
|
99
|
-
* while the desktop holds the mailbox
|
|
99
|
+
* while the desktop holds the mailbox, and losing one machine moves only the
|
|
100
100
|
* accounts it was reading. A single gate covering the whole poller could not
|
|
101
101
|
* express that: it would hand every account over together, and a node with a
|
|
102
102
|
* credential for only one of them could never take part at all.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cluster-group-composition.ts
|
|
2
|
+
* cluster-group-composition.ts, this machine's membership of a LAN group.
|
|
3
3
|
*
|
|
4
4
|
* Leader election answers "which of us reads the inbox". This answers the
|
|
5
5
|
* question underneath it: "which machines are US". Without a group, any daemon
|
|
6
|
-
* that happened to be on the same network
|
|
7
|
-
* container someone left running
|
|
6
|
+
* that happened to be on the same network, a neighbour's, a colleague's, a
|
|
7
|
+
* container someone left running, would join the same coordination and one of
|
|
8
8
|
* you would silently stop receiving messages.
|
|
9
9
|
*
|
|
10
10
|
* The group layer owns the socket and the election rides on it: every
|
|
@@ -77,7 +77,7 @@ export function createClusterGroupComposition(options: {
|
|
|
77
77
|
* The leader election's own answer to "am I the master".
|
|
78
78
|
*
|
|
79
79
|
* Config replication needs exactly one machine issuing revisions, and it must
|
|
80
|
-
* be the SAME machine leadership already picked
|
|
80
|
+
* be the SAME machine leadership already picked, two notions of master in
|
|
81
81
|
* one process would disagree the moment one of them changed.
|
|
82
82
|
*
|
|
83
83
|
* Late-bound because the coordinator is built from this composition's
|
|
@@ -91,7 +91,7 @@ export function createClusterGroupComposition(options: {
|
|
|
91
91
|
* in `cluster status` and in /status. Late-bound for the same reason
|
|
92
92
|
* `isMaster` is. Absent means the group layer reports that the information is
|
|
93
93
|
* unavailable rather than reporting an empty list as though this machine held
|
|
94
|
-
* nothing
|
|
94
|
+
* nothing, a distinction an operator diagnosing a silent inbox depends on.
|
|
95
95
|
*/
|
|
96
96
|
readonly surfaceHoldings?: (() => readonly ClusterSurfaceHolding[]) | undefined;
|
|
97
97
|
}): ClusterGroupComposition {
|
|
@@ -181,7 +181,7 @@ export function createClusterGroupComposition(options: {
|
|
|
181
181
|
* through a closure because the coordinator does not exist yet when the group
|
|
182
182
|
* layer is constructed.
|
|
183
183
|
*
|
|
184
|
-
* Constructing either is inert
|
|
184
|
+
* Constructing either is inert, no socket, no key material read, until
|
|
185
185
|
* `startCluster` runs.
|
|
186
186
|
*/
|
|
187
187
|
export function createClusterServices(options: {
|
|
@@ -228,7 +228,7 @@ export async function startClusterServices(services: {
|
|
|
228
228
|
* Ask the group to take this machine back, on start.
|
|
229
229
|
*
|
|
230
230
|
* This is the zero-touch return. A machine that has been switched off for
|
|
231
|
-
* months
|
|
231
|
+
* months, through many group-key rotations and possibly a join-key change,
|
|
232
232
|
* comes up holding stale keys, says "it is still me" with a key that never
|
|
233
233
|
* rotates, and is re-keyed to the current generation by whichever member
|
|
234
234
|
* answers. The operator does nothing.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// conversation-rewind-port.ts
|
|
2
|
+
// conversation-rewind-port.ts, the daemon's RewindConversationPort for the
|
|
3
3
|
// SDK's unified rewind service.
|
|
4
4
|
//
|
|
5
5
|
// The SDK's UnifiedRewindService (platform/rewind) joins files rewind (workspace
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// truncation and captures the pre-/post-truncation snapshots so the reversal can
|
|
10
10
|
// be undone and re-applied. The truncation boundary is the message count recorded
|
|
11
11
|
// for the anchor's turnId at TURN_COMPLETED (the SDK's platform/rewind turn
|
|
12
|
-
// anchors)
|
|
12
|
+
// anchors), the same
|
|
13
13
|
// join key files rewind uses against the workspace checkpoint.
|
|
14
14
|
//
|
|
15
15
|
// This module is the daemon's implementation of that port, for sessions THIS
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// anchor.sessionId from the registry below.
|
|
18
18
|
//
|
|
19
19
|
// While conversation loops run in the surfaces, that registry is empty here, and
|
|
20
|
-
// it used to answer an empty resolution with "0 messages to drop"
|
|
20
|
+
// it used to answer an empty resolution with "0 messages to drop", the same
|
|
21
21
|
// answer a conversation already at the anchor gives, which is a confident wrong
|
|
22
22
|
// answer rather than a missing one. It now reports the anchor as UNAVAILABLE
|
|
23
23
|
// with the reason, which the SDK's rewind service turns into a plan warning.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// The surfaces reach conversation rewind a different way now: they offer their
|
|
26
26
|
// live conversation over the control plane (rewind.conversation.*), and the
|
|
27
27
|
// SDK's host broker asks them directly. This port is the fallback the broker
|
|
28
|
-
// falls through to for sessions no surface has offered
|
|
28
|
+
// falls through to for sessions no surface has offered, which is exactly the
|
|
29
29
|
// case where the daemon hosts the conversation itself. Files-scope rewind,
|
|
30
30
|
// entirely daemon-owned, was never affected either way.
|
|
31
31
|
//
|
|
@@ -65,7 +65,7 @@ export interface ConversationRewindPort extends RewindConversationPort {
|
|
|
65
65
|
restoreAfter(undoSnapshotId: string): boolean;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** One truncation's captured state
|
|
68
|
+
/** One truncation's captured state, the target conversation and its snapshots. */
|
|
69
69
|
interface SnapshotPair {
|
|
70
70
|
readonly conv: RewindableConversation;
|
|
71
71
|
readonly before: ConversationJson;
|
|
@@ -80,7 +80,7 @@ const NO_LIVE_CONVERSATION =
|
|
|
80
80
|
* Build a conversation rewind port. `resolveConversation` maps an anchor's
|
|
81
81
|
* sessionId to the live conversation: the daemon looks the session up in the
|
|
82
82
|
* registry below. A null resolution is reported as unavailable with the reason
|
|
83
|
-
|
|
83
|
+
*, never as a count, because "nobody here is holding those messages" and
|
|
84
84
|
* "there are no messages to drop" are different facts and only one of them is
|
|
85
85
|
* true.
|
|
86
86
|
*/
|
|
@@ -139,7 +139,7 @@ export function createConversationRewindPort(
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// ---------------------------------------------------------------------------
|
|
142
|
-
// Live per-session conversation registry
|
|
142
|
+
// Live per-session conversation registry, the daemon-hosted mutable store the
|
|
143
143
|
// composed daemon's rewind.plan/apply verbs fall back to. A process INSIDE this
|
|
144
144
|
// daemon that runs a conversation registers it here; a surface in another
|
|
145
145
|
// process offers its conversation over the control plane instead
|
|
@@ -162,7 +162,7 @@ export function unregisterSessionConversation(sessionId: string): void {
|
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* The conversation rewind port the composed daemon threads into
|
|
165
|
-
* registerGatewayVerbGroups
|
|
165
|
+
* registerGatewayVerbGroups, it resolves each anchor's live conversation from
|
|
166
166
|
* the registry above, so the daemon's own rewind verbs serve conversation scope
|
|
167
167
|
* live in this process.
|
|
168
168
|
*/
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* These are grouped because they share one fact, and it is the fact this round
|
|
8
8
|
* had to fix: WHERE the daemon's own credentials live. `daemonHome` is threaded
|
|
9
9
|
* rather than defaulted, because without it a daemon told to run out of a temp
|
|
10
|
-
* tree moved its identity directory and nothing else
|
|
10
|
+
* tree moved its identity directory and nothing else, the credential store
|
|
11
11
|
* stayed in the real home, so an "isolated" test daemon held live credentials
|
|
12
12
|
* and long-polled a real account. A default here would silently restore that.
|
|
13
13
|
*
|
|
@@ -25,7 +25,7 @@ interface CredentialCompositionInput {
|
|
|
25
25
|
readonly homeDirectory: string;
|
|
26
26
|
/**
|
|
27
27
|
* The daemon's state root when the host was told one. Absent means "not
|
|
28
|
-
* overridden" and must stay absent rather than being defaulted
|
|
28
|
+
* overridden" and must stay absent rather than being defaulted, see above.
|
|
29
29
|
*/
|
|
30
30
|
readonly daemonHomeDirectory?: string | undefined;
|
|
31
31
|
readonly configManager: ConstructorParameters<typeof SecretsManager>[0]['configManager'];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* daemon-handler-composition.ts
|
|
2
|
+
* daemon-handler-composition.ts, the daemon's HOST-side handler surfaces.
|
|
3
3
|
*
|
|
4
4
|
* Attaches this repository's handlers to the SDK-auto-registered builtin
|
|
5
5
|
* gateway descriptors (channels.* / email.* / calendar.*) via
|
|
6
|
-
* catalog.register(descriptor, handler, { replace: true })
|
|
6
|
+
* catalog.register(descriptor, handler, { replace: true }), the SDK owns
|
|
7
7
|
* every id, descriptor and schema; only the behaviour is ours. The remote
|
|
8
8
|
* surface reuses the SAME DistributedRuntimeManager the SDK facade injects.
|
|
9
9
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* The one behavioural decision that lives here: the inbox poller is handed to
|
|
14
14
|
* the cluster coordinator instead of being started eagerly. It is this
|
|
15
|
-
* product's own inbound consumer
|
|
15
|
+
* product's own inbound consumer, the SDK facade does not know it exists,
|
|
16
16
|
* so if it is not gated here it is not gated anywhere, and two goodvibes nodes
|
|
17
17
|
* on one network each read the shared inbox and answer the same message twice.
|
|
18
18
|
*/
|
|
@@ -20,6 +20,8 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
20
20
|
import type { ClusterCoordinator } from '@pellux/goodvibes-sdk/platform/cluster';
|
|
21
21
|
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
22
22
|
import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
23
|
+
import type { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
|
|
24
|
+
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
23
25
|
import { registerDaemonHandlers, type DaemonHandlerSurfaces } from '../daemon/handlers/index.ts';
|
|
24
26
|
import type { HandlerContext, HandlerLogger } from '../daemon/handlers/context.ts';
|
|
25
27
|
import { createDaemonCredentialStore } from '../daemon/handlers/credentials.ts';
|
|
@@ -28,7 +30,10 @@ import { registerInboxMethods } from '../daemon/handlers/inbox/index.ts';
|
|
|
28
30
|
import { registerTriagedInbox } from '../daemon/handlers/triage/index.ts';
|
|
29
31
|
import { registerDraftMethods } from '../daemon/handlers/drafts/index.ts';
|
|
30
32
|
import { registerRemoteSurface } from '../daemon/handlers/remote/index.ts';
|
|
33
|
+
import { createPaymentsServices } from './payments-composition.ts';
|
|
31
34
|
import { inboxPollerGate } from './cluster-composition.ts';
|
|
35
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
36
|
+
import type { BrowserCheckoutSeamHolder } from './browser-checkout-seam-holder.ts';
|
|
32
37
|
|
|
33
38
|
export interface DaemonHandlerCompositionOptions {
|
|
34
39
|
readonly gatewayMethods: GatewayMethodCatalog;
|
|
@@ -36,12 +41,23 @@ export interface DaemonHandlerCompositionOptions {
|
|
|
36
41
|
readonly configManager: ConfigManager;
|
|
37
42
|
readonly workingDirectory: string;
|
|
38
43
|
readonly homeDirectory: string;
|
|
44
|
+
/** Resolves the surface-scoped control-plane paths the payment stores live at. */
|
|
45
|
+
readonly shellPaths: ShellPathService;
|
|
39
46
|
readonly distributedRuntime: NonNullable<Parameters<typeof registerRemoteSurface>[1]>['manager'];
|
|
40
47
|
/**
|
|
41
48
|
* Decides whether THIS node polls the shared inbox. Always supplied by the
|
|
42
49
|
* composition root; the poller is never started outside it.
|
|
43
50
|
*/
|
|
44
51
|
readonly clusterCoordinator: ClusterCoordinator;
|
|
52
|
+
/**
|
|
53
|
+
* Where `payments.checkout.*` reads the browser-checkout seam, filled later
|
|
54
|
+
* by services.ts's own `onBrowserCheckout`. See
|
|
55
|
+
* runtime/browser-checkout-seam-holder.ts and payments-composition.ts's
|
|
56
|
+
* header for why this has to be a getter rather than the seam itself.
|
|
57
|
+
*/
|
|
58
|
+
readonly checkoutSeam: BrowserCheckoutSeamHolder['get'];
|
|
59
|
+
readonly channelDeliveryRouter: Pick<ChannelDeliveryRouter, 'deliver'>;
|
|
60
|
+
readonly providerRegistry: Pick<ProviderRegistry, 'getCurrentModel' | 'getForModel'>;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
export function createDaemonHandlerComposition(
|
|
@@ -65,12 +81,53 @@ export function createDaemonHandlerComposition(
|
|
|
65
81
|
registerInbox: (ctx, routing) =>
|
|
66
82
|
registerTriagedInbox(ctx, (inboxCtx) => registerInboxMethods(inboxCtx, routing, {
|
|
67
83
|
// Hands polling to leadership. The `channels.inbox.list` read stays
|
|
68
|
-
// available on every node
|
|
84
|
+
// available on every node, a standby still SERVES the persisted feed,
|
|
69
85
|
// it just does not FETCH into it.
|
|
70
86
|
gatePolling: (providerId, control) =>
|
|
71
87
|
options.clusterCoordinator.register(inboxPollerGate(providerId, control)),
|
|
72
88
|
})).unregister,
|
|
73
89
|
registerDrafts: (ctx) => registerDraftMethods(ctx),
|
|
90
|
+
// The payment stores need a path resolver and a daemon-scoped secret writer,
|
|
91
|
+
// neither of which is on HandlerContext, so they are built here and the
|
|
92
|
+
// provider only carries the teardown. See payments-composition.ts for the
|
|
93
|
+
// full checkout composition (address store, notifier, merchant judge,
|
|
94
|
+
// browser-checkout seam).
|
|
95
|
+
registerPayments: () => createPaymentsServices({
|
|
96
|
+
gatewayMethods: options.gatewayMethods,
|
|
97
|
+
configManager: options.configManager,
|
|
98
|
+
secretsManager: options.secretsManager,
|
|
99
|
+
shellPaths: options.shellPaths,
|
|
100
|
+
// True only on a machine that is not clustered at all, and false on every
|
|
101
|
+
// node of a cluster.
|
|
102
|
+
//
|
|
103
|
+
// `isMaster` was the obvious answer and it is the wrong one: it means
|
|
104
|
+
// "this node holds at least one inbound surface", which two nodes sharing
|
|
105
|
+
// a mailbox and a Slack workspace both satisfy, so a two-node cluster
|
|
106
|
+
// would have answered true twice. The SDK's gates.ts is explicit that
|
|
107
|
+
// exactly one node may act and that the wrong answer here is a
|
|
108
|
+
// double-spend.
|
|
109
|
+
//
|
|
110
|
+
// The alternative was to register a payments surface with the coordinator
|
|
111
|
+
// and read holdsSurface(), which is this repo's per-surface idiom
|
|
112
|
+
// (inboxPollerGate). It is the right answer once there is something to
|
|
113
|
+
// elect over, and today there is not: ClusterConsumerGate.start() is
|
|
114
|
+
// specified to not resolve until consumption has actually begun, and this
|
|
115
|
+
// composition has no payments consumer to start it. Checkout is now
|
|
116
|
+
// wired (payments.checkout.begin/.fillCard), but registering a gate whose
|
|
117
|
+
// start() does nothing would STILL put a fake consumer in the election and
|
|
118
|
+
// in `cluster status`; a real election is a separate piece of work, left
|
|
119
|
+
// for a later pass, not something wiring the checkout pair itself needed.
|
|
120
|
+
//
|
|
121
|
+
// So the honest reading of the topology stays what it was: clustering off
|
|
122
|
+
// means this is the only node, and it is trivially the one that would
|
|
123
|
+
// spend; clustering on means no payments election has been held and this
|
|
124
|
+
// node cannot claim to have won it. False on every node is also the safe
|
|
125
|
+
// direction, checkPaymentGates refuses on false.
|
|
126
|
+
isPaymentsLeader: () => !options.clusterCoordinator.enabled,
|
|
127
|
+
checkoutSeam: options.checkoutSeam,
|
|
128
|
+
channelDeliveryRouter: options.channelDeliveryRouter,
|
|
129
|
+
providerRegistry: options.providerRegistry,
|
|
130
|
+
}).unregister,
|
|
74
131
|
registerRemote: (ctx) => registerRemoteSurface(ctx, { manager: options.distributedRuntime }),
|
|
75
132
|
});
|
|
76
133
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* device-posture-composition.ts
|
|
2
|
+
* device-posture-composition.ts, the paired-phone feature inside THIS daemon.
|
|
3
3
|
*
|
|
4
4
|
* A phone pairs with whichever daemon the person runs. Until this module
|
|
5
5
|
* existed this daemon had no device posture at all: `device.nodes.maxPaired`
|
|
6
6
|
* was enforced at the pairing path (SDK-side) and `device.capabilities.mode`
|
|
7
7
|
* was an onboarding toggle, while the other eleven `device.*` keys were
|
|
8
|
-
* recorded, read back, and governed nothing
|
|
8
|
+
* recorded, read back, and governed nothing, the capability service they
|
|
9
9
|
* describe was never built here, so there was nothing for them to govern.
|
|
10
10
|
*
|
|
11
11
|
* The feature itself is platform-owned (`platform/devices`): the settings→policy
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
* confirmation flow, and the `phone` tool. This module supplies the three seams
|
|
14
14
|
* that are actually ours and nothing else:
|
|
15
15
|
*
|
|
16
|
-
* - the peer transport
|
|
16
|
+
* - the peer transport, the same DistributedRuntimeManager the remote surface
|
|
17
17
|
* pairs devices onto, so a phone paired here is a node here,
|
|
18
|
-
* - the approval path
|
|
18
|
+
* - the approval path, the shared approval broker, so the confirmation
|
|
19
19
|
* appears wherever the person is looking (terminal, web app, companion),
|
|
20
20
|
* - the storage root and the live config manager.
|
|
21
21
|
*
|
|
22
22
|
* Constructing this starts nothing. `startHousekeeping()` is called from the
|
|
23
23
|
* bootstrap tail: grants and captures both outlive a restart, so a grant whose
|
|
24
24
|
* phone is gone, or a capture torn by a crash, is reaped BEFORE the first
|
|
25
|
-
* request of this run is served
|
|
25
|
+
* request of this run is served, and the periodic sweep after it is what keeps
|
|
26
26
|
* a long-running daemon from going days without one. A failed sweep is logged
|
|
27
27
|
* and the app still runs: housekeeping failing is a reason to say so, not a
|
|
28
28
|
* reason to refuse to start.
|
|
@@ -52,8 +52,8 @@ export interface DevicePostureCompositionOptions {
|
|
|
52
52
|
/**
|
|
53
53
|
* Binding the catalog turns the whole devices.* family from
|
|
54
54
|
* cataloged-but-unhandled into real handlers: nodes.list, grants.*,
|
|
55
|
-
* housekeeping.run, and
|
|
56
|
-
* could read the grants and never open a camera
|
|
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
57
|
* artifacts.list/read. That is what makes the paired phone usable from the web
|
|
58
58
|
* app and the companion rather than only through this process's own tool.
|
|
59
59
|
*
|
|
@@ -85,8 +85,8 @@ export function createDevicePostureServices(options: DevicePostureCompositionOpt
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Everything a host with a tool registry has to do once it is up: register the
|
|
88
|
-
* `phone` tool
|
|
89
|
-
* the posture keys govern nothing a session can observe
|
|
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
90
|
*
|
|
91
91
|
* One call, because these two belong to the same feature and a host that did the
|
|
92
92
|
* first and forgot the second would serve requests while never reaping a grant
|
|
@@ -103,7 +103,7 @@ export function installDevicePosture(
|
|
|
103
103
|
/**
|
|
104
104
|
* The recovery sweep plus the periodic timer. Separate from construction so
|
|
105
105
|
* composing a runtime in a test starts no timer and touches no disk, and
|
|
106
|
-
* separate from the call above so this daemon
|
|
106
|
+
* separate from the call above so this daemon, which registers no tools,
|
|
107
107
|
* still sweeps.
|
|
108
108
|
*/
|
|
109
109
|
export function startDeviceHousekeeping(devicePosture: DevicePostureRuntime): void {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Teardown for every poller the daemon's runtime graph starts.
|
|
5
5
|
*
|
|
6
|
-
* The mechanics
|
|
7
|
-
* all-required owner list
|
|
6
|
+
* The mechanics, the ordered, best-effort, idempotent scope and the
|
|
7
|
+
* all-required owner list, live in the SDK (`platform/runtime/disposal`), so
|
|
8
8
|
* the daemon and the SDK's own composition root cannot drift into two different
|
|
9
9
|
* ideas of what "stop the graph" means. What lives here is only the mapping from
|
|
10
10
|
* the daemon's assembled graph onto that list, plus the four pollers the daemon
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Ownership note, and the reason any of this exists: this product builds the
|
|
20
20
|
* graph and hands the SAME object to `DaemonServer`. The SDK facade disposes
|
|
21
21
|
* only a graph it constructed itself, so it deliberately leaves this one alone
|
|
22
|
-
|
|
22
|
+
*, nothing upstream will ever stop these pollers. The shutdown paths in
|
|
23
23
|
* daemon/cli.ts and the one-shot CLI commands are the only things that can.
|
|
24
24
|
*
|
|
25
25
|
* The owner type below is declared structurally rather than imported from
|
|
@@ -44,7 +44,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
|
|
|
44
44
|
/**
|
|
45
45
|
* Daemon-only: the wake-word recovery sweep and a pending boot provision
|
|
46
46
|
* Started only when an entrypoint opted into boot
|
|
47
|
-
* provisioning, and a no-op otherwise
|
|
47
|
+
* provisioning, and a no-op otherwise, but it is on this list unconditionally,
|
|
48
48
|
* because "the graph did not start it this time" is not a reason for the
|
|
49
49
|
* teardown path to have no way to stop it.
|
|
50
50
|
*/
|
|
@@ -53,7 +53,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
|
|
|
53
53
|
* Daemon-only: the product handler surfaces (daemon-handler-composition.ts).
|
|
54
54
|
*
|
|
55
55
|
* `unregister()` detaches the gateway handlers AND stops the two pollers this
|
|
56
|
-
* product's inbox surface owns
|
|
56
|
+
* product's inbox surface owns, the retention sweep inside `InboxCursorStore`
|
|
57
57
|
* and the per-provider `InboundPoller` intervals. The SDK does not know either
|
|
58
58
|
* exists, so if this surface does not stop them nothing does.
|
|
59
59
|
*/
|
|
@@ -68,7 +68,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* The poller owners this daemon holds that are NOT reachable from the
|
|
71
|
-
* assembled graph
|
|
71
|
+
* assembled graph, handles the factory keeps as locals.
|
|
72
72
|
*/
|
|
73
73
|
export interface RuntimeDisposalExtras {
|
|
74
74
|
/** Handle returned by `ConfigManager.watchConfigFiles()`. */
|
|
@@ -79,7 +79,7 @@ export interface RuntimeDisposalExtras {
|
|
|
79
79
|
* Register the stop call for every poller the graph started.
|
|
80
80
|
*
|
|
81
81
|
* `services` is the fully-assembled graph, which already exposes each poller
|
|
82
|
-
* owner as a field
|
|
82
|
+
* owner as a field, so a poller whose owner reaches the public surface is
|
|
83
83
|
* wired by name rather than by threading another local out of the factory.
|
|
84
84
|
*/
|
|
85
85
|
export function registerDaemonRuntimePollers(
|
|
@@ -93,7 +93,7 @@ export function registerDaemonRuntimePollers(
|
|
|
93
93
|
registry.add('wake-word housekeeping', services.stopWakeHousekeeping);
|
|
94
94
|
// Registered LAST so it tears down FIRST (the scope unwinds in reverse), which
|
|
95
95
|
// is the order daemon/cli.ts already used by hand: release the handler surfaces
|
|
96
|
-
|
|
96
|
+
//, closing the inbox store and stopping its poll timers, before the pollers
|
|
97
97
|
// the rest of the graph owns. Being on this list is what makes a plain
|
|
98
98
|
// `dispose()` total: every shutdown path stops these, not just the one that
|
|
99
99
|
// remembered to call `unregister()` itself.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fleet-needs-input-push.ts
|
|
2
|
+
* fleet-needs-input-push.ts, wires the fleet "needs-input" browser-push fan-out
|
|
3
3
|
* that the SDK's registerGatewayVerbGroups (reached through the terminal-shell's
|
|
4
4
|
* attachWsOnlyGatewayVerbHandlers wrapper) gates on two deps: `runtimeBus` and
|
|
5
5
|
* `sessionPresence` (see push/service.ts's attachFleetNeedsInputSource). When a
|
|
6
6
|
* fleet node blocks on the operator, it pushes an "Input needed" notification
|
|
7
|
-
* carrying the session/node deep link to every registered device
|
|
7
|
+
* carrying the session/node deep link to every registered device, suppressed
|
|
8
8
|
* while an operator surface is already attached to that node's session.
|
|
9
9
|
*
|
|
10
10
|
* TWO seams have to be live for a real push, not just descriptor presence:
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* runtime bus's 'fleet' domain in the first place. That is
|
|
13
13
|
* attachFleetEmitBridge, diffing the process registry's own coalesced
|
|
14
14
|
* snapshot tick into per-node lifecycle events. The terminal-shell package's
|
|
15
|
-
* createArchivableFleetRegistry only builds the registry
|
|
16
|
-
* attach the bridge
|
|
15
|
+
* createArchivableFleetRegistry only builds the registry, it does not
|
|
16
|
+
* attach the bridge, so the composition root that owns both the registry
|
|
17
17
|
* and the bus (this file) is the one place responsible for it. The SDK's
|
|
18
18
|
* own daemon composition (platform/runtime/services.ts) attaches this same
|
|
19
19
|
* bridge right after building its process registry.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fleet-services.ts
|
|
2
|
+
* fleet-services.ts, the shared, archive-aware fleet registry construction,
|
|
3
3
|
* lifted out of services.ts so the composition root stays under the file-size
|
|
4
4
|
* cap. Mirrors the sibling create*Services helpers (durability, code-index,
|
|
5
5
|
* workstream): a single dependency-injected call that services.ts makes once.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hosted-session-composition.ts
|
|
2
|
+
* hosted-session-composition.ts, what this daemon states so it may host
|
|
3
3
|
* conversation loops.
|
|
4
4
|
*
|
|
5
5
|
* The engine is the SDK's (`@pellux/goodvibes-sdk/platform/hosted-sessions`):
|
|
6
6
|
* lifecycle, the detach policy, the bounded disk state, the verbs, the
|
|
7
7
|
* lifecycle channel. It is off until a product says how a workspace FLOOR is
|
|
8
|
-
* built, and that is deliberate
|
|
8
|
+
* built, and that is deliberate, the floor's `requestApproval` seam is where a
|
|
9
9
|
* product's trust posture lives, and no default can stand in for a decision
|
|
10
10
|
* about who may write files and run commands in a directory a client named over
|
|
11
11
|
* the wire.
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* session hosted in another directory has to be gated by that directory's
|
|
21
21
|
* decision, not by the daemon's.
|
|
22
22
|
*
|
|
23
|
-
* Everything else in a floor comes from `createClientRuntimeServices
|
|
24
|
-
* same composition a terminal runs
|
|
23
|
+
* Everything else in a floor comes from `createClientRuntimeServices`, the
|
|
24
|
+
* same composition a terminal runs, so a hosted turn's tools, hooks, plugins
|
|
25
25
|
* and model stack are the ones the platform already has, not a second set.
|
|
26
26
|
*
|
|
27
27
|
* ── The exec posture a hosted turn runs under ──────────────────────────────
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
* "The same composition a terminal runs" is what BUILDS the exec tool, and it
|
|
30
30
|
* builds it the same way here: the `sandbox.*` config and the `exec-sandbox`
|
|
31
31
|
* gate this daemon already reads produce the same bubblewrap boundary a local
|
|
32
|
-
* exec gets
|
|
32
|
+
* exec gets, network, PID, UTS and IPC namespaced, the system read-only, /tmp
|
|
33
33
|
* and $HOME masked, the workspace writable, and `sandbox.egressAllowlist` the
|
|
34
|
-
* one way network comes back
|
|
34
|
+
* one way network comes back, with the self-labelling note on every result.
|
|
35
35
|
*
|
|
36
36
|
* What was NOT the same was the fallback. When no boundary could be applied,
|
|
37
37
|
* the exec tool ran the command on the host and said so, which is right for a
|
|
38
38
|
* terminal (a person asked, a person is reading) and wrong for a turn nobody is
|
|
39
39
|
* watching. A hosted conversational turn reached the whole host that way: the
|
|
40
|
-
* full process table, the owner's /proc, and his tmux session
|
|
40
|
+
* full process table, the owner's /proc, and his tmux session, where it typed.
|
|
41
41
|
*
|
|
42
42
|
* So this daemon states the posture rather than inheriting a default:
|
|
43
43
|
*
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* REQUIRED: a command that cannot be contained is refused, naming why, and
|
|
47
47
|
* running on the host is not a fallback available to it. The owner's
|
|
48
48
|
* terminal is denied outright, at the exec guard, regardless of boundary.
|
|
49
|
-
* - WORKSTREAM
|
|
50
|
-
* need the machine itself
|
|
49
|
+
* - WORKSTREAM, a real work chain the owner authorized, which may genuinely
|
|
50
|
+
* need the machine itself, is a per-spawn grant this composition makes
|
|
51
51
|
* explicitly. There is no such spawn today, so the function below never
|
|
52
52
|
* returns it; when there is one, the grant is written HERE, in the daemon's
|
|
53
53
|
* own composition, where a reader can see which spawns hold it. Nothing on
|
|
@@ -75,13 +75,13 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
|
|
|
75
75
|
[
|
|
76
76
|
'You are a GoodVibes session hosted by the daemon rather than by a terminal.',
|
|
77
77
|
`Your working directory is ${input.workspaceRoot}.`,
|
|
78
|
-
'Someone may be attached and watching this turn, or may have detached and read it later
|
|
78
|
+
'Someone may be attached and watching this turn, or may have detached and read it later;',
|
|
79
79
|
'write for both. Say what you did and why; never report work you did not do.',
|
|
80
80
|
'Tool permissions are decided by whoever is attached: an ask you raise may take a while to be',
|
|
81
81
|
'answered, and an unanswered one is a refusal, not a reason to find another way round.',
|
|
82
82
|
].join(' '),
|
|
83
83
|
// The same contract every other conversational turn is held to, from the
|
|
84
|
-
// SDK rather than restated here
|
|
84
|
+
// SDK rather than restated here, a second copy is a copy that drifts.
|
|
85
85
|
CONVERSATIONAL_DIAGNOSIS_SECTION,
|
|
86
86
|
].join('\n\n');
|
|
87
87
|
}
|
|
@@ -92,7 +92,7 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
|
|
|
92
92
|
* Every session this daemon hosts is a conversation, so every one of them is
|
|
93
93
|
* contained (see the module header). Stated on the floor rather than left to
|
|
94
94
|
* the engine's default so that the day a workstream spawn exists, the grant is
|
|
95
|
-
* a visible change to THIS function and not an option someone set elsewhere
|
|
95
|
+
* a visible change to THIS function and not an option someone set elsewhere,
|
|
96
96
|
* and it sits beside the trust gate, which is the other statement this daemon
|
|
97
97
|
* makes about how much authority a hosted run carries.
|
|
98
98
|
*/
|
|
@@ -162,7 +162,7 @@ export function createHostedSessionOptions(services: RuntimeServices): DaemonHos
|
|
|
162
162
|
// boot (the persisted discovery cache) and from the LAN scan; a floor
|
|
163
163
|
// builds its own registry and would otherwise be the only place on this
|
|
164
164
|
// box where they are not routable. Servers found by a scan that finishes
|
|
165
|
-
// AFTER a floor is built reach the next floor, not this one
|
|
165
|
+
// AFTER a floor is built reach the next floor, not this one, stated
|
|
166
166
|
// rather than hidden, because a wrong claim here would look like a model
|
|
167
167
|
// that exists everywhere except in hosted sessions.
|
|
168
168
|
const discovered = services.providerRegistry.listDiscoveredServers();
|
package/src/runtime/index.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Type-only: the runtime namespace objects must never be READ at module scope.
|
|
16
16
|
// An eager `export const X = ns.X` compiles to a top-level property read off a
|
|
17
17
|
// lazy namespace object, and Bun's single-file compiler orders module bodies
|
|
18
|
-
// nondeterministically
|
|
18
|
+
// nondeterministically, on some builds the read lands before the defining
|
|
19
19
|
// module and the binary dies at load. Values below are grouped live re-exports
|
|
20
20
|
// from the SDK's registered subpaths instead; the toolchain post-build-smoke
|
|
21
21
|
// scans compiled artifacts for the eager pattern and fails the build if one
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// knowledge-services.ts
|
|
2
|
+
// knowledge-services.ts, the knowledge/wiki + home-graph stack
|
|
3
3
|
//
|
|
4
4
|
// Constructs the three KnowledgeStores (regular wiki, agent, home-graph), their
|
|
5
5
|
// semantic services, the ingest services, the home-graph service, and the
|
|
6
6
|
// project-planning + work-plan stores. Extracted into its own module rather
|
|
7
7
|
// than built inline in services.ts, which sits at the architecture check's
|
|
8
|
-
// 800-line cap (scripts/check-architecture.ts)
|
|
8
|
+
// 800-line cap (scripts/check-architecture.ts), new/large construction blocks
|
|
9
9
|
// get their own module and a single wiring call there (mirrors
|
|
10
10
|
// createWorkstreamServices / createDurabilityServices / createCodeIndexServices).
|
|
11
11
|
//
|