@ziggs-ai/api-client 0.34.1 → 0.36.0

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.
@@ -10,10 +10,6 @@ export interface WalletBalance {
10
10
  balance: number;
11
11
  availableBalance: number;
12
12
  }
13
- export interface PaymentTransactionView {
14
- transactionId?: string;
15
- [key: string]: unknown;
16
- }
17
13
  /**
18
14
  * A resolved wallet reference (GET /payments/wallets/resolve).
19
15
  *
@@ -24,27 +20,6 @@ export interface PaymentTransactionView {
24
20
  */
25
21
  import type { WalletRef } from '@ziggs-ai/contracts';
26
22
  export type { WalletRef };
27
- export type TransferResult = {
28
- status: 'approval_required';
29
- approvalId: string | null;
30
- expiresAt: string | null;
31
- reason: string | null;
32
- toWalletId: string;
33
- amount: number;
34
- } | {
35
- status: 'transferred';
36
- transactionId: string | null;
37
- toWalletId: string;
38
- amount: number;
39
- };
40
- export interface HoldResult {
41
- transaction?: PaymentTransactionView;
42
- [key: string]: unknown;
43
- }
44
- export interface ReleaseResult {
45
- transaction?: PaymentTransactionView;
46
- [key: string]: unknown;
47
- }
48
23
  /**
49
24
  * A payment grant as this rail answers it.
50
25
  *
@@ -69,40 +44,14 @@ export interface RevokeGrantResult {
69
44
  revoked?: unknown;
70
45
  [key: string]: unknown;
71
46
  }
72
- export interface PaymentApproval {
73
- approvalId?: string;
74
- status?: string;
75
- executedTransactionId?: string | null;
76
- [key: string]: unknown;
77
- }
78
- export type WaitForApprovalResult = {
79
- status: 'gone';
80
- } | {
81
- status: 'executed';
82
- approval: PaymentApproval;
83
- transactionId: string | null;
84
- } | {
85
- status: 'rejected' | 'expired' | 'timeout';
86
- approval: PaymentApproval;
87
- };
88
- /**
89
- * Who to pay on a transfer. Kind is declared by the caller — never inferred
90
- * from id shape. A bare string is only a wallet id (`wal_…`).
91
- */
92
- export type TransferPayee = string | {
93
- walletId: string;
94
- } | {
95
- agentId: string;
96
- } | {
97
- userId: string;
98
- };
99
47
  /**
100
48
  * the one payments client for every surface (agent-sdk, ziggs-mcp,
101
49
  * scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
102
50
  * ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
103
- * policy-gated server-side (transfers above threshold return
104
- * `approval_required`). Wallet-rail grant mutations (issue / attenuate /
105
- * revoke) live here; grant *listing* is the unified GET /grants.
51
+ * policy-gated server-side. Points move only when an agreement or a task
52
+ * settles, and the server does that; there is no standalone movement verb on
53
+ * this client. Wallet-rail grant mutations (issue / attenuate / revoke) live
54
+ * here; grant *listing* is the unified GET /grants.
106
55
  */
107
56
  export declare class PaymentsClient {
108
57
  private readonly operatorKey;
@@ -128,60 +77,6 @@ export declare class PaymentsClient {
128
77
  userId?: string;
129
78
  agentId?: string;
130
79
  }): Promise<WalletRef | null>;
131
- /**
132
- * Resolve `transfer.to` to a wallet id. String form is wallet-only; agent /
133
- * user payees must name their kind.
134
- */
135
- private resolvePayeeWalletId;
136
- transfer({ to, amount, idempotencyKey, description, paymentGrantId, agreementId, }: {
137
- to: TransferPayee;
138
- amount: number;
139
- idempotencyKey?: string;
140
- description?: string;
141
- paymentGrantId?: string;
142
- /**
143
- * The hire this spend belongs to. Used for attribution, and to prefer that
144
- * hire's own grant over a standing one — a preference about which budget is
145
- * billed, never about which grants may be spent. That answer is the
146
- * server's, and it turns on who gave the grant.
147
- */
148
- agreementId?: string;
149
- }): Promise<TransferResult>;
150
- hold({ amount, idempotencyKey, description, paymentGrantId, agreementId, }: {
151
- amount: number;
152
- idempotencyKey?: string;
153
- description?: string;
154
- paymentGrantId?: string;
155
- /**
156
- * The hire this spend belongs to. Used for attribution, and to prefer that
157
- * hire's own grant over a standing one — a preference about which budget is
158
- * billed, never about which grants may be spent. That answer is the
159
- * server's, and it turns on who gave the grant.
160
- */
161
- agreementId?: string;
162
- }): Promise<HoldResult>;
163
- /**
164
- * Choose the grant that actually covers this spend.
165
- *
166
- * This used to be `grants[0]` — the first active grant, matched against
167
- * nothing. An agent holding both its standing grant and an
168
- * agreement-conferred one presented whichever came back first, the backend
169
- * correctly refused it against wallet or amount, and the agent was refused a
170
- * spend it was genuinely authorized to make.
171
- *
172
- * The source wallet is read rather than assumed: a grant on some other
173
- * wallet cannot authorize money leaving this one, and the agent may hold
174
- * grants on a counterparty's wallet from an engagement. A failure to read it
175
- * is not fatal — selection then skips the wallet test and the server still
176
- * validates, which is the same position we were in before.
177
- */
178
- private selectPaymentGrant;
179
- release({ holdId, action, toWalletId, idempotencyKey, }: {
180
- holdId: string;
181
- action?: string;
182
- toWalletId?: string;
183
- idempotencyKey?: string;
184
- }): Promise<ReleaseResult>;
185
80
  history(params?: {
186
81
  limit?: number;
187
82
  offset?: number;
@@ -202,27 +97,11 @@ export declare class PaymentsClient {
202
97
  * (retired GET /payments/grants), following the cursor to completion.
203
98
  */
204
99
  listGrants(): Promise<GrantView[]>;
205
- createTopUpIntent({ amount, description, currency, }?: {
206
- amount?: number;
207
- description?: string;
208
- currency?: string;
209
- }): Promise<unknown>;
210
- confirmMockIntent(intentId: string): Promise<unknown>;
211
100
  faucet({ amount, description, idempotencyKey, }?: {
212
101
  amount?: number;
213
102
  description?: string;
214
103
  idempotencyKey?: string;
215
104
  }): Promise<unknown>;
216
- approvals({ status }?: {
217
- status?: string;
218
- }): Promise<PaymentApproval[]>;
219
- getApproval(approvalId: string): Promise<PaymentApproval | null>;
220
- decide(approvalId: string, decision: string, note?: string): Promise<unknown>;
221
- waitForApproval(approvalId: string, opts?: {
222
- pollMs?: number;
223
- timeoutMs?: number;
224
- signal?: AbortSignal;
225
- }): Promise<WaitForApprovalResult>;
226
105
  private _request;
227
106
  private _get;
228
107
  private _post;
@@ -1,7 +1,6 @@
1
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
2
  import { buildOperatorHeaders } from './operatorHeaders.js';
3
3
  import { GrantsClient } from './GrantsClient.js';
4
- import { describeNoGrant, selectPaymentGrant, } from './paymentGrantSelection.js';
5
4
  import { throwApiError } from '../shared/apiError.js';
6
5
  function randomIdempotencyKey(prefix = 'op') {
7
6
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
@@ -10,9 +9,10 @@ function randomIdempotencyKey(prefix = 'op') {
10
9
  * the one payments client for every surface (agent-sdk, ziggs-mcp,
11
10
  * scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
12
11
  * ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
13
- * policy-gated server-side (transfers above threshold return
14
- * `approval_required`). Wallet-rail grant mutations (issue / attenuate /
15
- * revoke) live here; grant *listing* is the unified GET /grants.
12
+ * policy-gated server-side. Points move only when an agreement or a task
13
+ * settles, and the server does that; there is no standalone movement verb on
14
+ * this client. Wallet-rail grant mutations (issue / attenuate / revoke) live
15
+ * here; grant *listing* is the unified GET /grants.
16
16
  */
17
17
  export class PaymentsClient {
18
18
  operatorKey;
@@ -61,139 +61,6 @@ export class PaymentsClient {
61
61
  const res = (await this._get(`/payments/wallets/resolve?${params}`));
62
62
  return res['wallet'] || null;
63
63
  }
64
- /**
65
- * Resolve `transfer.to` to a wallet id. String form is wallet-only; agent /
66
- * user payees must name their kind.
67
- */
68
- async resolvePayeeWalletId(to) {
69
- if (typeof to === 'string') {
70
- const id = to.trim();
71
- if (!id)
72
- throw new Error('transfer: `to` is required');
73
- if (id.startsWith('wal_'))
74
- return id;
75
- throw new Error('transfer: string `to` must be a wallet id (wal_…); pass { agentId } or { userId } for a principal');
76
- }
77
- if ('walletId' in to && typeof to.walletId === 'string' && to.walletId.trim()) {
78
- return to.walletId.trim();
79
- }
80
- if ('agentId' in to && typeof to.agentId === 'string' && to.agentId.trim()) {
81
- const w = await this.resolve({ agentId: to.agentId.trim() });
82
- if (!w?.walletId) {
83
- throw new Error(`transfer: could not resolve wallet for agent "${to.agentId}"`);
84
- }
85
- return w.walletId;
86
- }
87
- if ('userId' in to && typeof to.userId === 'string' && to.userId.trim()) {
88
- const w = await this.resolve({ userId: to.userId.trim() });
89
- if (!w?.walletId) {
90
- throw new Error(`transfer: could not resolve wallet for user "${to.userId}"`);
91
- }
92
- return w.walletId;
93
- }
94
- throw new Error('transfer: `to` must be a wallet id (wal_…) or { walletId | agentId | userId }');
95
- }
96
- async transfer({ to, amount, idempotencyKey, description, paymentGrantId, agreementId, }) {
97
- if (to == null || to === '')
98
- throw new Error('transfer: `to` is required');
99
- if (!(Number.isInteger(amount) && amount > 0))
100
- throw new Error('transfer: `amount` must be a positive integer (cents)');
101
- // The recipient is resolved BEFORE choosing a grant: an `allowed_recipients`
102
- // caveat cannot be checked against a name the client has not resolved yet.
103
- const toWalletId = await this.resolvePayeeWalletId(to);
104
- // Choose the grant that covers this spend when the caller omitted one.
105
- // An explicit id still wins — a caller who names a grant means it.
106
- if (this.agentId && !paymentGrantId) {
107
- const picked = await this.selectPaymentGrant({ amount, toWalletId, agreementId });
108
- if (!picked.grant) {
109
- throw new Error(describeNoGrant('transfer', { amount, toWalletId, agreementId }, picked.rejected));
110
- }
111
- paymentGrantId = picked.grant.grantId;
112
- }
113
- const result = (await this._post('/payments/transfer', {
114
- toWalletId,
115
- amount,
116
- idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'),
117
- description,
118
- paymentGrantId,
119
- }));
120
- if (result?.['status'] === 'approval_required') {
121
- const approval = result['approval'] || {};
122
- return {
123
- status: 'approval_required',
124
- approvalId: approval['approvalId'] || null,
125
- expiresAt: approval['expiresAt'] || null,
126
- reason: approval['reason'] || 'Amount exceeds auto-approve threshold',
127
- toWalletId,
128
- amount,
129
- };
130
- }
131
- const tx = result?.['transaction'];
132
- return {
133
- status: 'transferred',
134
- transactionId: tx?.['transactionId'] || null,
135
- toWalletId,
136
- amount,
137
- };
138
- }
139
- async hold({ amount, idempotencyKey, description, paymentGrantId, agreementId, }) {
140
- if (!(Number.isInteger(amount) && amount > 0))
141
- throw new Error('hold: `amount` must be a positive integer (cents)');
142
- // A hold reserves the owner's money, so it needs a grant like a transfer.
143
- // It has no counterparty, so no recipient is offered to the caveats.
144
- if (this.agentId && !paymentGrantId) {
145
- const picked = await this.selectPaymentGrant({ amount, agreementId });
146
- if (!picked.grant) {
147
- throw new Error(describeNoGrant('hold', { amount, agreementId }, picked.rejected));
148
- }
149
- paymentGrantId = picked.grant.grantId;
150
- }
151
- return (await this._post('/payments/hold', {
152
- amount,
153
- idempotencyKey: idempotencyKey || randomIdempotencyKey('hold'),
154
- description,
155
- paymentGrantId,
156
- }));
157
- }
158
- /**
159
- * Choose the grant that actually covers this spend.
160
- *
161
- * This used to be `grants[0]` — the first active grant, matched against
162
- * nothing. An agent holding both its standing grant and an
163
- * agreement-conferred one presented whichever came back first, the backend
164
- * correctly refused it against wallet or amount, and the agent was refused a
165
- * spend it was genuinely authorized to make.
166
- *
167
- * The source wallet is read rather than assumed: a grant on some other
168
- * wallet cannot authorize money leaving this one, and the agent may hold
169
- * grants on a counterparty's wallet from an engagement. A failure to read it
170
- * is not fatal — selection then skips the wallet test and the server still
171
- * validates, which is the same position we were in before.
172
- */
173
- async selectPaymentGrant(spend) {
174
- let grants = [];
175
- try {
176
- grants = await this.listGrants();
177
- }
178
- catch {
179
- return { grant: null, rejected: [] };
180
- }
181
- let fromWalletId = null;
182
- try {
183
- fromWalletId = (await this.balance()).walletId;
184
- }
185
- catch {
186
- fromWalletId = null;
187
- }
188
- return selectPaymentGrant(grants, { ...spend, fromWalletId });
189
- }
190
- async release({ holdId, action = 'complete', toWalletId, idempotencyKey, }) {
191
- return (await this._post(`/payments/release/${holdId}`, {
192
- idempotencyKey: idempotencyKey || randomIdempotencyKey('rel'),
193
- action,
194
- toWalletId,
195
- }));
196
- }
197
64
  async history(params = {}) {
198
65
  const q = new URLSearchParams();
199
66
  if (params.limit != null)
@@ -224,12 +91,6 @@ export class PaymentsClient {
224
91
  const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
225
92
  return grants.listAllGrants({ scopeKind: 'wallet', health: 'active' });
226
93
  }
227
- async createTopUpIntent({ amount, description, currency, } = {}) {
228
- return this._post('/payments/onramp/intent', { amount, description, currency });
229
- }
230
- async confirmMockIntent(intentId) {
231
- return this._post(`/payments/onramp/mock-confirm/${intentId}`, {});
232
- }
233
94
  async faucet({ amount, description = 'Faucet', idempotencyKey, } = {}) {
234
95
  return this._post('/payments/wallet/fund', {
235
96
  amount,
@@ -237,68 +98,6 @@ export class PaymentsClient {
237
98
  description,
238
99
  });
239
100
  }
240
- async approvals({ status = 'pending' } = {}) {
241
- const res = (await this._get(`/payments/approvals?status=${encodeURIComponent(status)}`));
242
- return res['approvals'] || [];
243
- }
244
- async getApproval(approvalId) {
245
- if (!approvalId)
246
- throw new Error('getApproval: approvalId is required');
247
- try {
248
- const res = (await this._get(`/payments/approvals/${approvalId}`));
249
- return res?.['approval'] || null;
250
- }
251
- catch (err) {
252
- if (err.status === 404)
253
- return null;
254
- throw err;
255
- }
256
- }
257
- async decide(approvalId, decision, note = '') {
258
- return this._post(`/payments/approvals/${approvalId}/decide`, { decision, note });
259
- }
260
- async waitForApproval(approvalId, opts = {}) {
261
- const pollMs = Math.max(500, opts.pollMs ?? 3_000);
262
- const timeoutMs = Math.max(pollMs, opts.timeoutMs ?? 120_000);
263
- const signal = opts.signal;
264
- const deadline = Date.now() + timeoutMs;
265
- const sleep = (ms) => new Promise((resolve, reject) => {
266
- const timer = setTimeout(resolve, ms);
267
- if (signal) {
268
- if (signal.aborted) {
269
- clearTimeout(timer);
270
- reject(new Error('aborted'));
271
- return;
272
- }
273
- signal.addEventListener('abort', () => {
274
- clearTimeout(timer);
275
- reject(new Error('aborted'));
276
- }, { once: true });
277
- }
278
- });
279
- while (true) {
280
- if (signal?.aborted)
281
- throw new Error('aborted');
282
- const approval = await this.getApproval(approvalId);
283
- if (!approval)
284
- return { status: 'gone' };
285
- const s = approval.status;
286
- if (s === 'executed')
287
- return {
288
- status: 'executed',
289
- approval,
290
- transactionId: approval.executedTransactionId || null,
291
- };
292
- if (s === 'rejected')
293
- return { status: 'rejected', approval };
294
- if (s === 'expired')
295
- return { status: 'expired', approval };
296
- const remaining = deadline - Date.now();
297
- if (remaining <= 0)
298
- return { status: 'timeout', approval };
299
- await sleep(Math.min(pollMs, remaining));
300
- }
301
- }
302
101
  async _request(method, path, body) {
303
102
  const init = {
304
103
  method,
@@ -22,7 +22,7 @@ export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, Cont
22
22
  export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
23
23
  export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, GrantHolderKind, GrantAccessKind, GrantBasisKind, } from './grants.js';
24
24
  export { PaymentsClient } from './PaymentsClient.js';
25
- export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferPayee, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
25
+ export type { PaymentsError, WalletBalance, WalletRef, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, } from './PaymentsClient.js';
26
26
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
27
27
  export type { ConnectionsError, ConnectionProxyParams, ConnectionGrant, ConnectionWithGrants, McpConnectionRequestParams, } from './ConnectionsClient.js';
28
28
  export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, } from './OrgsClient.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.34.1",
3
+ "version": "0.36.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,58 +0,0 @@
1
- import { type GrantView } from './grants.js';
2
- /**
3
- * Which grant to present for a spend, and — when none fits — why each candidate
4
- * did not.
5
- *
6
- * The client used to take `grants[0]` from the active list. It matched nothing
7
- * else: not the source wallet, not the amount, not the agreement the spend
8
- * belongs to. An agent holding both its standing grant and an agreement-conferred
9
- * one presented whichever came back first, the backend correctly refused it
10
- * against wallet or amount, and the agent was refused a spend it was genuinely
11
- * authorized to make.
12
- *
13
- * Selection is a pure function of the grants and the spend so it can be tested
14
- * without a server, and so the refusal can name what it considered.
15
- */
16
- export interface SpendShape {
17
- /** The wallet the money leaves. A grant on another wallet cannot authorize it. */
18
- fromWalletId?: string | null;
19
- /** Cents. Checked against `max_amount`. */
20
- amount: number;
21
- /** The wallet receiving it, when known. Checked against `allowed_recipients`. */
22
- toWalletId?: string | null;
23
- /** The engagement this spend belongs to, when the caller names one. */
24
- agreementId?: string | null;
25
- }
26
- export interface RejectedGrant {
27
- grantId: string;
28
- reason: string;
29
- }
30
- export interface GrantSelection {
31
- grant: GrantView | null;
32
- /** Every grant that was looked at and did not fit, with the reason. */
33
- rejected: RejectedGrant[];
34
- }
35
- /**
36
- * Pick the grant to present, preferring the one that belongs to the engagement.
37
- *
38
- * Order among grants that all fit: the agreement's own grant first when the
39
- * spend names one, then the standing grant. A standing grant is the buyer
40
- * bootstrap and the right default; reaching for it while an agreement grant
41
- * covers the same spend would bill the wrong budget.
42
- *
43
- * This is a preference, never a fence. Every grant here is already one the
44
- * server said this wake may spend, so picking a different one cannot reach
45
- * another party's authority — it would only bill the wrong budget of the same
46
- * party. That is why the mismatch is no longer a rejection above.
47
- */
48
- export declare function selectPaymentGrant(grants: readonly GrantView[], spend: SpendShape): GrantSelection;
49
- /**
50
- * The sentence an agent gets when nothing fits.
51
- *
52
- * It names every grant considered and why each was not it, because "no payment
53
- * grant" is a condition and an agent cannot act on a condition — it retries,
54
- * and every retry is a full model call. Which ceiling was in the way is the
55
- * thing that tells the agent whether to ask for a bigger grant, name the
56
- * agreement, or stop.
57
- */
58
- export declare function describeNoGrant(verb: string, spend: SpendShape, rejected: readonly RejectedGrant[]): string;
@@ -1,121 +0,0 @@
1
- import { grantCaveat } from './grants.js';
2
- /**
3
- * A caveat off a grant that may not carry the array at all.
4
- *
5
- * `grantCaveat` assumes `caveats` is populated, which the unified list read
6
- * does. A caller handing us a partial view should get "no such caveat", not a
7
- * crash — this runs on the spend path, where throwing would turn a missing
8
- * field into a refused payment.
9
- */
10
- function caveatValue(grant, type) {
11
- const caveats = grant.caveats;
12
- if (!Array.isArray(caveats))
13
- return undefined;
14
- return grantCaveat(grant, type);
15
- }
16
- function numericCaveat(grant, type) {
17
- const raw = caveatValue(grant, type);
18
- return typeof raw === 'number' && Number.isFinite(raw) ? raw : null;
19
- }
20
- function recipientCaveat(grant) {
21
- const raw = caveatValue(grant, 'allowed_recipients');
22
- if (!Array.isArray(raw))
23
- return null;
24
- const ids = raw.filter((r) => typeof r === 'string');
25
- return ids.length ? ids : null;
26
- }
27
- /**
28
- * Why this grant cannot authorize this spend, or null if it can.
29
- *
30
- * `daily_budget` is deliberately not checked. It is a ceiling on spend already
31
- * made, which only the server can see — guessing here would refuse a grant that
32
- * would have worked. The server validates it inside the same transaction as the
33
- * ledger write, which is the only place the answer is not already stale.
34
- */
35
- function whyNot(grant, spend) {
36
- // A stated kind that is not `wallet` is disqualifying. An ABSENT one is not:
37
- // `listGrants` asks the server for `scopeKind: 'wallet'`, so the rail has
38
- // already answered, and treating a field the response did not populate as a
39
- // wrong answer would refuse every grant. Same reading as an unread source
40
- // wallet below — absence means unknown, never no.
41
- if (grant.scope?.kind && grant.scope.kind !== 'wallet') {
42
- return `is a ${grant.scope.kind} grant, not a wallet grant`;
43
- }
44
- if (spend.fromWalletId && grant.scope?.id && grant.scope.id !== spend.fromWalletId) {
45
- return `is on wallet ${grant.scope.id}, and this spend leaves ${spend.fromWalletId}`;
46
- }
47
- const maxAmount = numericCaveat(grant, 'max_amount');
48
- if (maxAmount !== null && spend.amount > maxAmount) {
49
- return `caps a single spend at ${maxAmount}, and this one is ${spend.amount}`;
50
- }
51
- const allowed = recipientCaveat(grant);
52
- if (allowed && spend.toWalletId && !allowed.includes(spend.toWalletId)) {
53
- return `may only pay ${allowed.join(', ')}, and this one pays ${spend.toWalletId}`;
54
- }
55
- // A grant's agreement is NOT checked here, deliberately.
56
- //
57
- // It used to be refused when it did not match the spend's agreement, which
58
- // made a customer's own grant unusable inside their second hire of the same
59
- // agent, and made a standing grant unusable on any spend that named a deal.
60
- // Whether a grant may be spent at all is the server's answer, and it turns on
61
- // who GAVE the grant against who the agent is acting for — not on which of
62
- // that party's deals is running. A list the server answered for this wake
63
- // holds only grants it may spend, so there is nothing left to refuse here.
64
- //
65
- // The agreement still decides the ORDER below, which is a budget preference
66
- // rather than a question of authority.
67
- return null;
68
- }
69
- /**
70
- * Pick the grant to present, preferring the one that belongs to the engagement.
71
- *
72
- * Order among grants that all fit: the agreement's own grant first when the
73
- * spend names one, then the standing grant. A standing grant is the buyer
74
- * bootstrap and the right default; reaching for it while an agreement grant
75
- * covers the same spend would bill the wrong budget.
76
- *
77
- * This is a preference, never a fence. Every grant here is already one the
78
- * server said this wake may spend, so picking a different one cannot reach
79
- * another party's authority — it would only bill the wrong budget of the same
80
- * party. That is why the mismatch is no longer a rejection above.
81
- */
82
- export function selectPaymentGrant(grants, spend) {
83
- const rejected = [];
84
- const fitting = [];
85
- for (const grant of grants) {
86
- const reason = whyNot(grant, spend);
87
- if (reason)
88
- rejected.push({ grantId: grant.grantId, reason });
89
- else
90
- fitting.push(grant);
91
- }
92
- if (fitting.length === 0)
93
- return { grant: null, rejected };
94
- // Explicit both ways, because the deal is no longer a fence: until it was
95
- // removed, a deal-bound grant simply never reached this list on a spend that
96
- // named nothing, so "prefer the standing grant" happened by accident of the
97
- // rejection. Now it has to be said.
98
- const standing = fitting.find((g) => !g.agreementId);
99
- const preferred = spend.agreementId
100
- ? (fitting.find((g) => g.agreementId === spend.agreementId) ?? standing)
101
- : standing;
102
- return { grant: preferred ?? fitting[0], rejected };
103
- }
104
- /**
105
- * The sentence an agent gets when nothing fits.
106
- *
107
- * It names every grant considered and why each was not it, because "no payment
108
- * grant" is a condition and an agent cannot act on a condition — it retries,
109
- * and every retry is a full model call. Which ceiling was in the way is the
110
- * thing that tells the agent whether to ask for a bigger grant, name the
111
- * agreement, or stop.
112
- */
113
- export function describeNoGrant(verb, spend, rejected) {
114
- if (rejected.length === 0) {
115
- return (`${verb}: you hold no active payment grant, so this spend cannot be ` +
116
- `authorized. The wallet owner issues one; nothing you can do from here.`);
117
- }
118
- const lines = rejected.map((r) => ` - ${r.grantId} ${r.reason}`).join('\n');
119
- return (`${verb}: none of your ${rejected.length} active payment grant(s) covers ` +
120
- `this spend of ${spend.amount}.\n${lines}`);
121
- }