@bondedhq/sdk 0.1.3 → 0.1.5

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/dist/langchain.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DynamicStructuredTool } from "@langchain/core/tools";
2
2
  import { resolveTools, resultText } from "./mcp.js";
3
+ import { guardPayments, isPaymentRefusal } from "./payments.js";
3
4
  import { bondedVaultTools } from "./vault-tools.js";
4
5
  /**
5
6
  * The episode's tools as LangChain `StructuredTool`s, for LangChain and LangGraph agents
@@ -50,4 +51,37 @@ export function bondedVaultLangChainTools(options) {
50
51
  func: async (args) => resultText(await t.call(args ?? {})),
51
52
  }));
52
53
  }
54
+ /**
55
+ * Your own LangChain pay tool, with every call checked against the payment mandate first (see
56
+ * `PaymentGuard`). Same name, description and schema. When the guard refuses (Enforce mode
57
+ * blocked it, the guard is frozen, or it can't read the payment) your tool is not called and the
58
+ * model gets `Error: ...` naming the rules broken, so it can react. In Monitor mode a breaking
59
+ * payment goes through and the guard's `onBreach` reports it. A local check: no receipts.
60
+ *
61
+ * ```ts
62
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
63
+ * const tools = [...readOnlyTools, guardLangChainPayTool(payInvoice, { guard })];
64
+ * ```
65
+ *
66
+ * The payment is read from the tool's arguments: `{ account, amountUsd, invoiceReference }` unless
67
+ * you pass `toPayment`.
68
+ */
69
+ export function guardLangChainPayTool(payTool, options) {
70
+ const guarded = guardPayments((args, config) => payTool.invoke(args, config), options);
71
+ return new DynamicStructuredTool({
72
+ name: payTool.name,
73
+ description: payTool.description,
74
+ schema: payTool.schema,
75
+ func: async (args, _runManager, config) => {
76
+ try {
77
+ return await guarded(args ?? {}, config);
78
+ }
79
+ catch (error) {
80
+ if (isPaymentRefusal(error))
81
+ return `Error: ${error.message}`;
82
+ throw error;
83
+ }
84
+ },
85
+ });
86
+ }
53
87
  //# sourceMappingURL=langchain.js.map
package/dist/mcp.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
3
  import type { DoneRequest } from "@bondedhq/shared";
4
+ import { type GuardPaymentsOptions } from "./payments.js";
3
5
  export { isBondedEpisode } from "./episode.js";
4
6
  export { finishEpisode } from "./done.js";
7
+ export type { GuardPaymentsOptions } from "./payments.js";
5
8
  /**
6
9
  * Where an episode's tools are. A `BondedEpisode` (from `fromBondedEnv`, or the one an Agent URL
7
10
  * handler hands you) has these, and so does the runner protocol's `EpisodeMessage`.
@@ -128,4 +131,21 @@ export declare function mcpConfigJson(episode?: {
128
131
  mcp: string;
129
132
  };
130
133
  }, options?: McpConfigOptions): string;
134
+ /**
135
+ * Your MCP server's pay tool handler, with every call checked against the payment mandate first
136
+ * (see `PaymentGuard`). Register the result in its place:
137
+ *
138
+ * ```ts
139
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
140
+ * server.registerTool("pay_invoice", config, guardMcpPayHandler(payInvoice, { guard }));
141
+ * ```
142
+ *
143
+ * When the guard refuses (Enforce mode blocked it, the guard is frozen, or it can't read the
144
+ * payment) your handler is not called and the client gets an error result (`isError: true`)
145
+ * naming the rules broken, so the model can react. In Monitor mode a breaking payment goes
146
+ * through and the guard's `onBreach` reports it. The payment is read from the tool's arguments:
147
+ * `{ account, amountUsd, invoiceReference }` unless you pass `toPayment`. A local check: no
148
+ * receipts.
149
+ */
150
+ export declare function guardMcpPayHandler<I, Rest extends unknown[]>(handler: (args: I, ...rest: Rest) => CallToolResult | Promise<CallToolResult>, options: GuardPaymentsOptions<I>): (args: I, ...rest: Rest) => Promise<CallToolResult>;
131
151
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.js CHANGED
@@ -3,6 +3,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
3
3
  import { BondedError } from "./errors.js";
4
4
  import { finishEpisode } from "./done.js";
5
5
  import { fromBondedEnv } from "./episode.js";
6
+ import { guardPayments, isPaymentRefusal } from "./payments.js";
6
7
  export { isBondedEpisode } from "./episode.js";
7
8
  export { finishEpisode } from "./done.js";
8
9
  /**
@@ -108,4 +109,33 @@ export function mcpConfig(episode = fromBondedEnv(), options = {}) {
108
109
  export function mcpConfigJson(episode = fromBondedEnv(), options = {}) {
109
110
  return `${JSON.stringify(mcpConfig(episode, options), null, 2)}\n`;
110
111
  }
112
+ /**
113
+ * Your MCP server's pay tool handler, with every call checked against the payment mandate first
114
+ * (see `PaymentGuard`). Register the result in its place:
115
+ *
116
+ * ```ts
117
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
118
+ * server.registerTool("pay_invoice", config, guardMcpPayHandler(payInvoice, { guard }));
119
+ * ```
120
+ *
121
+ * When the guard refuses (Enforce mode blocked it, the guard is frozen, or it can't read the
122
+ * payment) your handler is not called and the client gets an error result (`isError: true`)
123
+ * naming the rules broken, so the model can react. In Monitor mode a breaking payment goes
124
+ * through and the guard's `onBreach` reports it. The payment is read from the tool's arguments:
125
+ * `{ account, amountUsd, invoiceReference }` unless you pass `toPayment`. A local check: no
126
+ * receipts.
127
+ */
128
+ export function guardMcpPayHandler(handler, options) {
129
+ const guarded = guardPayments(handler, options);
130
+ return async (args, ...rest) => {
131
+ try {
132
+ return await guarded(args, ...rest);
133
+ }
134
+ catch (error) {
135
+ if (isPaymentRefusal(error))
136
+ return { content: [{ type: "text", text: error.message }], isError: true };
137
+ throw error;
138
+ }
139
+ };
140
+ }
111
141
  //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,33 @@
1
+ import { type GuardPaymentsOptions } from "./payments.js";
2
+ export type { GuardPaymentsOptions } from "./payments.js";
3
+ /**
4
+ * The part of an OpenAI Agents SDK function tool (`tool()` from `@openai/agents`) the guard needs.
5
+ * The SDK calls `invoke` with the run context and the model's arguments as a JSON string.
6
+ */
7
+ export interface OpenAiAgentsFunctionTool {
8
+ type: "function";
9
+ name: string;
10
+ invoke: (runContext: never, input: string, details?: never) => Promise<unknown>;
11
+ }
12
+ /**
13
+ * Your own OpenAI Agents SDK pay tool, with every call checked against the payment mandate first
14
+ * (see `PaymentGuard`, or pass the hosted `PaymentGate` as `guard`). Same name, description,
15
+ * parameters, approval setting and guardrails. When the guard refuses (Enforce mode blocked it,
16
+ * the guard is frozen, the gate couldn't be reached, or it can't read the payment) your tool is not
17
+ * called and the model gets `Error: ...` naming the rules broken, so it can react. In Monitor mode
18
+ * a breaking payment goes through and the guard's `onBreach` reports it.
19
+ *
20
+ * ```ts
21
+ * import { Agent, tool } from "@openai/agents";
22
+ * import { guardOpenAiAgentsPayTool } from "@bondedhq/sdk/openai-agents";
23
+ *
24
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
25
+ * const agent = new Agent({ name: "AP clerk", tools: [...readOnlyTools, guardOpenAiAgentsPayTool(payInvoice, { guard })] });
26
+ * ```
27
+ *
28
+ * The payment is read from the tool's arguments: `{ account, amountUsd, invoiceReference }` unless
29
+ * you pass `toPayment`. If the tool needs a person's approval, the run stops for it before the
30
+ * guard is asked, so an approved payment can still be blocked.
31
+ */
32
+ export declare function guardOpenAiAgentsPayTool<T extends OpenAiAgentsFunctionTool>(payTool: T, options: GuardPaymentsOptions<Record<string, unknown>>): T;
33
+ //# sourceMappingURL=openai-agents.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { BondedError } from "./errors.js";
2
+ import { guardPayments, isPaymentRefusal } from "./payments.js";
3
+ /**
4
+ * Your own OpenAI Agents SDK pay tool, with every call checked against the payment mandate first
5
+ * (see `PaymentGuard`, or pass the hosted `PaymentGate` as `guard`). Same name, description,
6
+ * parameters, approval setting and guardrails. When the guard refuses (Enforce mode blocked it,
7
+ * the guard is frozen, the gate couldn't be reached, or it can't read the payment) your tool is not
8
+ * called and the model gets `Error: ...` naming the rules broken, so it can react. In Monitor mode
9
+ * a breaking payment goes through and the guard's `onBreach` reports it.
10
+ *
11
+ * ```ts
12
+ * import { Agent, tool } from "@openai/agents";
13
+ * import { guardOpenAiAgentsPayTool } from "@bondedhq/sdk/openai-agents";
14
+ *
15
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
16
+ * const agent = new Agent({ name: "AP clerk", tools: [...readOnlyTools, guardOpenAiAgentsPayTool(payInvoice, { guard })] });
17
+ * ```
18
+ *
19
+ * The payment is read from the tool's arguments: `{ account, amountUsd, invoiceReference }` unless
20
+ * you pass `toPayment`. If the tool needs a person's approval, the run stops for it before the
21
+ * guard is asked, so an approved payment can still be blocked.
22
+ */
23
+ export function guardOpenAiAgentsPayTool(payTool, options) {
24
+ if (payTool?.type !== "function" || typeof payTool.invoke !== "function")
25
+ throw new BondedError("CONFIG", "guardOpenAiAgentsPayTool needs a function tool");
26
+ const invoke = payTool.invoke.bind(payTool);
27
+ const guarded = guardPayments((_args, runContext, input, details) => invoke(runContext, input, details), options);
28
+ return {
29
+ ...payTool,
30
+ invoke: (async (runContext, input, details) => {
31
+ let args;
32
+ try {
33
+ args = JSON.parse(input || "{}");
34
+ }
35
+ catch {
36
+ return "Error: the payment's arguments are not valid JSON; no payment was made";
37
+ }
38
+ // A custom toPayment reads fields: null or an array would throw before the guard answers.
39
+ if (!args || typeof args !== "object" || Array.isArray(args))
40
+ return "Error: the payment's arguments must be an object; no payment was made";
41
+ try {
42
+ return await guarded(args, runContext, input, details);
43
+ }
44
+ catch (error) {
45
+ if (isPaymentRefusal(error))
46
+ return `Error: ${error.message}`;
47
+ throw error;
48
+ }
49
+ }),
50
+ };
51
+ }
52
+ //# sourceMappingURL=openai-agents.js.map
@@ -0,0 +1,227 @@
1
+ import { type BudgetTerms, type GateRule, type PaidPayment, type PayableInvoice, type PayableRefund, type PaymentAttempt, type PaymentLimits, type SignedGateReceipt, type VerifiedChange } from "@bondedhq/shared";
2
+ import { type BudgetRequest, type PaymentBudget } from "./budget.js";
3
+ import { BondedError, PaymentBlockedError, PaymentFrozenError, PaymentGateError } from "./errors.js";
4
+ /** The off-chain payment mandate an agent's payments are checked against. */
5
+ export interface PaymentMandate extends PaymentLimits {
6
+ /** Monitor lets a breaking payment through and reports it; Enforce blocks it. */
7
+ mode: "monitor" | "enforce";
8
+ /** After the first breach, refuse every payment until a person unfreezes the guard. */
9
+ freezeOnBreach: boolean;
10
+ /**
11
+ * False when your payments have no invoices: a payment needs a vendor on the list and to stay
12
+ * within the limits, and its reference is optional. Refunds still need an approved refund.
13
+ */
14
+ invoiceMatching?: boolean;
15
+ /** Payments allowed in any hour, with invoice matching off. No limit unless set. */
16
+ maxPaymentsPerHour?: number;
17
+ }
18
+ /** A supplier as your vendor master holds it. Its account is one the agent may pay. */
19
+ export interface PaymentVendor {
20
+ id: string;
21
+ /** Bank account (an IBAN or account number). Compared without spaces, case-insensitively. */
22
+ account: string;
23
+ name?: string;
24
+ }
25
+ /** A list the guard reads on every check: fixed, or read from your own system each time. */
26
+ export type PaymentSource<T> = readonly T[] | (() => readonly T[] | Promise<readonly T[]>);
27
+ /** What the guard remembers between payments. Persist it to keep it across restarts. */
28
+ export interface PaymentGuardState {
29
+ /** Payments made through the guard (and any made before it, if you restore them), oldest first. */
30
+ payments: PaidPayment[];
31
+ /** Frozen after a breach: every payment is refused until `unfreeze()`. */
32
+ frozen: boolean;
33
+ /**
34
+ * Bank changes made without a verified ticket (let through in Monitor mode). The vendor master
35
+ * may now show these accounts, but they never count as the vendor's: paying one is
36
+ * `unknown_payee`.
37
+ */
38
+ unverifiedChanges: VerifiedChange[];
39
+ /**
40
+ * Open budgets (`budget()`): their terms and what was counted against them. Each one's whole
41
+ * amount is in `payments` until it closes. Absent in state saved before budgets existed.
42
+ */
43
+ budgets?: LocalBudgetState[];
44
+ /** When budget payments were made in the last hour (Unix milliseconds), for the hourly limit. */
45
+ budgetSpends?: number[];
46
+ }
47
+ /** An open local budget, as the guard's state keeps it. */
48
+ export interface LocalBudgetState {
49
+ id: string;
50
+ terms: BudgetTerms;
51
+ spentMicros: number;
52
+ payments: number;
53
+ }
54
+ /** One payment (or bank change) that broke the mandate, as the guard reports it. */
55
+ export interface PaymentBreach {
56
+ kind: "invoice" | "refund" | "vendor_change" | "budget";
57
+ /** Every rule broken, most serious first. */
58
+ rules: GateRule[];
59
+ /** Each rule in plain words (`GATE_RULE_WORDS`), in the same order. */
60
+ reasons: string[];
61
+ /** Enforce mode stopped it: the pay function was not called. */
62
+ blocked: boolean;
63
+ account: string;
64
+ /** The invoice or refund reference; empty for a bank change or a budget. */
65
+ reference: string;
66
+ /** The vendor whose verified account this is (or, for a bank change, the vendor changed). */
67
+ vendorId: string | null;
68
+ amountUsd: number;
69
+ /** If it went out: paid to an account outside the vendor master, gone for good. */
70
+ lossUsd: number;
71
+ /** If it went out: paid to a known payee beyond what it was owed, claimable back. */
72
+ overpaidUsd: number;
73
+ /** UTC day it counts against, YYYY-MM-DD. */
74
+ day: string;
75
+ /** From the hosted gate (`PaymentGate`): the decision's id in its log. */
76
+ decisionId?: string;
77
+ /** From the hosted gate: Bonded's signed receipt for the decision. */
78
+ receipt?: SignedGateReceipt;
79
+ }
80
+ /** The rule verdict for a payment, without making it. */
81
+ export interface PaymentVerdict {
82
+ /** Every rule the payment breaks, most serious first. Empty when it is allowed. */
83
+ rules: GateRule[];
84
+ reasons: string[];
85
+ amountUsd: number;
86
+ reference: string;
87
+ vendorId: string | null;
88
+ lossUsd: number;
89
+ overpaidUsd: number;
90
+ /** The guard is frozen: the payment would be refused whatever the rules say. */
91
+ frozen: boolean;
92
+ }
93
+ export interface PaymentGuardOptions {
94
+ mandate: PaymentMandate;
95
+ /** The vendor master: each vendor's account on file. */
96
+ vendors: PaymentSource<PaymentVendor>;
97
+ /** Invoices the business approved for payment. */
98
+ invoices?: PaymentSource<PayableInvoice>;
99
+ /** Customer refunds finance approved. */
100
+ refunds?: PaymentSource<PayableRefund>;
101
+ /**
102
+ * Bank changes finance verified out of band (a call to the number on file). A ticket's account
103
+ * is a known payee for its vendor, and the only way a vendor's account may change.
104
+ */
105
+ changes?: PaymentSource<VerifiedChange>;
106
+ /** State to start from, as `onStateChange` last saved it (or `state()` returned). */
107
+ state?: PaymentGuardState;
108
+ /** Called with the new state after every change, inside the guard's lock. Save it here. */
109
+ onStateChange?: (state: PaymentGuardState) => void | Promise<void>;
110
+ /**
111
+ * Called with every breach before the guard acts on it: blocked in Enforce mode, let through
112
+ * in Monitor mode. If it throws, the payment is not made.
113
+ */
114
+ onBreach?: (breach: PaymentBreach) => void | Promise<void>;
115
+ now?: () => Date;
116
+ }
117
+ /**
118
+ * The payment mandate, checked inside your own process: the same rules (`@bondedhq/shared`'s
119
+ * payment mandate) the Arena rates agents against, with no network and no model. You give it the
120
+ * mandate and your vendor master, approved invoices and refunds, and verified bank changes; it
121
+ * keeps the payments made and the frozen flag, and hands them to `onStateChange` to persist.
122
+ *
123
+ * Wrap your pay function with `guardPayments` (or a framework helper) so every payment is checked
124
+ * first. In Enforce mode a payment that breaks a rule throws `PaymentBlockedError` and your pay
125
+ * function is never called; in Monitor mode it goes through and `onBreach` reports it. A frozen
126
+ * guard refuses everything (`PaymentFrozenError`).
127
+ *
128
+ * It is a local check: the agent's code can still call the pay function without it, and it makes
129
+ * no receipts. Payments are checked and made one at a time, so two at once can't both pass the
130
+ * duplicate or daily checks.
131
+ */
132
+ export declare class PaymentGuard {
133
+ #private;
134
+ readonly mandate: Readonly<PaymentMandate>;
135
+ constructor(options: PaymentGuardOptions);
136
+ get frozen(): boolean;
137
+ /** A copy of what the guard remembers, to persist or inspect. */
138
+ state(): PaymentGuardState;
139
+ /** Lets payments through again after a freeze. For a person who reviewed the breach. */
140
+ unfreeze(): Promise<void>;
141
+ /** The rule verdict for a payment, without making or recording it. */
142
+ check(attempt: PaymentAttempt, kind?: "invoice" | "refund"): Promise<PaymentVerdict>;
143
+ /**
144
+ * Checks a payment, then calls `pay` if the mandate allows it (or Monitor mode lets it through)
145
+ * and records it once `pay` returns. Throws `PaymentBlockedError` (Enforce), `PaymentFrozenError`
146
+ * or `BondedError` `INVALID_PAYMENT` without calling `pay`. A `pay` that throws records nothing.
147
+ */
148
+ pay<R>(attempt: PaymentAttempt, pay: () => R | Promise<R>, kind?: "invoice" | "refund"): Promise<R>;
149
+ /**
150
+ * Checks a vendor bank change, then calls `update` if a verified ticket covers that vendor and
151
+ * account (or Monitor mode lets it through). An unverified change breaks
152
+ * `unverified_vendor_change`; let through in Monitor mode, its account still never counts as
153
+ * the vendor's.
154
+ */
155
+ changeVendorAccount<R>(request: {
156
+ vendorId: string;
157
+ account: string;
158
+ }, update: () => R | Promise<R>): Promise<R>;
159
+ /**
160
+ * Opens a budget: one check for many small payments to one payee (see `PaymentBudget`). Needs
161
+ * `invoiceMatching: false`. The payee must be a vendor's account (`unknown_payee`), the whole
162
+ * budget must fit in what's left of the day's limit (`over_daily_cap`), and with
163
+ * `maxPaymentsPerHour` there must be room for a payment (`over_hourly_limit`); the budget then
164
+ * covers what's left of the hour. Enforce mode throws `PaymentBlockedError`; Monitor mode opens it
165
+ * and reports the breach. The whole budget counts toward the day until `close()`, when what was
166
+ * spent (rounded up to the cent) counts instead. A budget past its end is closed the next time
167
+ * one is opened.
168
+ */
169
+ budget(request: BudgetRequest): Promise<PaymentBudget>;
170
+ }
171
+ /** A payment guard: see `PaymentGuard`. */
172
+ export declare function paymentGuard(options: PaymentGuardOptions): PaymentGuard;
173
+ export interface GuardPaymentsOptions<I> {
174
+ /**
175
+ * A `PaymentGuard`, or the hosted `PaymentGate`. Share one between your invoice and refund
176
+ * functions, so limits and freezes are shared.
177
+ */
178
+ guard: Pick<PaymentGuard, "pay">;
179
+ /** `invoice` (the default) pays approved invoices; `refund` pays approved customer refunds. */
180
+ kind?: "invoice" | "refund";
181
+ /**
182
+ * Reads the payment from your function's first argument. Default: `{ account, amountUsd,
183
+ * invoiceReference }` (or `reference`), the Arena's `pay_invoice` and `issue_refund` arguments.
184
+ */
185
+ toPayment?: (input: I) => PaymentAttempt;
186
+ }
187
+ /**
188
+ * Wraps your own pay function so every call is checked against the payment mandate first. The
189
+ * wrapped function takes the same arguments and returns what yours returns. In Enforce mode a
190
+ * payment that breaks a rule throws `PaymentBlockedError` and your function is not called; in
191
+ * Monitor mode it is called and the guard's `onBreach` reports the breach. A frozen guard throws
192
+ * `PaymentFrozenError`.
193
+ *
194
+ * ```ts
195
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
196
+ * const pay = guardPayments(bank.pay, { guard });
197
+ * await pay({ account: "DE89 3704 0044 0532 0130 00", amountUsd: 1840.5, invoiceReference: "ACM-2291" });
198
+ * ```
199
+ *
200
+ * A local check: it makes no receipts, and code that calls your function directly skips it.
201
+ */
202
+ export declare function guardPayments<I, Rest extends unknown[], R>(pay: (input: I, ...rest: Rest) => R | Promise<R>, options: GuardPaymentsOptions<I>): (input: I, ...rest: Rest) => Promise<R>;
203
+ export interface GuardVendorChangesOptions<I> {
204
+ /** A `PaymentGuard`, or the hosted `PaymentGate`. */
205
+ guard: Pick<PaymentGuard, "changeVendorAccount">;
206
+ /**
207
+ * Reads the change from your function's first argument. Default: `{ vendorId, account }`, and
208
+ * `ticket` when there is one (the hosted gate checks it).
209
+ */
210
+ toChange?: (input: I) => {
211
+ vendorId: string;
212
+ account: string;
213
+ ticket?: string;
214
+ };
215
+ }
216
+ /**
217
+ * Wraps your own "change a vendor's bank account" function: a change no verified ticket covers
218
+ * breaks `unverified_vendor_change`. Enforce mode throws `PaymentBlockedError` without calling
219
+ * it; Monitor mode calls it, reports the breach, and never treats the new account as the vendor's.
220
+ */
221
+ export declare function guardVendorChanges<I, Rest extends unknown[], R>(update: (input: I, ...rest: Rest) => R | Promise<R>, options: GuardVendorChangesOptions<I>): (input: I, ...rest: Rest) => Promise<R>;
222
+ /**
223
+ * The payment guard or gate refused (blocked, frozen, arguments it can't check, or the gate gave no
224
+ * answer to act on): no money moved.
225
+ */
226
+ export declare function isPaymentRefusal(error: unknown): error is PaymentBlockedError | PaymentFrozenError | PaymentGateError | BondedError;
227
+ //# sourceMappingURL=payments.d.ts.map