@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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* register.ts, the `payments.*` handlers this daemon attaches.
|
|
3
|
+
*
|
|
4
|
+
* ── The SDK now owns most of these bodies ──────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `registerPaymentsGatewayMethods` (platform/control-plane, exported from the
|
|
7
|
+
* barrel as of sdk 2.0.18) ships real handler bodies for all seven `payments.*`
|
|
8
|
+
* verbs, built over a `PaymentsGatewayService` seam. `budgetStatus`, `listCards`
|
|
9
|
+
* and `deleteCard` are answered from that seam directly below and attached
|
|
10
|
+
* through the SDK's registrar; the local handler bodies that used to duplicate
|
|
11
|
+
* them are gone.
|
|
12
|
+
*
|
|
13
|
+
* Two verbs stay LOCAL rather than going through the SDK's own route handlers
|
|
14
|
+
* for it, and both are deliberate, not oversights:
|
|
15
|
+
*
|
|
16
|
+
* - `payments.cards.create`: the SDK's `createPaymentsCardsCreateHandler`
|
|
17
|
+
* wraps the whole call to `service.createCard(...)` in one try/catch that
|
|
18
|
+
* replaces ANY thrown error, whatever its shape, with a fixed 500
|
|
19
|
+
* "Storing the card failed. Nothing was saved." This daemon's contract is
|
|
20
|
+
* narrower than the published input schema (a card number has to contain
|
|
21
|
+
* enough digits to be one, a CVV has to be three or four digits, an expiry
|
|
22
|
+
* month has to be 1-12, see the field readers below) and reports each of
|
|
23
|
+
* those with an honest 400 naming the field. Delegating create to the SDK
|
|
24
|
+
* handler would still validate the fields, but every refusal would answer
|
|
25
|
+
* 500 instead of 400, a real wire-behaviour regression, not merely an
|
|
26
|
+
* implementation detail. So this verb keeps its own thin wrapper, which does
|
|
27
|
+
* the narrowing and then calls the same `service.createCard` the SDK
|
|
28
|
+
* handler would have, for the store write and the response shape.
|
|
29
|
+
* - `payments.purchases.list`: the SDK's `createPaymentsPurchasesListHandler`
|
|
30
|
+
* only accepts `limit` when it arrives already typed as a JS number. A GET
|
|
31
|
+
* request's query string never is, `?limit=5` arrives as the string `"5"`,
|
|
32
|
+
* so every caller of the real REST route would silently lose the ability to
|
|
33
|
+
* bound the page size and always get the handler's own default. This
|
|
34
|
+
* daemon's contract reads a numeric-looking string the same way it reads a
|
|
35
|
+
* number (`optionalCount` below), so this verb also keeps its own thin
|
|
36
|
+
* wrapper, which does that reading and then calls `service.listPurchases`.
|
|
37
|
+
*
|
|
38
|
+
* `payments.checkout.begin` and `payments.checkout.fillCard` are now attached
|
|
39
|
+
* too, over the sdk 2.0.19 browser-checkout seam
|
|
40
|
+
* (`platform/control-plane`'s `composeDaemonBrowser`/`onBrowserCheckout`/
|
|
41
|
+
* `BrowserCheckoutSeam`). They do NOT go through
|
|
42
|
+
* `registerPaymentsGatewayMethods`'s own route handlers, for the same reason
|
|
43
|
+
* `payments.cards.create`/`payments.purchases.list` do not: those handlers
|
|
44
|
+
* call `service.beginCheckout(input)`/`service.fillCardIntoCheckout(input)`
|
|
45
|
+
* with no invocation context at all, and this composition's whole
|
|
46
|
+
* "approving a purchase is a distinct act" ruling (see `registerPaymentsMethods`
|
|
47
|
+
* below) needs `context.explicitUserRequest`, which only reaches a handler
|
|
48
|
+
* attached through this daemon's own `registerCatalogHandlers`. So both verbs
|
|
49
|
+
* are attached locally, alongside `cardsCreate`/`purchasesList`, reading and
|
|
50
|
+
* shaping the SAME wire shapes `routes/payments.ts` does (ported here rather
|
|
51
|
+
* than imported, since the SDK does not publish those parsing functions on
|
|
52
|
+
* their own), and calling into the ONE `PaymentsGatewayServiceImpl` this
|
|
53
|
+
* registration's checkout pair shares for its whole life (see
|
|
54
|
+
* checkout-handlers.ts's `CheckoutServiceHolder` for why one, not one per
|
|
55
|
+
* call: its own `CheckoutRegistry` is in-memory, per-instance state, and
|
|
56
|
+
* `begin` and `fillCard` are separate control-plane calls that both need to
|
|
57
|
+
* see it).
|
|
58
|
+
*
|
|
59
|
+
* `deps.checkout` (a `CheckoutComposition`, see below) is REQUIRED, not
|
|
60
|
+
* optional: in the real daemon it is always supplied
|
|
61
|
+
* (runtime/payments-composition.ts), and its own `seam()` getter is what may
|
|
62
|
+
* legitimately be absent, either because this composition never builds a
|
|
63
|
+
* browser at all (no `homeDirectory`, a narrow embed) or because
|
|
64
|
+
* `onBrowserCheckout` has not fired yet (see
|
|
65
|
+
* runtime/browser-checkout-seam-holder.ts for why that race is benign). Either
|
|
66
|
+
* way `payments.checkout.begin`/`.fillCard` answer an honest refusal rather
|
|
67
|
+
* than 501 NOT_INVOKABLE or a crash; see `checkoutBegin`/`checkoutFillCard`
|
|
68
|
+
* below.
|
|
69
|
+
*
|
|
70
|
+
* ── Containment ───────────────────────────────────────────────────────────
|
|
71
|
+
*
|
|
72
|
+
* Every response below is BUILT from named fields rather than spread from a
|
|
73
|
+
* store record, for the reason the SDK's own route module gives: an allowlist
|
|
74
|
+
* silently drops a field a later change adds, a denylist silently ships it, and
|
|
75
|
+
* for anything on a card's code path that is the correct direction to fail.
|
|
76
|
+
* No handler here reads card material, and no failure path forwards a message
|
|
77
|
+
* from a call that had material in its arguments.
|
|
78
|
+
*/
|
|
79
|
+
import type { BudgetLedger, PaymentsConfigReader } from '@pellux/goodvibes-sdk/platform/payments';
|
|
80
|
+
import { MemoryCheckoutJournal, readDefaultCardId, readPaymentsEnabled, readPaymentsServiceConfig } from '@pellux/goodvibes-sdk/platform/payments';
|
|
81
|
+
import type { CardMetadata, CheckoutJournal } from '@pellux/goodvibes-sdk/platform/payments';
|
|
82
|
+
import {
|
|
83
|
+
registerPaymentsGatewayMethods,
|
|
84
|
+
type GatewayMethodCatalog,
|
|
85
|
+
type GatewayMethodDescriptor,
|
|
86
|
+
type PaymentPurchaseView,
|
|
87
|
+
type PaymentsGatewayService,
|
|
88
|
+
} from '../contracts.ts';
|
|
89
|
+
import { HandlerError } from '../errors.ts';
|
|
90
|
+
import { registerCatalogHandlers, type TypedHandler, type Unregister } from '../register.ts';
|
|
91
|
+
import { CheckoutServiceHolder, checkoutBeginHandler, checkoutFillCardHandler, type CheckoutComposition } from './checkout-handlers.ts';
|
|
92
|
+
import { CardStoreUnreadableError, type DaemonCardStore } from './card-store.ts';
|
|
93
|
+
import { MAX_PURCHASE_LIST_LIMIT, type DaemonPurchaseLedger, type StoredPurchase } from './purchase-ledger.ts';
|
|
94
|
+
|
|
95
|
+
export type { CheckoutComposition } from './checkout-handlers.ts';
|
|
96
|
+
|
|
97
|
+
/** The verbs this module attaches. Named so a test can assert the exact set. */
|
|
98
|
+
export const ATTACHED_PAYMENTS_METHOD_IDS: readonly string[] = [
|
|
99
|
+
'payments.budget.status',
|
|
100
|
+
'payments.cards.list',
|
|
101
|
+
'payments.cards.create',
|
|
102
|
+
'payments.cards.delete',
|
|
103
|
+
'payments.purchases.list',
|
|
104
|
+
'payments.checkout.begin',
|
|
105
|
+
'payments.checkout.fillCard',
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Kept, empty, rather than deleted: `gateway-payments-verbs.test.ts` and
|
|
110
|
+
* `register.test.ts` iterate this to assert the unattached set, and an empty
|
|
111
|
+
* array keeps that assertion meaningful (a future verb added here without a
|
|
112
|
+
* handler still gets caught) instead of forcing every caller to delete the
|
|
113
|
+
* loop. Nothing in this module's registration reads it any more.
|
|
114
|
+
*/
|
|
115
|
+
export const UNATTACHED_PAYMENTS_METHOD_IDS: readonly { readonly id: string; readonly reason: string }[] = [];
|
|
116
|
+
|
|
117
|
+
const DEFAULT_PURCHASE_LIST_LIMIT = 100;
|
|
118
|
+
|
|
119
|
+
export interface PaymentsHandlerDeps {
|
|
120
|
+
readonly cards: DaemonCardStore;
|
|
121
|
+
readonly purchases: DaemonPurchaseLedger;
|
|
122
|
+
/** Today's pools. The checkout flow is the sole writer; the composition root is responsible for making this durable. */
|
|
123
|
+
readonly budget: BudgetLedger;
|
|
124
|
+
readonly config: PaymentsConfigReader;
|
|
125
|
+
/**
|
|
126
|
+
* Whether this node is the one allowed to spend.
|
|
127
|
+
*
|
|
128
|
+
* Reported, never defaulted, see the SDK's gates.ts: on a clustered install a
|
|
129
|
+
* wrong answer here is a double-spend. The composition root supplies the
|
|
130
|
+
* coordinator's own answer.
|
|
131
|
+
*/
|
|
132
|
+
readonly isPaymentsLeader: () => boolean;
|
|
133
|
+
readonly now?: (() => number) | undefined;
|
|
134
|
+
readonly checkout: CheckoutComposition;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Input readers
|
|
139
|
+
//
|
|
140
|
+
// Each names the FIELD and never the value, the property the SDK's own route
|
|
141
|
+
// module enforces: an error string is a read path like any other.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
function invalid(field: string, requirement: string): HandlerError {
|
|
145
|
+
return new HandlerError(`${field} ${requirement}`, 'INVALID_ARGUMENT', 400);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Run a card-store call and refuse in the caller's terms.
|
|
150
|
+
*
|
|
151
|
+
* Two outcomes, and the difference is what the caller is allowed to be told:
|
|
152
|
+
*
|
|
153
|
+
* - `CardStoreUnreadableError` is a message this codebase WROTE, naming the
|
|
154
|
+
* file and what to do about it. It is forwarded verbatim because the operator
|
|
155
|
+
* cannot fix a damaged card file they are not told about, and 409 says the
|
|
156
|
+
* honest thing: nothing is wrong with the request, the store is not in a
|
|
157
|
+
* state that can serve it.
|
|
158
|
+
* - Anything else came out of the secret store, and its message can name the
|
|
159
|
+
* store path, the key, or the value it was handling. It is DISCARDED and
|
|
160
|
+
* replaced here. Without this, `registerCatalogHandler`'s generic wrapper
|
|
161
|
+
* forwards the original as a 500 body, which put a store path in front of any
|
|
162
|
+
* caller holding read:payments.
|
|
163
|
+
*/
|
|
164
|
+
async function overStore<T>(what: string, run: () => Promise<T>): Promise<T> {
|
|
165
|
+
try {
|
|
166
|
+
return await run();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof CardStoreUnreadableError) {
|
|
169
|
+
throw new HandlerError(error.message, 'FAILED_PRECONDITION', 409);
|
|
170
|
+
}
|
|
171
|
+
void error;
|
|
172
|
+
throw new HandlerError(`${what} failed.`, 'INTERNAL_ERROR', 500);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readString(source: Record<string, unknown>, field: string): string {
|
|
177
|
+
const value = source[field];
|
|
178
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
179
|
+
throw invalid(field, 'is required.');
|
|
180
|
+
}
|
|
181
|
+
return value.trim();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readInteger(source: Record<string, unknown>, field: string, min: number, max: number): number {
|
|
185
|
+
const value = source[field];
|
|
186
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max) {
|
|
187
|
+
throw invalid(field, `must be a whole number between ${String(min)} and ${String(max)}.`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function asRecord(body: unknown): Record<string, unknown> {
|
|
193
|
+
return typeof body === 'object' && body !== null && !Array.isArray(body)
|
|
194
|
+
? (body as Record<string, unknown>)
|
|
195
|
+
: {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A number that arrived as a query string (a GET) or as JSON (an invoke). */
|
|
199
|
+
function optionalCount(raw: unknown): number | undefined {
|
|
200
|
+
if (typeof raw === 'number' && Number.isInteger(raw) && raw > 0) return raw;
|
|
201
|
+
if (typeof raw === 'string' && /^[0-9]+$/.test(raw.trim())) {
|
|
202
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
203
|
+
if (parsed > 0) return parsed;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Response builders (allowlists, never spreads)
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
interface CardView {
|
|
213
|
+
readonly id: string;
|
|
214
|
+
readonly label: string;
|
|
215
|
+
readonly brand: string;
|
|
216
|
+
readonly last4: string;
|
|
217
|
+
readonly kind: 'virtual' | 'real';
|
|
218
|
+
readonly expiryMonth: number;
|
|
219
|
+
readonly expiryYear: number;
|
|
220
|
+
readonly issuerCapMinorUnits: number | null;
|
|
221
|
+
readonly addedAt: string;
|
|
222
|
+
readonly materialComplete: boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function cardView(card: CardMetadata, materialComplete: boolean): CardView {
|
|
226
|
+
return {
|
|
227
|
+
id: card.id,
|
|
228
|
+
label: card.label,
|
|
229
|
+
brand: card.brand,
|
|
230
|
+
last4: card.last4,
|
|
231
|
+
kind: card.kind,
|
|
232
|
+
expiryMonth: card.expiryMonth,
|
|
233
|
+
expiryYear: card.expiryYear,
|
|
234
|
+
issuerCapMinorUnits: card.issuerCapMinorUnits,
|
|
235
|
+
addedAt: card.addedAt,
|
|
236
|
+
materialComplete,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function purchaseView(row: StoredPurchase): PaymentPurchaseView {
|
|
241
|
+
return {
|
|
242
|
+
purchaseId: row.purchaseId,
|
|
243
|
+
atUtc: row.atUtc,
|
|
244
|
+
dayKey: row.dayKey,
|
|
245
|
+
timezone: row.timezone,
|
|
246
|
+
merchantDomain: row.merchantDomain,
|
|
247
|
+
item: String(row.item),
|
|
248
|
+
currency: String(row.currency),
|
|
249
|
+
itemMinorUnits: row.itemMinorUnits,
|
|
250
|
+
taxMinorUnits: row.taxMinorUnits,
|
|
251
|
+
feesMinorUnits: row.feesMinorUnits,
|
|
252
|
+
shippingMinorUnits: row.shippingMinorUnits,
|
|
253
|
+
totalMinorUnits: row.totalMinorUnits,
|
|
254
|
+
shippingTierRequested: row.shippingTierRequested,
|
|
255
|
+
shippingTierUsed: row.shippingTierUsed,
|
|
256
|
+
steppedDown: row.steppedDown === true,
|
|
257
|
+
itemPoolDraw: row.itemPoolDraw,
|
|
258
|
+
overagePoolDraw: row.overagePoolDraw,
|
|
259
|
+
tolerancePoolDraw: row.tolerancePoolDraw,
|
|
260
|
+
cardLast4: row.cardLast4,
|
|
261
|
+
windowKind: row.windowKind,
|
|
262
|
+
windowOutcome: row.windowOutcome,
|
|
263
|
+
answeredBy: row.answeredBy ?? null,
|
|
264
|
+
outcome: row.outcome,
|
|
265
|
+
refusalReason: row.refusalReason ?? null,
|
|
266
|
+
merchantOrderId: row.merchantOrderId ?? null,
|
|
267
|
+
refundedAt: row.refundedAt ?? null,
|
|
268
|
+
merchantRecognised: row.merchantRecognised === true,
|
|
269
|
+
merchantQualifier: row.merchantQualifier ?? null,
|
|
270
|
+
merchantDiscovered: row.merchantDiscovered === true,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A checkout verb reached through the service seam despite neither local
|
|
276
|
+
* checkout handler below ever calling it: both call into the ONE
|
|
277
|
+
* `PaymentsGatewayServiceImpl` this registration's checkout pair shares for
|
|
278
|
+
* its whole life, held by `CheckoutServiceHolder` (checkout-handlers.ts), not
|
|
279
|
+
* a fresh instance built per call. Only `begin` needs anything per-invocation,
|
|
280
|
+
* the gate-input cell (`CheckoutGateInputsCell`) it writes just before each
|
|
281
|
+
* call, since the shared service's `gates()` closure has no other way to see a
|
|
282
|
+
* given call's `context.explicitUserRequest` or card/address facts; `fillCard`
|
|
283
|
+
* reads nothing per-invocation at all, it types into fields the prior `begin`
|
|
284
|
+
* already found. Either way, `PaymentsGatewayService`'s plain
|
|
285
|
+
* `beginCheckout(input)`/`fillCardIntoCheckout(input)` shape has no room for
|
|
286
|
+
* that context, which is the actual reason these two verbs are attached as
|
|
287
|
+
* local wrappers rather than through this service (see this file's header).
|
|
288
|
+
* The stub below exists only so `PaymentsGatewayService` stays fully
|
|
289
|
+
* implemented for `registerPaymentsGatewayMethods`'s throwaway first
|
|
290
|
+
* attachment, immediately replaced by `registerPaymentsMethods`.
|
|
291
|
+
*/
|
|
292
|
+
function checkoutNotWired(methodId: string): Error {
|
|
293
|
+
return new Error(
|
|
294
|
+
`${methodId} is served by this daemon's own local handler, never through this service. `
|
|
295
|
+
+ 'See registerPaymentsMethods in register.ts.',
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The `PaymentsGatewayService` this daemon hands the SDK's registrar.
|
|
303
|
+
*
|
|
304
|
+
* `createCard` and `listPurchases` are real, not stubs: `payments.cards.create`
|
|
305
|
+
* and `payments.purchases.list` keep their own thin local wrappers (below) for
|
|
306
|
+
* the field-shape validation and the string-tolerant query reading the SDK's
|
|
307
|
+
* generic route handlers do not do, and both wrappers call straight into these
|
|
308
|
+
* same two methods for the store write and the response shape, so there is
|
|
309
|
+
* exactly one place that talks to `DaemonCardStore.create` and to
|
|
310
|
+
* `DaemonPurchaseLedger.list`.
|
|
311
|
+
*/
|
|
312
|
+
function buildPaymentsGatewayService(deps: PaymentsHandlerDeps): PaymentsGatewayService {
|
|
313
|
+
const now = deps.now ?? Date.now;
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
async budgetStatus() {
|
|
317
|
+
const config = readPaymentsServiceConfig(deps.config);
|
|
318
|
+
const nowMs = now();
|
|
319
|
+
const pools = deps.budget.snapshot(config.limits, nowMs, config.timezone);
|
|
320
|
+
const live = deps.budget.state().reservations.filter((entry) => entry.expiresAtMs > nowMs);
|
|
321
|
+
return {
|
|
322
|
+
enabled: readPaymentsEnabled(deps.config),
|
|
323
|
+
currency: String(config.budgetCurrency),
|
|
324
|
+
pools,
|
|
325
|
+
reservationCount: live.length,
|
|
326
|
+
isPaymentsLeader: deps.isPaymentsLeader(),
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async listCards() {
|
|
331
|
+
return overStore('Listing the stored cards', async () => {
|
|
332
|
+
const built: CardView[] = [];
|
|
333
|
+
for (const card of deps.cards.list()) {
|
|
334
|
+
built.push(cardView(card, await deps.cards.materialComplete(card.id)));
|
|
335
|
+
}
|
|
336
|
+
return { cards: built, defaultCardId: readDefaultCardId(deps.config) };
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
async createCard(input) {
|
|
341
|
+
let card: CardMetadata;
|
|
342
|
+
try {
|
|
343
|
+
card = await deps.cards.create(input);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
// A damaged card file is the operator's to fix and its message says how,
|
|
346
|
+
// so it is forwarded; see overStore. Everything else is discarded, because
|
|
347
|
+
// the failing call had the card in its arguments.
|
|
348
|
+
if (error instanceof CardStoreUnreadableError) {
|
|
349
|
+
throw new HandlerError(error.message, 'FAILED_PRECONDITION', 409);
|
|
350
|
+
}
|
|
351
|
+
void error;
|
|
352
|
+
throw new HandlerError('Storing the card failed. Nothing was saved.', 'INTERNAL_ERROR', 500);
|
|
353
|
+
}
|
|
354
|
+
return cardView(card, await overStore('Reading the card back', () => deps.cards.materialComplete(card.id)));
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
async deleteCard(id) {
|
|
358
|
+
return overStore('Deleting the card', () => deps.cards.remove(id));
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
async beginCheckout() {
|
|
362
|
+
throw checkoutNotWired('payments.checkout.begin');
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
async fillCardIntoCheckout() {
|
|
366
|
+
throw checkoutNotWired('payments.checkout.fillCard');
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
async listPurchases(input) {
|
|
370
|
+
const result = deps.purchases.list(input);
|
|
371
|
+
return { purchases: result.purchases.map(purchaseView), total: result.total };
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Registration
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Attach the seven `payments.*` handlers to the descriptors the SDK catalog
|
|
382
|
+
* already holds. Returns the teardown.
|
|
383
|
+
*
|
|
384
|
+
* NOT gated on `payments.enabled`. That key defaults to false, and
|
|
385
|
+
* `payments.cards.*` is how a surface CONFIGURES the capability, so gating
|
|
386
|
+
* registration on it would leave the configuration surface unreachable until the
|
|
387
|
+
* capability was already configured, which is the shape of the defect this
|
|
388
|
+
* module exists to fix. The setting is reported live by `budget.status` instead,
|
|
389
|
+
* and it is `checkPaymentGates` at purchase time that stops a disabled daemon
|
|
390
|
+
* from spending.
|
|
391
|
+
*/
|
|
392
|
+
export function registerPaymentsMethods(
|
|
393
|
+
catalog: GatewayMethodCatalog,
|
|
394
|
+
deps: PaymentsHandlerDeps,
|
|
395
|
+
): Unregister {
|
|
396
|
+
// Every descriptor this module attaches to, captured BEFORE any
|
|
397
|
+
// registration runs. `registerCatalogHandlers`' own teardown (used below for
|
|
398
|
+
// four of these) removes the DESCRIPTOR from the catalog entirely rather
|
|
399
|
+
// than merely clearing its handler slot (`GatewayMethodCatalog.register`'s
|
|
400
|
+
// returned teardown calls `unregister`, a `Map.delete`, not a handler
|
|
401
|
+
// reset), so restoring a handler-less descriptor after THIS module's own
|
|
402
|
+
// teardown, matching what the SDK's three descriptors are restored to below,
|
|
403
|
+
// needs the descriptor object captured here rather than re-fetched from the
|
|
404
|
+
// catalog afterward, when it may no longer be there to fetch.
|
|
405
|
+
const descriptors = new Map<string, GatewayMethodDescriptor>();
|
|
406
|
+
for (const id of ATTACHED_PAYMENTS_METHOD_IDS) {
|
|
407
|
+
const descriptor = catalog.get(id);
|
|
408
|
+
if (descriptor) descriptors.set(id, descriptor);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const service = buildPaymentsGatewayService(deps);
|
|
412
|
+
|
|
413
|
+
// Attaches all seven `payments.*` descriptors. budget/list/delete stay
|
|
414
|
+
// attached through this; create/purchases-list/checkout-begin/checkout-
|
|
415
|
+
// fillCard are all transiently attached here and immediately replaced below
|
|
416
|
+
// with this daemon's own local handlers. See the header comment for why
|
|
417
|
+
// each group needs its own wrapper.
|
|
418
|
+
registerPaymentsGatewayMethods(catalog, service);
|
|
419
|
+
|
|
420
|
+
// The journal backing this registration's checkout pair's in-flight
|
|
421
|
+
// registry (the SDK's own `CheckoutRegistry`, built inside
|
|
422
|
+
// `PaymentsGatewayServiceImpl`'s constructor from whatever `CheckoutJournal`
|
|
423
|
+
// it is handed, see checkout-handlers.ts's `buildCheckoutService`; this
|
|
424
|
+
// value here is the journal, not the registry itself). `MemoryCheckoutJournal`
|
|
425
|
+
// (the sdk's own, checkout-registry.ts, documented "durable across nothing")
|
|
426
|
+
// is a deliberate choice for THIS pass, not an oversight: a durable journal
|
|
427
|
+
// is real work (a file or store write on every `registry.advance`, including
|
|
428
|
+
// the `submit-pending` flush checkout-flow.ts's step 9 makes right before the
|
|
429
|
+
// merchant submit) that has not been done yet. The gap it leaves is exactly
|
|
430
|
+
// the one that flush exists to close: if this process crashes between that
|
|
431
|
+
// `submit-pending` write and seeing the merchant's response, a restart with a
|
|
432
|
+
// durable journal could tell the owner "this purchase may already have been
|
|
433
|
+
// submitted, do not resubmit it"; with this in-memory journal, that record is
|
|
434
|
+
// gone the moment the process is, and a restart has no way to know the
|
|
435
|
+
// purchase was ever in flight at all. Recorded as future work in
|
|
436
|
+
// `.goodvibes/memory/decisions.json`, not implied to be solved here.
|
|
437
|
+
const checkoutJournal: CheckoutJournal = new MemoryCheckoutJournal();
|
|
438
|
+
// The ONE checkout service instance this registration's begin/fillCard pair
|
|
439
|
+
// share for their whole life; see checkout-handlers.ts's own header.
|
|
440
|
+
const checkoutServiceHolder = new CheckoutServiceHolder(deps, checkoutJournal);
|
|
441
|
+
|
|
442
|
+
const cardsCreate: TypedHandler<unknown, Record<string, unknown>> = async ({ body }) => {
|
|
443
|
+
const params = asRecord(body);
|
|
444
|
+
const kind = readString(params, 'kind');
|
|
445
|
+
if (kind !== 'virtual' && kind !== 'real') {
|
|
446
|
+
throw invalid('kind', "must be 'virtual' or 'real'.");
|
|
447
|
+
}
|
|
448
|
+
const label = readString(params, 'label');
|
|
449
|
+
const number = readString(params, 'number');
|
|
450
|
+
// Narrower than the published input schema, which types these as a plain
|
|
451
|
+
// string and a plain number. Each check below is a property of BEING a card
|
|
452
|
+
// rather than a policy about one: a value that fails it could not be
|
|
453
|
+
// charged, and storing it would produce a card the surface offers and the
|
|
454
|
+
// checkout can never fill. None of them names anything but the field.
|
|
455
|
+
if (number.replace(/\D/g, '').length < 12) {
|
|
456
|
+
throw invalid('number', 'does not contain enough digits to be a card number.');
|
|
457
|
+
}
|
|
458
|
+
const expiryMonth = readInteger(params, 'expiryMonth', 1, 12);
|
|
459
|
+
// A full four-digit year: `cardFieldValue` derives the two-digit form by
|
|
460
|
+
// slicing this one, so a year stored as 29 would type as "29" in a
|
|
461
|
+
// four-digit field and as "29" in a two-digit one, and only one of those is
|
|
462
|
+
// right.
|
|
463
|
+
const expiryYear = readInteger(params, 'expiryYear', 1000, 9999);
|
|
464
|
+
const cvv = readString(params, 'cvv');
|
|
465
|
+
if (!/^[0-9]{3,4}$/.test(cvv)) {
|
|
466
|
+
throw invalid('cvv', 'must be the three or four digit code printed on the card.');
|
|
467
|
+
}
|
|
468
|
+
const cardholderName = readString(params, 'cardholderName');
|
|
469
|
+
const rawCap = params['issuerCapMinorUnits'];
|
|
470
|
+
|
|
471
|
+
const card = await service.createCard({
|
|
472
|
+
label,
|
|
473
|
+
kind,
|
|
474
|
+
number,
|
|
475
|
+
expiryMonth,
|
|
476
|
+
expiryYear,
|
|
477
|
+
cvv,
|
|
478
|
+
cardholderName,
|
|
479
|
+
issuerCapMinorUnits: typeof rawCap === 'number' && Number.isInteger(rawCap) ? rawCap : null,
|
|
480
|
+
});
|
|
481
|
+
return { card };
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const purchasesList: TypedHandler<unknown, Record<string, unknown>> = async ({ body, query }) => {
|
|
485
|
+
const params = { ...query, ...asRecord(body) };
|
|
486
|
+
const requested = optionalCount(params['limit']);
|
|
487
|
+
const rawDay = params['dayKey'];
|
|
488
|
+
const dayKey = typeof rawDay === 'string' && rawDay.trim().length > 0 ? rawDay.trim() : undefined;
|
|
489
|
+
return service.listPurchases({
|
|
490
|
+
limit: Math.min(requested ?? DEFAULT_PURCHASE_LIST_LIMIT, MAX_PURCHASE_LIST_LIMIT),
|
|
491
|
+
dayKey,
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const localTeardown = registerCatalogHandlers(catalog, [
|
|
496
|
+
{ id: 'payments.cards.create', handler: cardsCreate as TypedHandler<unknown, unknown> },
|
|
497
|
+
{ id: 'payments.purchases.list', handler: purchasesList as TypedHandler<unknown, unknown> },
|
|
498
|
+
{ id: 'payments.checkout.begin', handler: checkoutBeginHandler(deps, checkoutServiceHolder) as TypedHandler<unknown, unknown> },
|
|
499
|
+
{ id: 'payments.checkout.fillCard', handler: checkoutFillCardHandler(deps, checkoutServiceHolder) as TypedHandler<unknown, unknown> },
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
return () => {
|
|
503
|
+
localTeardown();
|
|
504
|
+
// Restores EVERY descriptor this module attached to, handler-less, not
|
|
505
|
+
// only the three `registerPaymentsGatewayMethods` still holds a live
|
|
506
|
+
// handler on. The other four had their descriptor removed outright by
|
|
507
|
+
// `localTeardown()` above (see the `descriptors` capture at the top of
|
|
508
|
+
// this function for why), so without this a re-registration on the SAME
|
|
509
|
+
// catalog (a second `registerPaymentsMethods` call, as a restart-without-
|
|
510
|
+
// recompose test does) would find those four ids gone from the catalog
|
|
511
|
+
// and throw `METHOD_NOT_FOUND` trying to attach to them, rather than
|
|
512
|
+
// finding the SDK's own builtin descriptor there to replace, exactly as
|
|
513
|
+
// it would on a catalog this module had never touched.
|
|
514
|
+
for (const [, descriptor] of descriptors) {
|
|
515
|
+
catalog.register(descriptor, undefined, { replace: true });
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
@@ -60,8 +60,8 @@ function errorMessage(error: unknown): string {
|
|
|
60
60
|
*
|
|
61
61
|
* An ABSENT context is accepted and read as the empty one: no principal, no
|
|
62
62
|
* scopes, not admin, nobody claiming a person asked. The type says a context is
|
|
63
|
-
* always there and at runtime it is not always
|
|
64
|
-
* builds the invocation by hand can omit it
|
|
63
|
+
* always there and at runtime it is not always, an in-process invoke that
|
|
64
|
+
* builds the invocation by hand can omit it, and reading `.metadata` off
|
|
65
65
|
* `undefined` turned that into a TypeError thrown out of the handler wrapper
|
|
66
66
|
* instead of the refusal every caller can act on. Defaulting to the least
|
|
67
67
|
* privilege is the only safe reading: it can cost a caller an authorization it
|
|
@@ -124,7 +124,7 @@ export function registerCatalogHandler<TBody, TResult>(
|
|
|
124
124
|
const descriptor = catalog.get(methodId);
|
|
125
125
|
if (!descriptor) {
|
|
126
126
|
// 'METHOD_NOT_FOUND' (not the old locally-coined 'UNKNOWN_METHOD') so this lines
|
|
127
|
-
// up byte-for-byte with SDKErrorCodes.METHOD_NOT_FOUND
|
|
127
|
+
// up byte-for-byte with SDKErrorCodes.METHOD_NOT_FOUND, the code the SDK's own
|
|
128
128
|
// uncataloged-method 404 now carries (method-catalog.ts's GatewayMethodCatalog
|
|
129
129
|
// .invoke(), daemon/control-plane.ts's invokeGatewayMethodCall, and daemon-sdk's
|
|
130
130
|
// control-routes.ts getGatewayMethod/invokeGatewayMethod). A literal string, not
|
|
@@ -20,12 +20,20 @@ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../../../../config/surface.ts';
|
|
|
20
20
|
* Cloud-terminal backend: executes a command in a managed cloud shell / VM via
|
|
21
21
|
* the provider CLI (gcloud / aws / az). The provider credential is resolved from
|
|
22
22
|
* the daemon credential store and supplied to the CLI via a 0600 credentials
|
|
23
|
-
* file or a provider-specific env var
|
|
23
|
+
* file or a provider-specific env var, never as an argv token, never logged.
|
|
24
24
|
*/
|
|
25
25
|
export function createCloudTerminalBackend(ctx: BackendContext): Backend {
|
|
26
26
|
const credDir = join(ctx.homeDirectory, '.goodvibes', GOODVIBES_DAEMON_SURFACE_ROOT, 'operator', 'cloud-creds');
|
|
27
|
+
// Crash-window sweep: dispatch's finally block only removes a credential
|
|
28
|
+
// file on an orderly return; a hard crash mid-dispatch (SIGKILL, OOM-kill,
|
|
29
|
+
// power loss) skips both that and teardown(), leaving the file behind for
|
|
30
|
+
// every daemon restart until now. Sweep at construction, before any
|
|
31
|
+
// dispatch on this instance can have written anything, so a crash converges
|
|
32
|
+
// to the same clean state a graceful shutdown already produces.
|
|
33
|
+
const credDirSwept = rm(credDir, { recursive: true, force: true }).catch(() => {});
|
|
27
34
|
|
|
28
35
|
async function writeCredentialFile(peerId: string, value: string): Promise<string> {
|
|
36
|
+
await credDirSwept;
|
|
29
37
|
await mkdir(credDir, { recursive: true });
|
|
30
38
|
await chmod(credDir, 0o700).catch(() => {});
|
|
31
39
|
const suffix = randomBytes(4).toString('hex');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Shared subprocess runner built on Bun.spawn. Captures stdout/stderr/exit code
|
|
2
2
|
// with a hard timeout. Used by every backend that shells out (docker/ssh/cloud/
|
|
3
|
-
// local-process). No credentials are ever passed as argv
|
|
3
|
+
// local-process). No credentials are ever passed as argv, callers pass key
|
|
4
4
|
// material via files or the `env` overlay.
|
|
5
5
|
|
|
6
6
|
export interface RunOptions {
|
|
@@ -20,7 +20,7 @@ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../../../../config/surface.ts';
|
|
|
20
20
|
* Persistent-key material is written to {homeDirectory}/.goodvibes/tui/operator/
|
|
21
21
|
* ssh-keys/{peerId}.key with 0600 permissions and reused across invocations
|
|
22
22
|
* (connection pooling via the OpenSSH ControlMaster multiplexer). The key value
|
|
23
|
-
* itself comes only from the daemon credential store
|
|
23
|
+
* itself comes only from the daemon credential store, never argv, never logs.
|
|
24
24
|
*/
|
|
25
25
|
interface PooledIdentity {
|
|
26
26
|
keyPath: string;
|
|
@@ -31,6 +31,13 @@ interface PooledIdentity {
|
|
|
31
31
|
export function createSshBackend(ctx: BackendContext): Backend {
|
|
32
32
|
const pool = new Map<string, PooledIdentity>();
|
|
33
33
|
const keyDir = join(ctx.homeDirectory, '.goodvibes', GOODVIBES_DAEMON_SURFACE_ROOT, 'operator', 'ssh-keys');
|
|
34
|
+
// Crash-window sweep: an abrupt daemon exit (SIGKILL, OOM-kill, power loss)
|
|
35
|
+
// skips teardown() and can leave a previous process's key files behind. The
|
|
36
|
+
// pool above starts empty regardless, so those leftovers are unreachable
|
|
37
|
+
// dead weight; clear them here so a crash converges to the same clean state
|
|
38
|
+
// a graceful shutdown already produces. Every write below waits on this
|
|
39
|
+
// first so the sweep can never race a key this instance just wrote.
|
|
40
|
+
const keyDirSwept = rm(keyDir, { recursive: true, force: true }).catch(() => {});
|
|
34
41
|
|
|
35
42
|
async function ensureIdentity(
|
|
36
43
|
peer: PeerRecord,
|
|
@@ -40,6 +47,7 @@ export function createSshBackend(ctx: BackendContext): Backend {
|
|
|
40
47
|
if (existing && existing.identityRef === config.identityRef) {
|
|
41
48
|
return existing;
|
|
42
49
|
}
|
|
50
|
+
await keyDirSwept;
|
|
43
51
|
const key = await ctx.credentials.resolveRef(config.identityRef);
|
|
44
52
|
if (!key || key.length === 0) {
|
|
45
53
|
throw new BackendDispatchError(
|
|
@@ -29,7 +29,7 @@ export interface BackendDispatchResult {
|
|
|
29
29
|
export interface BackendContext {
|
|
30
30
|
credentials: DaemonCredentialStore;
|
|
31
31
|
logger: HandlerLogger;
|
|
32
|
-
/** Daemon home dir
|
|
32
|
+
/** Daemon home dir, used for ephemeral key material under a 0700 subdir. */
|
|
33
33
|
homeDirectory: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -84,7 +84,7 @@ export class BackendDispatchError extends Error {
|
|
|
84
84
|
* REMOTE-SHELL SEMANTICS (intentional, documented asymmetry vs local-process):
|
|
85
85
|
* positional `payload.args` are joined onto the command with a single space and
|
|
86
86
|
* are NOT shell-escaped, because these backends hand a single command STRING to
|
|
87
|
-
* a remote shell
|
|
87
|
+
* a remote shell, the operator's `command` may itself contain pipes, redirects,
|
|
88
88
|
* globs, or quoting that must survive the hop verbatim. The local-process
|
|
89
89
|
* backend, by contrast, never invokes a shell and passes args as discrete argv.
|
|
90
90
|
*
|
|
@@ -17,7 +17,7 @@ function sha256First(input: string, hexChars: number): string {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
|
-
// Work-item hook
|
|
20
|
+
// Work-item hook, long-running invocations are enqueued as work items visible
|
|
21
21
|
// in remote.work.list. The dispatcher does not own the distributed runtime; the
|
|
22
22
|
// integrator wires this hook to the DistributedRuntimeManager work queue.
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
@@ -36,7 +36,7 @@ export interface RemoteWorkEnqueuer {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
|
-
// Invoke result
|
|
39
|
+
// Invoke result, returned to the agent through remote.peers.invoke. Includes
|
|
40
40
|
// stdoutDigest (sha256 of FULL stdout, 64 hex chars) per the receipt contract.
|
|
41
41
|
// The agent may receive only a truncated stdout preview.
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
@@ -168,7 +168,7 @@ export class RemoteDispatcher {
|
|
|
168
168
|
/**
|
|
169
169
|
* Best-effort teardown: invoke every backend's optional teardown so ephemeral
|
|
170
170
|
* key/credential material (ssh-keys/, cloud-creds/) is swept from disk and
|
|
171
|
-
* does not outlive the daemon. Failures are swallowed
|
|
171
|
+
* does not outlive the daemon. Failures are swallowed, teardown must never
|
|
172
172
|
* throw during surface shutdown.
|
|
173
173
|
*/
|
|
174
174
|
async teardown(): Promise<void> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remote handler surface
|
|
2
|
+
* Remote handler surface, the host backend for `remote.peers.*`.
|
|
3
3
|
*
|
|
4
4
|
* `remote.peers.invoke` is NOT a catalog method: the SDK publishes it as an HTTP
|
|
5
5
|
* route and injects a `DistributedRuntimeRouteService` (the host's
|